From 17c0536958b3d64b4be8790ebbb18bbdb07c4ec1 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Sat, 11 Jul 2026 12:25:52 +0800 Subject: [PATCH] fix: enable only curated agent skills --- .../0018-agent-execution-surface-bounded-by-workspace.md | 3 ++- hub/curated-skills-plugin/.claude-plugin/plugin.json | 2 +- hub/package-lock.json | 4 ++-- hub/package.json | 2 +- hub/src/agent/runner.ts | 5 ++++- hub/test/unit/runner.test.ts | 7 ++++--- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/adr/0018-agent-execution-surface-bounded-by-workspace.md b/docs/adr/0018-agent-execution-surface-bounded-by-workspace.md index 82a145b..f76a893 100644 --- a/docs/adr/0018-agent-execution-surface-bounded-by-workspace.md +++ b/docs/adr/0018-agent-execution-surface-bounded-by-workspace.md @@ -125,7 +125,8 @@ The boundary is enforced by the Claude Code SDK's built-in sandbox - Platform-curated Agent skills are immutable Hub release assets, loaded as a programmatic local plugin from a release-owned path. The sandbox exposes that path read-only, and the SDK receives only plugin-qualified allowlist names - through its `skills` option. Filesystem setting sources remain disabled, so a + through its `skills` option; SDK-bundled skills are disabled. Filesystem + setting sources remain disabled, so a project cannot register another skill or widen its tools through `.claude` settings. Requested skill ids are recorded on `run.created`; SDK initialization/results remain the authoritative evidence that loading diff --git a/hub/curated-skills-plugin/.claude-plugin/plugin.json b/hub/curated-skills-plugin/.claude-plugin/plugin.json index 6201838..0e6db4d 100644 --- a/hub/curated-skills-plugin/.claude-plugin/plugin.json +++ b/hub/curated-skills-plugin/.claude-plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "cph-curated", "description": "Reviewed curriculum-production skills shipped with the Curriculum Project Hub.", - "version": "0.0.1" + "version": "0.0.2" } diff --git a/hub/package-lock.json b/hub/package-lock.json index 5975297..825db59 100644 --- a/hub/package-lock.json +++ b/hub/package-lock.json @@ -1,12 +1,12 @@ { "name": "@paradigm/hub", - "version": "0.0.8", + "version": "0.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@paradigm/hub", - "version": "0.0.8", + "version": "0.0.9", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", diff --git a/hub/package.json b/hub/package.json index bac8f72..327ba74 100644 --- a/hub/package.json +++ b/hub/package.json @@ -1,6 +1,6 @@ { "name": "@paradigm/hub", - "version": "0.0.8", + "version": "0.0.9", "private": true, "type": "module", "engines": { diff --git a/hub/src/agent/runner.ts b/hub/src/agent/runner.ts index fd6643d..09099a7 100644 --- a/hub/src/agent/runner.ts +++ b/hub/src/agent/runner.ts @@ -151,7 +151,9 @@ export async function runAgent(req: RunRequest): Promise { type QueryOptions = NonNullable[0]["options"]>; const options: QueryOptions = { cwd: security.cwd, - tools: [...toolConfig.tools], + // `skills` controls discovery/allowlisting, but an explicit `tools` + // list still has to expose the Skill dispatcher itself. + tools: [...toolConfig.tools, "Skill"], allowedTools: [...toolConfig.allowedTools], maxTurns: cap, includePartialMessages: true, @@ -169,6 +171,7 @@ export async function runAgent(req: RunRequest): Promise { // The project workspace is untrusted input. Do not load user/project // settings that could widen tools, hooks, MCP servers, or sandbox paths. settingSources: [], + settings: { disableBundledSkills: true }, plugins: [{ type: "local", path: security.skillPluginRoot, skipMcpDiscovery: true }], skills: [...security.skillIds], strictMcpConfig: true, diff --git a/hub/test/unit/runner.test.ts b/hub/test/unit/runner.test.ts index 768367d..3752b50 100644 --- a/hub/test/unit/runner.test.ts +++ b/hub/test/unit/runner.test.ts @@ -112,6 +112,7 @@ describe("runAgent", () => { permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, settingSources: [], + settings: { disableBundledSkills: true }, plugins: [expect.objectContaining({ type: "local", skipMcpDiscovery: true })], skills: ["cph-curated:outline", "cph-curated:lesson-project", "cph-curated:data-processing-spec"], strictMcpConfig: true, @@ -182,13 +183,13 @@ describe("runAgent", () => { expect(queryMock.mock.calls[0]?.[0]).toMatchObject({ options: { - tools: ["Read", "Bash"], + tools: ["Read", "Bash", "Skill"], allowedTools: ["Read", "Bash", "mcp__cph_hub__send_file"], }, }); }); - it("disables all SDK tools for an empty role tool whitelist", async () => { + it("keeps only the curated Skill dispatcher for an empty role tool whitelist", async () => { queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-1"))); await runAgent({ @@ -204,7 +205,7 @@ describe("runAgent", () => { expect(queryMock.mock.calls[0]?.[0]).toMatchObject({ options: { - tools: [], + tools: ["Skill"], allowedTools: [], }, });