refactor(skills): extract SkillRuntime abstraction from Agent#1074
Open
lyingbug wants to merge 2 commits intoTencent:mainTencent/WeKnora:mainfrom
lyingbug:feat/skill-runtime-decouplinglyingbug/WeKnora:feat/skill-runtime-decouplingCopy head branch name to clipboard
Open
refactor(skills): extract SkillRuntime abstraction from Agent#1074lyingbug wants to merge 2 commits intoTencent:mainTencent/WeKnora:mainfrom lyingbug:feat/skill-runtime-decouplinglyingbug/WeKnora:feat/skill-runtime-decouplingCopy head branch name to clipboard
lyingbug wants to merge 2 commits intoTencent:mainTencent/WeKnora:mainfrom
lyingbug:feat/skill-runtime-decouplinglyingbug/WeKnora:feat/skill-runtime-decouplingCopy head branch name to clipboard
Conversation
Decouple Skill capabilities from AgentEngine so they can be consumed by HTTP handlers, workflows and any future module via a stable interface. Changes: - Introduce skills.SkillRuntime interface (runtime.go) plus SkillRepository abstraction (repository.go) so the storage layer (currently filesystem only) can be swapped for DB / OSS / marketplace implementations later without touching consumers. - Add fileSystemRuntime (runtime_fs.go) as the default implementation, consolidating the previous Manager and Loader logic with explicit concurrency safety and an idempotent Initialize. - Centralise sandbox construction and env-var parsing in factory.go (NewRuntime / NewRuntimeFromEnv), removing duplicated bootstrap code from agent_service. - Register SkillRuntime as a singleton in the DI container so a single filesystem scan is shared across all agent requests; per-request AllowedSkills filtering is applied at the engine layer via FilterMetadata. - Tools (read_skill, execute_skill_script), AgentEngine and SkillHandler now depend on the SkillRuntime interface instead of the concrete *skills.Manager type, enabling easy mocking and future decoration (RBAC, audit logging, batch execution). - Delete obsolete files: skills/manager.go, skills/loader.go, application/service/skill_service.go, types/interfaces/skill.go. - Update tests to cover the new Repository / Runtime API including path-traversal protection, allow-list enforcement and disabled state. HTTP contract (GET /api/v1/skills response shape and skills_available field), AgentConfig fields and WEKNORA_* environment variables are all preserved — no operator-visible changes.
Follow-ups from the review of the initial extraction: - Remove AgentConfig.SkillDirs. The field was silently ignored after the runtime became a DI singleton, which hid a behaviour change behind an unchanged JSON surface. Drop it plus the two now-dead assignments in session_agent_qa.go. Agents now rely on the process-wide skill directory resolved from WEKNORA_SKILLS_DIR at startup. - Rebuild fsRepository cache from scratch inside Discover so Reload correctly drops skills that have disappeared from the filesystem. - Simplify NewRuntime / NewRuntimeFromEnv to return a single SkillRuntime value. The error slot never produced a non-nil error — sandbox construction failures already fall back to NewDisabledManager internally. - Remove the unused MustBeReady helper. - Harden path-traversal protection in fsRepository.ReadFile by comparing against absSkill + PathSeparator, preventing a sibling directory whose name starts with the target skill's name from being treated as nested inside it. Covered by a new regression test. - Switch fileSystemRuntime.Initialize to sync.Once so concurrent first-time callers no longer trigger duplicate filesystem scans. Reload bypasses the once guard deliberately — it is the retry path. - Fix struct field alignment in engine.go and wrap newFileSystemRuntime signature below the 120-char line limit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Decouple Skill capabilities from
AgentEngineso they can be consumed by HTTP handlers, workflows, and any future module via a stable interface. The current design tightly couples*skills.Managerto the agent engine and rebuilds it on every agent request, blocking reuse and making the layer hard to evolve (no DB persistence, no RBAC decoration, no mocking, etc.).This PR introduces a clean three-layer architecture without changing any external contract (HTTP response shape, env vars,
AgentConfigfields are all preserved).Architecture
Key changes
New abstractions
internal/agent/skills/runtime.go—SkillRuntimeinterface +ExecuteRequest/SkillInfointernal/agent/skills/repository.go—SkillRepositoryinterface +fsRepository(path-traversal protection preserved)internal/agent/skills/runtime_fs.go—fileSystemRuntimeconsolidating the previousManagerandLoader, with explicit concurrency safety and idempotentInitializeinternal/agent/skills/factory.go— centralised sandbox construction and env-var parsing (NewRuntime/NewRuntimeFromEnv/FilterMetadata)Consumers switched to the interface
tools/skill_read.go,tools/skill_execute.godepend onSkillRuntime(mockable)agent/engine.gofield renamedskillsManager → skillRuntime; per-requestAllowedSkillsfiltering applied viaFilterMetadatasince the runtime is a global singletonhandler/skill_handler.gono longer reads env vars;skills_availablecomes fromruntime.SandboxAvailable()DI
container.goregistersskills.NewRuntimeFromEnvas a singleton (single filesystem scan shared across requests, vs. per-request reconstruction before)agent_service.NewAgentServicenow takesskills.SkillRuntimeas a dependency; the 68-lineinitializeSkillsManagerhelper is removedDeleted
internal/agent/skills/manager.gointernal/agent/skills/loader.gointernal/application/service/skill_service.gointernal/types/interfaces/skill.goBackwards compatibility
GET /api/v1/skillsresponse format unchanged (includingskills_available)AgentConfig.{SkillsEnabled, SkillDirs, AllowedSkills}JSON fields unchangedWEKNORA_SKILLS_DIR,WEKNORA_SANDBOX_*unchangedskills/preloaded/<name>/SKILL.md) unchangedBehaviour difference (intended improvement)
runtime.Reload()(no HTTP endpoint added in this PR — can come later).Future extension points unlocked
With this refactor, the following can each land as a small isolated PR:
SkillRepositoryimplementationRuntimedecoratorExecuteBatch)Test plan
go build ./...passesgo vet ./...passesgo test ./internal/agent/...passes (including new tests coveringRepository.Discover,GetByName,ReadFilepath-traversal rejection,Runtime.Initialize, allow-list filtering, disabled state,FilterMetadata)grep -r "skills.Manager\|NewSkillService\|interfaces.SkillService" internal/returns emptyWEKNORA_SANDBOX_MODE=docker+ skills enabled in agent config; verifyread_skillandexecute_skill_scripttools work end-to-end against a preloaded skillGET /api/v1/skillsreturns the same JSON shape (data + skills_available) as before