shadow-claw 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/main/skill-creator/SKILL.md +39 -0
- package/.agents/skills/main/skill-creator/references/declarative-pipelines.md +44 -0
- package/.agents/skills/main/skill-creator/references/declarative-tools.md +35 -0
- package/.agents/skills/main/skill-creator/references/frontmatter-spec.md +50 -0
- package/.well-known/agent-skills/index.json +14 -0
- package/README.md +5 -3
- package/bin/build/build.mjs +146 -15
- package/bin/build/build.test.mjs +57 -0
- package/bin/cli.mjs +26 -0
- package/bin/commands/mcp.mjs +277 -55
- package/bin/commands/mcp.test.mjs +275 -1
- package/bin/commands/skills-index.mjs +328 -0
- package/bin/commands/skills-index.test.mjs +161 -0
- package/bin/site-config/utils/custom-elements/get-approved-custom-element-scripts.test.mjs +2 -0
- package/bin/site-config/utils/custom-elements/sanitize-embedded-custom-element-scripts.test.mjs +10 -1
- package/dist/lib/components/index.js +25 -25
- package/dist/lib/components/settings/shadow-claw-accounts.js +1 -1
- package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/settings/shadow-claw-git.js +1 -1
- package/dist/lib/components/settings/shadow-claw-integrations.js +1 -1
- package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
- package/dist/lib/components/settings/shadow-claw-mcp-remote.js +1 -1
- package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
- package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
- package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw-a2ui.js +1 -1
- package/dist/lib/components/shadow-claw-accounts.js +1 -1
- package/dist/lib/components/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/shadow-claw-channels.js +1 -1
- package/dist/lib/components/shadow-claw-chat.js +1 -1
- package/dist/lib/components/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/shadow-claw-conversations.js +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw-files.js +1 -1
- package/dist/lib/components/shadow-claw-git.js +1 -1
- package/dist/lib/components/shadow-claw-integrations.js +1 -1
- package/dist/lib/components/shadow-claw-llm.js +1 -1
- package/dist/lib/components/shadow-claw-mcp-remote.js +1 -1
- package/dist/lib/components/shadow-claw-networking.js +1 -1
- package/dist/lib/components/shadow-claw-page-header.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/components/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/shadow-claw-settings.js +1 -1
- package/dist/lib/components/shadow-claw-storage.js +1 -1
- package/dist/lib/components/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/shadow-claw-tasks.js +1 -1
- package/dist/lib/components/shadow-claw-terminal.js +1 -1
- package/dist/lib/components/shadow-claw-tools.js +1 -1
- package/dist/lib/components/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/{configurePeerJs-CrlGan0m.js → configurePeerJs-oODzltaC.js} +2 -2
- package/dist/lib/{configurePeerJs-CrlGan0m.js.map → configurePeerJs-oODzltaC.js.map} +1 -1
- package/dist/lib/core/control-plane-client.d.ts +1 -0
- package/dist/lib/{downloadGroupFile-DboUjmp2.js → downloadGroupFile-DmcBQU1n.js} +2 -2
- package/dist/lib/{downloadGroupFile-DboUjmp2.js.map → downloadGroupFile-DmcBQU1n.js.map} +1 -1
- package/dist/lib/{file-viewer-BrK2Wxgh.js → file-viewer-C3YKnq_-.js} +4 -3
- package/dist/lib/{file-viewer-BrK2Wxgh.js.map → file-viewer-C3YKnq_-.js.map} +1 -1
- package/dist/lib/index.js +26 -26
- package/dist/lib/{initControlPlane-CsO4UR6_.js → initControlPlane-C2zURVIS.js} +86 -13
- package/dist/lib/initControlPlane-C2zURVIS.js.map +1 -0
- package/dist/lib/{markdown-eeSTFnMA.js → markdown-C6EHl9zs.js} +3 -3
- package/dist/lib/{markdown-eeSTFnMA.js.map → markdown-C6EHl9zs.js.map} +1 -1
- package/dist/lib/{orchestrator-CJvoC2Uu.js → orchestrator-CACLa_Ro.js} +7 -6
- package/dist/lib/{orchestrator-CJvoC2Uu.js.map → orchestrator-CACLa_Ro.js.map} +1 -1
- package/dist/lib/{orchestrator-DCfomsn9.js → orchestrator-KRjx0eba.js} +6 -534
- package/dist/lib/{orchestrator-DCfomsn9.js.map → orchestrator-KRjx0eba.js.map} +1 -1
- package/dist/lib/parsePath-DwC_YZjw.js +16 -0
- package/dist/lib/parsePath-DwC_YZjw.js.map +1 -0
- package/dist/lib/{peerjs-Dp02bXcG.js → peerjs-DFWOBcXf.js} +4 -4
- package/dist/lib/{peerjs-Dp02bXcG.js.map → peerjs-DFWOBcXf.js.map} +1 -1
- package/dist/lib/{readGroupFileBytes-Bve-e5e_.js → readGroupFileBytes-C8oOe8Iu.js} +3 -16
- package/dist/lib/readGroupFileBytes-C8oOe8Iu.js.map +1 -0
- package/dist/lib/security/custom-element-security.d.ts +8 -0
- package/dist/lib/server/control-plane-types.d.ts +1 -1
- package/dist/lib/{shadow-claw-D8xINM3c.js → shadow-claw-6UHdiy4a.js} +5 -5
- package/dist/lib/{shadow-claw-D8xINM3c.js.map → shadow-claw-6UHdiy4a.js.map} +1 -1
- package/dist/lib/{shadow-claw-a2ui-7rJqG4Ke.js → shadow-claw-a2ui-CpZUTzlh.js} +5 -4
- package/dist/lib/{shadow-claw-a2ui-7rJqG4Ke.js.map → shadow-claw-a2ui-CpZUTzlh.js.map} +1 -1
- package/dist/lib/{shadow-claw-accounts-CdruwN8A.js → shadow-claw-accounts-03oJ__XC.js} +2 -2
- package/dist/lib/{shadow-claw-accounts-CdruwN8A.js.map → shadow-claw-accounts-03oJ__XC.js.map} +1 -1
- package/dist/lib/{shadow-claw-channel-config-Bj0-Nk3t.js → shadow-claw-channel-config-DmfwJc6G.js} +3 -3
- package/dist/lib/{shadow-claw-channel-config-Bj0-Nk3t.js.map → shadow-claw-channel-config-DmfwJc6G.js.map} +1 -1
- package/dist/lib/{shadow-claw-channels-C_jI4KhP.js → shadow-claw-channels-1N6Jl-By.js} +3 -3
- package/dist/lib/{shadow-claw-channels-C_jI4KhP.js.map → shadow-claw-channels-1N6Jl-By.js.map} +1 -1
- package/dist/lib/{shadow-claw-chat-B16MspoP.js → shadow-claw-chat-BMoqQJRK.js} +10 -10
- package/dist/lib/{shadow-claw-chat-B16MspoP.js.map → shadow-claw-chat-BMoqQJRK.js.map} +1 -1
- package/dist/lib/{shadow-claw-control-plane-x8Ldid_h.js → shadow-claw-control-plane-UG0o7pCz.js} +6 -6
- package/dist/lib/{shadow-claw-control-plane-x8Ldid_h.js.map → shadow-claw-control-plane-UG0o7pCz.js.map} +1 -1
- package/dist/lib/{shadow-claw-conversations-Ct1kAYdB.js → shadow-claw-conversations-BwXep3iu.js} +4 -3
- package/dist/lib/{shadow-claw-conversations-Ct1kAYdB.js.map → shadow-claw-conversations-BwXep3iu.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-BfDHHgpA.js → shadow-claw-file-viewer-DUxzVxDT.js} +14 -10
- package/dist/lib/shadow-claw-file-viewer-DUxzVxDT.js.map +1 -0
- package/dist/lib/{shadow-claw-files-EYhOPYIH.js → shadow-claw-files-DIREEnT2.js} +7 -7
- package/dist/lib/{shadow-claw-files-EYhOPYIH.js.map → shadow-claw-files-DIREEnT2.js.map} +1 -1
- package/dist/lib/{shadow-claw-git-tcSpT-P0.js → shadow-claw-git-Bg1SWgsF.js} +4 -4
- package/dist/lib/{shadow-claw-git-tcSpT-P0.js.map → shadow-claw-git-Bg1SWgsF.js.map} +1 -1
- package/dist/lib/{shadow-claw-integrations-NbcCx_ZI.js → shadow-claw-integrations-Ds-CLPhj.js} +2 -2
- package/dist/lib/{shadow-claw-integrations-NbcCx_ZI.js.map → shadow-claw-integrations-Ds-CLPhj.js.map} +1 -1
- package/dist/lib/{shadow-claw-llm-BazJdD5f.js → shadow-claw-llm-CazBrVpM.js} +3 -3
- package/dist/lib/{shadow-claw-llm-BazJdD5f.js.map → shadow-claw-llm-CazBrVpM.js.map} +1 -1
- package/dist/lib/{shadow-claw-mcp-remote-CYmhpg7a.js → shadow-claw-mcp-remote-CT5Q-54E.js} +2 -2
- package/dist/lib/{shadow-claw-mcp-remote-CYmhpg7a.js.map → shadow-claw-mcp-remote-CT5Q-54E.js.map} +1 -1
- package/dist/lib/{shadow-claw-networking-DImPxe0H.js → shadow-claw-networking-Df85tCbO.js} +2 -2
- package/dist/lib/{shadow-claw-networking-DImPxe0H.js.map → shadow-claw-networking-Df85tCbO.js.map} +1 -1
- package/dist/lib/{shadow-claw-page-header-CcHUfFUx.js → shadow-claw-page-header-Cti2fzf-.js} +2 -2
- package/dist/lib/{shadow-claw-page-header-CcHUfFUx.js.map → shadow-claw-page-header-Cti2fzf-.js.map} +1 -1
- package/dist/lib/{shadow-claw-pages-D54jF1Qq.js → shadow-claw-pages-BgPyIEZO.js} +15 -11
- package/dist/lib/shadow-claw-pages-BgPyIEZO.js.map +1 -0
- package/dist/lib/{shadow-claw-peerjs-B9-T8Nlx.js → shadow-claw-peerjs-xkBXILed.js} +3 -3
- package/dist/lib/{shadow-claw-peerjs-B9-T8Nlx.js.map → shadow-claw-peerjs-xkBXILed.js.map} +1 -1
- package/dist/lib/{shadow-claw-settings-Bt1VAe-1.js → shadow-claw-settings-CN89xRVU.js} +4 -4
- package/dist/lib/{shadow-claw-settings-Bt1VAe-1.js.map → shadow-claw-settings-CN89xRVU.js.map} +1 -1
- package/dist/lib/{shadow-claw-storage-CFOjGf1d.js → shadow-claw-storage-VnhcObCe.js} +3 -3
- package/dist/lib/{shadow-claw-storage-CFOjGf1d.js.map → shadow-claw-storage-VnhcObCe.js.map} +1 -1
- package/dist/lib/{shadow-claw-task-server-BiKa2GOm.js → shadow-claw-task-server-jgEC2hHB.js} +2 -2
- package/dist/lib/{shadow-claw-task-server-BiKa2GOm.js.map → shadow-claw-task-server-jgEC2hHB.js.map} +1 -1
- package/dist/lib/{shadow-claw-tasks-DDHd8W4d.js → shadow-claw-tasks-i_evIufy.js} +6 -6
- package/dist/lib/{shadow-claw-tasks-DDHd8W4d.js.map → shadow-claw-tasks-i_evIufy.js.map} +1 -1
- package/dist/lib/{shadow-claw-terminal-kB6_tWKz.js → shadow-claw-terminal-CwScROlu.js} +2 -2
- package/dist/lib/{shadow-claw-terminal-kB6_tWKz.js.map → shadow-claw-terminal-CwScROlu.js.map} +1 -1
- package/dist/lib/{shadow-claw-tools-DxXIm0ku.js → shadow-claw-tools-DD3fyXP4.js} +5 -4
- package/dist/lib/{shadow-claw-tools-DxXIm0ku.js.map → shadow-claw-tools-DD3fyXP4.js.map} +1 -1
- package/dist/lib/{shadow-claw-webvm-Dc8oo7ao.js → shadow-claw-webvm-DbJq4I3-.js} +2 -2
- package/dist/lib/{shadow-claw-webvm-Dc8oo7ao.js.map → shadow-claw-webvm-DbJq4I3-.js.map} +1 -1
- package/dist/lib/{syncWebMcpRegistration-jyRGbC9w.js → syncWebMcpRegistration-DNLnQ-jz.js} +3 -2
- package/dist/lib/{syncWebMcpRegistration-jyRGbC9w.js.map → syncWebMcpRegistration-DNLnQ-jz.js.map} +1 -1
- package/dist/lib/tools-DwwB9RBa.js +546 -0
- package/dist/lib/tools-DwwB9RBa.js.map +1 -0
- package/dist/lib/{trusted-types-CkLDpLiZ.js → trusted-types-Dx0RZlrd.js} +19 -8
- package/dist/lib/trusted-types-Dx0RZlrd.js.map +1 -0
- package/dist/lib/utils/index.js +1 -1
- package/dist/lib/utils/utils.js +1 -1
- package/dist/public/.well-known/agent-skills/index.json +14 -0
- package/dist/public/AGENTS.md +5 -3
- package/dist/public/README.md +5 -3
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/{configurePeerJs-COcwE0E7.js → configurePeerJs-BqgeBjP7.js} +1 -1
- package/dist/public/custom-element-security-FU04Cq05.js +1 -0
- package/dist/public/dist-B9m1QM9v.js +58 -0
- package/dist/public/docs/README.md +31 -31
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/subsystems/a2ui.md +11 -1
- package/dist/public/docs/subsystems/cli.md +29 -0
- package/dist/public/docs/subsystems/control-plane.md +8 -2
- package/dist/public/docs/subsystems/custom-element-security.md +19 -9
- package/dist/public/docs/subsystems/mcp-server.md +39 -15
- package/dist/public/docs/subsystems/skills.md +30 -0
- package/dist/public/docs/subsystems/webmcp.md +9 -0
- package/dist/public/downloadGroupFile-D8KRJGGL.js +1 -0
- package/dist/public/e2e/README.md +7 -4
- package/dist/public/{e2e-bridge-CHARpbpW.js → e2e-bridge-Ceqr5kGc.js} +1 -1
- package/dist/public/effect-BJCrpFdp.js +1 -0
- package/dist/public/file-viewer-C-6Jc_tb.js +1 -0
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/initControlPlane-BN9cWyO0.js +1 -0
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/{markdown-1I3O6Mmd.js → markdown-C3M4OW_A.js} +1 -1
- package/dist/public/{orchestrator-Cl95w2vb.js → orchestrator-CMbCTjBf.js} +7 -7
- package/dist/public/{orchestrator-D-xlXG92.js → orchestrator-DHjdLtqa.js} +663 -663
- package/dist/public/peerjs-DUCKH-m9.js +1 -0
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-CaoKmIIB.js → shadow-claw-EAt2ml5Y.js} +3 -3
- package/dist/public/{shadow-claw-accounts-CJvYC4li.js → shadow-claw-accounts-xUOLBsQZ.js} +1 -1
- package/dist/public/{shadow-claw-channels-3K62sU5f.js → shadow-claw-channels-XsfuWFF8.js} +4 -4
- package/dist/public/{shadow-claw-chat-Cu2keU2I.js → shadow-claw-chat-CbSaOZ4q.js} +7 -7
- package/dist/public/{shadow-claw-control-plane-D1OwlQI_.js → shadow-claw-control-plane-DHvN3Ukg.js} +2 -2
- package/dist/public/{shadow-claw-conversations-DDtg3BEZ.js → shadow-claw-conversations-BWs5VVfs.js} +2 -2
- package/dist/public/{shadow-claw-file-viewer-BR__1z9S.js → shadow-claw-file-viewer-BWsoW5QS.js} +5 -4
- package/dist/public/{shadow-claw-files-BJSyZ-Bl.js → shadow-claw-files-CG7MqWiV.js} +8 -8
- package/dist/public/{shadow-claw-git-MAjhXRLc.js → shadow-claw-git-DBRPZNav.js} +6 -6
- package/dist/public/{shadow-claw-integrations-82UoFuDR.js → shadow-claw-integrations-BBVnO0Fc.js} +8 -8
- package/dist/public/{shadow-claw-llm-R8j0lwPP.js → shadow-claw-llm-DMvLgKXo.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-BzZoz4LX.js → shadow-claw-mcp-remote-Bkx0gsmF.js} +2 -2
- package/dist/public/{shadow-claw-networking-DLEd9cNm.js → shadow-claw-networking-Dd8Rw31D.js} +2 -2
- package/dist/public/{shadow-claw-notifications-1vwR7TbT.js → shadow-claw-notifications-Dbevj-lq.js} +2 -2
- package/dist/public/{shadow-claw-page-header-DMKbkv3s.js → shadow-claw-page-header-BccSHItx.js} +1 -1
- package/dist/public/{shadow-claw-pages-CjyeIV__.js → shadow-claw-pages-3_lQAv0t.js} +4 -3
- package/dist/public/{shadow-claw-settings-BfbT4dBU.js → shadow-claw-settings-t8TIlgrM.js} +5 -5
- package/dist/public/{shadow-claw-storage-C-mWQQML.js → shadow-claw-storage-CfEXoSIE.js} +2 -2
- package/dist/public/{shadow-claw-task-server-D3lHp0Nc.js → shadow-claw-task-server-DxQSsgRZ.js} +2 -2
- package/dist/public/{shadow-claw-tasks-DVVWEm-c.js → shadow-claw-tasks-DNpicQwz.js} +14 -14
- package/dist/public/{shadow-claw-terminal-BwuTRp-O.js → shadow-claw-terminal-DpY6zU_r.js} +1 -1
- package/dist/public/{shadow-claw-toast-ClCwgzFG.js → shadow-claw-toast-BkrQIP-3.js} +1 -1
- package/dist/public/{shadow-claw-tools-zFLk2nbV.js → shadow-claw-tools-WSwWohnk.js} +2 -2
- package/dist/public/{shadow-claw-webvm-qR9X7fg7.js → shadow-claw-webvm-CP6oFjg5.js} +2 -2
- package/dist/public/syncWebMcpRegistration-DOn9gLnD.js +1 -0
- package/dist/public/toast-BXgUfbyh.js +1 -0
- package/dist/public/tools-DvckJZG2.js +1 -0
- package/dist/server.js +188 -188
- package/package.json +3 -1
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.stories.ts +1380 -93
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.ts +11 -6
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +8 -1
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +13 -5
- package/src/components/shadow-claw-pages/utils/buildHtmlPageSrcdoc.ts +13 -5
- package/src/core/control-plane-client.ts +15 -0
- package/src/core/utils/initControlPlane.test.ts +187 -0
- package/src/core/utils/initControlPlane.ts +194 -20
- package/src/security/custom-element-security.test.ts +51 -0
- package/src/security/custom-element-security.ts +33 -15
- package/src/server/control-plane-types.ts +1 -0
- package/src/server/control-plane.test.ts +69 -0
- package/src/server/control-plane.ts +45 -3
- package/src/server/mcp/tools/built-in-tools.ts +80 -5
- package/src/server/mcp/tools/client-tool-relay.ts +183 -26
- package/src/server/mcp/tools/mcp-tools.test.ts +140 -0
- package/src/server/middleware/static-files.test.ts +142 -0
- package/src/server/middleware/static-files.ts +66 -8
- package/src/testing/jest-setup.ts +1 -0
- package/src/worker/utils/handleMessage.ts +4 -1
- package/dist/lib/initControlPlane-CsO4UR6_.js.map +0 -1
- package/dist/lib/readGroupFileBytes-Bve-e5e_.js.map +0 -1
- package/dist/lib/shadow-claw-file-viewer-BfDHHgpA.js.map +0 -1
- package/dist/lib/shadow-claw-pages-D54jF1Qq.js.map +0 -1
- package/dist/lib/trusted-types-CkLDpLiZ.js.map +0 -1
- package/dist/public/custom-element-security-NCqQ5lmC.js +0 -1
- package/dist/public/downloadGroupFile-G0BZQgQg.js +0 -1
- package/dist/public/effect-rHYr4nfu.js +0 -1
- package/dist/public/file-viewer-8PdCURDD.js +0 -1
- package/dist/public/initControlPlane-CcGXax0A.js +0 -1
- package/dist/public/peerjs-BZdxWQFv.js +0 -1
- package/dist/public/syncWebMcpRegistration-DD42YsYV.js +0 -1
- package/dist/public/toast-HAq8XWFj.js +0 -58
|
@@ -18,6 +18,8 @@ export interface ClientToolRelayOptions {
|
|
|
18
18
|
export interface ClientToolRelay {
|
|
19
19
|
discoverTools: () => Promise<McpTool[]>;
|
|
20
20
|
attachToServer: (server: McpServer) => void;
|
|
21
|
+
getActiveClientId?: () => string;
|
|
22
|
+
setActiveClientId?: (id: string) => void;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function createClientToolRelay(
|
|
@@ -28,7 +30,10 @@ export function createClientToolRelay(
|
|
|
28
30
|
|
|
29
31
|
let cachedTools: McpTool[] = [];
|
|
30
32
|
let cacheTime = 0;
|
|
33
|
+
let activeClientId = targetClientId || "";
|
|
31
34
|
const registeredHandlers = new Set<string>();
|
|
35
|
+
const toolSupportingClientsMap = new Map<string, any[]>();
|
|
36
|
+
const toolDefMap = new Map<string, any>();
|
|
32
37
|
|
|
33
38
|
async function discoverTools(): Promise<McpTool[]> {
|
|
34
39
|
const now = Date.now();
|
|
@@ -36,36 +41,110 @@ export function createClientToolRelay(
|
|
|
36
41
|
return cachedTools;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
let clients: any[] = [];
|
|
45
|
+
try {
|
|
46
|
+
clients =
|
|
47
|
+
typeof controlPlane.getConnectedClients === "function"
|
|
48
|
+
? controlPlane.getConnectedClients()
|
|
49
|
+
: typeof controlPlane.listClients === "function"
|
|
50
|
+
? await controlPlane.listClients()
|
|
51
|
+
: [];
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
|
|
54
|
+
if (!Array.isArray(clients) || clients.length === 0) {
|
|
55
|
+
toolSupportingClientsMap.clear();
|
|
56
|
+
toolDefMap.clear();
|
|
41
57
|
return [];
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
toolSupportingClientsMap.clear();
|
|
61
|
+
toolDefMap.clear();
|
|
62
|
+
|
|
63
|
+
for (const c of clients) {
|
|
64
|
+
const cid = (c as any).clientId || (c as any).id;
|
|
65
|
+
if (!cid) continue;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const res = await controlPlane.sendCommand(cid, "list-tools", {});
|
|
69
|
+
if (
|
|
70
|
+
res &&
|
|
71
|
+
res.success &&
|
|
72
|
+
res.data?.tools &&
|
|
73
|
+
Array.isArray(res.data.tools)
|
|
74
|
+
) {
|
|
75
|
+
for (const t of res.data.tools) {
|
|
76
|
+
if (!toolDefMap.has(t.name)) {
|
|
77
|
+
toolDefMap.set(t.name, t);
|
|
78
|
+
toolSupportingClientsMap.set(t.name, []);
|
|
79
|
+
}
|
|
80
|
+
toolSupportingClientsMap.get(t.name)!.push(c);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`[ClientToolRelay] Failed to query tools from client ${cid}:`,
|
|
86
|
+
err,
|
|
87
|
+
);
|
|
61
88
|
}
|
|
62
|
-
} catch (err) {
|
|
63
|
-
console.warn(
|
|
64
|
-
`[ClientToolRelay] Failed to query tools from client ${clientId}:`,
|
|
65
|
-
err,
|
|
66
|
-
);
|
|
67
89
|
}
|
|
68
90
|
|
|
91
|
+
const targetId = resolveTargetClientId(
|
|
92
|
+
controlPlane,
|
|
93
|
+
activeClientId || targetClientId,
|
|
94
|
+
);
|
|
95
|
+
const toolMap = new Map<string, McpTool>();
|
|
96
|
+
|
|
97
|
+
for (const [toolName, t] of toolDefMap.entries()) {
|
|
98
|
+
const supportingClients = toolSupportingClientsMap.get(toolName) || [];
|
|
99
|
+
const supportingClientIds = supportingClients
|
|
100
|
+
.map((cl: any) => cl.clientId || cl.id)
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
|
|
103
|
+
const preferredClient =
|
|
104
|
+
supportingClients.find(
|
|
105
|
+
(cl: any) => (cl.clientId || cl.id) === targetId,
|
|
106
|
+
) || supportingClients[0];
|
|
107
|
+
const toolDefaultId =
|
|
108
|
+
preferredClient?.clientId || preferredClient?.id || "";
|
|
109
|
+
const toolDefaultLabel = preferredClient?.deviceLabel || "Client";
|
|
110
|
+
|
|
111
|
+
const existingSchema =
|
|
112
|
+
t.inputSchema && typeof t.inputSchema === "object"
|
|
113
|
+
? t.inputSchema
|
|
114
|
+
: { type: "object", properties: {} };
|
|
115
|
+
|
|
116
|
+
const properties = {
|
|
117
|
+
...(existingSchema.properties || {}),
|
|
118
|
+
clientId: {
|
|
119
|
+
type: "string",
|
|
120
|
+
description: `Target ShadowClaw client ID (optional; defaults to ${toolDefaultId} [${toolDefaultLabel}]). Available on: ${supportingClients.map((cl: any) => `${cl.clientId || cl.id} (${cl.deviceLabel || "Client"})`).join(", ")}`,
|
|
121
|
+
enum: supportingClientIds,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const clientNote =
|
|
126
|
+
clients.length > 1
|
|
127
|
+
? supportingClients.length > 1
|
|
128
|
+
? ` [Default: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
|
|
129
|
+
: ` [Client: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
|
|
130
|
+
: "";
|
|
131
|
+
|
|
132
|
+
toolMap.set(toolName, {
|
|
133
|
+
name: t.name,
|
|
134
|
+
description:
|
|
135
|
+
(t.description || `Relayed tool '${t.name}' from connected client.`) +
|
|
136
|
+
clientNote,
|
|
137
|
+
inputSchema: {
|
|
138
|
+
...existingSchema,
|
|
139
|
+
type: "object",
|
|
140
|
+
properties,
|
|
141
|
+
},
|
|
142
|
+
annotations: t.annotations,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
cachedTools = Array.from(toolMap.values());
|
|
147
|
+
cacheTime = now;
|
|
69
148
|
return cachedTools;
|
|
70
149
|
}
|
|
71
150
|
|
|
@@ -94,7 +173,49 @@ export function createClientToolRelay(
|
|
|
94
173
|
_meta?: Record<string, unknown>,
|
|
95
174
|
inputResponses?: Record<string, unknown>,
|
|
96
175
|
): Promise<McpToolCallResult> {
|
|
97
|
-
const
|
|
176
|
+
const connectedClients =
|
|
177
|
+
typeof controlPlane.getConnectedClients === "function"
|
|
178
|
+
? controlPlane.getConnectedClients()
|
|
179
|
+
: typeof controlPlane.listClients === "function"
|
|
180
|
+
? await controlPlane.listClients()
|
|
181
|
+
: [];
|
|
182
|
+
if (!Array.isArray(connectedClients) || connectedClients.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
resultType: "complete",
|
|
185
|
+
isError: true,
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: "Error: No connected ShadowClaw client available to execute tool.",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (toolDefMap.size === 0) {
|
|
196
|
+
await discoverTools();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const supportingClients = toolSupportingClientsMap.get(toolName) || [];
|
|
200
|
+
const supportingClientIds = supportingClients
|
|
201
|
+
.map((cl: any) => cl.clientId || cl.id)
|
|
202
|
+
.filter(Boolean);
|
|
203
|
+
|
|
204
|
+
let resolvedClientCandidate = args.clientId as string | undefined;
|
|
205
|
+
if (!resolvedClientCandidate) {
|
|
206
|
+
const activeCandidate = activeClientId || targetClientId;
|
|
207
|
+
const activeId = resolveTargetClientId(controlPlane, activeCandidate);
|
|
208
|
+
if (supportingClientIds.includes(activeId)) {
|
|
209
|
+
resolvedClientCandidate = activeId;
|
|
210
|
+
} else if (supportingClientIds.length > 0) {
|
|
211
|
+
resolvedClientCandidate = supportingClientIds[0];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const clientId = resolveTargetClientId(
|
|
216
|
+
controlPlane,
|
|
217
|
+
resolvedClientCandidate,
|
|
218
|
+
);
|
|
98
219
|
if (!clientId) {
|
|
99
220
|
return {
|
|
100
221
|
resultType: "complete",
|
|
@@ -108,6 +229,23 @@ export function createClientToolRelay(
|
|
|
108
229
|
};
|
|
109
230
|
}
|
|
110
231
|
|
|
232
|
+
if (
|
|
233
|
+
supportingClientIds.length > 0 &&
|
|
234
|
+
!supportingClientIds.includes(clientId)
|
|
235
|
+
) {
|
|
236
|
+
return {
|
|
237
|
+
resultType: "complete",
|
|
238
|
+
isError: true,
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: "text",
|
|
242
|
+
text: `Error: Tool '${toolName}' is not enabled or available on client '${clientId}'. (Available on: ${supportingClientIds.join(", ")})`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
_meta: { clientId },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
111
249
|
// Interactive tool handling: ask_user via MRTR
|
|
112
250
|
if (toolName === "ask_user") {
|
|
113
251
|
if (!inputResponses || !inputResponses["response"]) {
|
|
@@ -147,14 +285,19 @@ export function createClientToolRelay(
|
|
|
147
285
|
: JSON.stringify(userResponse),
|
|
148
286
|
},
|
|
149
287
|
],
|
|
288
|
+
_meta: {
|
|
289
|
+
clientId,
|
|
290
|
+
},
|
|
150
291
|
};
|
|
151
292
|
}
|
|
152
293
|
|
|
153
294
|
// Standard tool execution via Control Plane invoke-tool command
|
|
154
295
|
try {
|
|
296
|
+
const toolArgs = { ...args };
|
|
297
|
+
delete toolArgs.clientId;
|
|
155
298
|
const res = await controlPlane.sendCommand(clientId, "invoke-tool", {
|
|
156
299
|
toolName,
|
|
157
|
-
input:
|
|
300
|
+
input: toolArgs,
|
|
158
301
|
});
|
|
159
302
|
|
|
160
303
|
if (!res.success) {
|
|
@@ -167,6 +310,9 @@ export function createClientToolRelay(
|
|
|
167
310
|
text: `Tool execution error: ${res.error || "Unknown client execution error"}`,
|
|
168
311
|
},
|
|
169
312
|
],
|
|
313
|
+
_meta: {
|
|
314
|
+
clientId,
|
|
315
|
+
},
|
|
170
316
|
};
|
|
171
317
|
}
|
|
172
318
|
|
|
@@ -185,6 +331,9 @@ export function createClientToolRelay(
|
|
|
185
331
|
text: textOutput,
|
|
186
332
|
},
|
|
187
333
|
],
|
|
334
|
+
_meta: {
|
|
335
|
+
clientId,
|
|
336
|
+
},
|
|
188
337
|
};
|
|
189
338
|
} catch (err: any) {
|
|
190
339
|
return {
|
|
@@ -196,6 +345,9 @@ export function createClientToolRelay(
|
|
|
196
345
|
text: `Failed to dispatch tool '${toolName}' to client: ${err.message || String(err)}`,
|
|
197
346
|
},
|
|
198
347
|
],
|
|
348
|
+
_meta: {
|
|
349
|
+
clientId,
|
|
350
|
+
},
|
|
199
351
|
};
|
|
200
352
|
}
|
|
201
353
|
}
|
|
@@ -203,5 +355,10 @@ export function createClientToolRelay(
|
|
|
203
355
|
return {
|
|
204
356
|
discoverTools,
|
|
205
357
|
attachToServer,
|
|
358
|
+
getActiveClientId: () =>
|
|
359
|
+
activeClientId || resolveTargetClientId(controlPlane, targetClientId),
|
|
360
|
+
setActiveClientId: (id: string) => {
|
|
361
|
+
activeClientId = id;
|
|
362
|
+
},
|
|
206
363
|
};
|
|
207
364
|
}
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, jest } from "@jest/globals";
|
|
|
2
2
|
import {
|
|
3
3
|
registerBuiltInTools,
|
|
4
4
|
SHADOWCLAW_BUILTIN_TOOLS,
|
|
5
|
+
resolveTargetClientId,
|
|
5
6
|
} from "./built-in-tools.js";
|
|
6
7
|
import { createClientToolRelay } from "./client-tool-relay.js";
|
|
7
8
|
import { McpServer } from "../mcp-server.js";
|
|
@@ -585,4 +586,143 @@ describe("Client Tool Relay", () => {
|
|
|
585
586
|
expect(resNoClient.result.content[0].text).toContain("No connected");
|
|
586
587
|
}
|
|
587
588
|
});
|
|
589
|
+
|
|
590
|
+
describe("resolveTargetClientId and multi-client routing", () => {
|
|
591
|
+
it("resolves target client by exact ID, index, prefix, and label", () => {
|
|
592
|
+
const mockControlPlane: any = {
|
|
593
|
+
getConnectedClients: jest.fn().mockReturnValue([
|
|
594
|
+
{
|
|
595
|
+
clientId: "client-01m1kxw07jtfvh0c4gmbfty2wz",
|
|
596
|
+
deviceLabel: "Linux Desktop",
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
clientId: "client-01m1n03c284hrtybdvx33zv7r4",
|
|
600
|
+
deviceLabel: "Pixel Phone",
|
|
601
|
+
},
|
|
602
|
+
]),
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// Default (no requestedId) -> first client
|
|
606
|
+
expect(resolveTargetClientId(mockControlPlane)).toBe(
|
|
607
|
+
"client-01m1kxw07jtfvh0c4gmbfty2wz",
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
// Exact ID
|
|
611
|
+
expect(
|
|
612
|
+
resolveTargetClientId(
|
|
613
|
+
mockControlPlane,
|
|
614
|
+
"client-01m1n03c284hrtybdvx33zv7r4",
|
|
615
|
+
),
|
|
616
|
+
).toBe("client-01m1n03c284hrtybdvx33zv7r4");
|
|
617
|
+
|
|
618
|
+
// Index "1"
|
|
619
|
+
expect(resolveTargetClientId(mockControlPlane, "1")).toBe(
|
|
620
|
+
"client-01m1n03c284hrtybdvx33zv7r4",
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
// Prefix match
|
|
624
|
+
expect(resolveTargetClientId(mockControlPlane, "01m1n03")).toBe(
|
|
625
|
+
"client-01m1n03c284hrtybdvx33zv7r4",
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Device label match
|
|
629
|
+
expect(resolveTargetClientId(mockControlPlane, "Pixel")).toBe(
|
|
630
|
+
"client-01m1n03c284hrtybdvx33zv7r4",
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("executes shadowclaw_set_active_client and routes multi-client tools", async () => {
|
|
635
|
+
const server = new McpServer();
|
|
636
|
+
let activeId = "";
|
|
637
|
+
const mockControlPlane: any = {
|
|
638
|
+
getConnectedClients: jest.fn().mockReturnValue([
|
|
639
|
+
{ clientId: "c1", deviceLabel: "Client 1" },
|
|
640
|
+
{ clientId: "c2", deviceLabel: "Client 2" },
|
|
641
|
+
]),
|
|
642
|
+
setActiveClientId: jest.fn((id: string) => {
|
|
643
|
+
activeId = id;
|
|
644
|
+
}),
|
|
645
|
+
getActiveClientId: jest.fn(() => activeId),
|
|
646
|
+
sendCommand: jest.fn().mockImplementation(async (cid, action) => {
|
|
647
|
+
if (action === "list-tools") {
|
|
648
|
+
return {
|
|
649
|
+
success: true,
|
|
650
|
+
data: {
|
|
651
|
+
tools: [
|
|
652
|
+
{
|
|
653
|
+
name: "list_files",
|
|
654
|
+
description: "List files",
|
|
655
|
+
inputSchema: { type: "object", properties: {} },
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (action === "invoke-tool") {
|
|
662
|
+
return {
|
|
663
|
+
success: true,
|
|
664
|
+
data: { result: `Output from ${cid}` },
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
return { success: true };
|
|
668
|
+
}),
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
registerBuiltInTools(server, mockControlPlane);
|
|
672
|
+
const relay = createClientToolRelay(mockControlPlane);
|
|
673
|
+
relay.attachToServer(server);
|
|
674
|
+
|
|
675
|
+
// 1. Discover tools across multi-client: schema should have clientId enum
|
|
676
|
+
const tools = await server.getTools();
|
|
677
|
+
const listFilesTool = tools.find((t) => t.name === "list_files");
|
|
678
|
+
expect(listFilesTool).toBeDefined();
|
|
679
|
+
const props = listFilesTool?.inputSchema.properties as any;
|
|
680
|
+
expect(props?.clientId).toBeDefined();
|
|
681
|
+
expect(props?.clientId.enum).toEqual(["c1", "c2"]);
|
|
682
|
+
|
|
683
|
+
// 2. Run shadowclaw_set_active_client
|
|
684
|
+
const setRes = await server.handleRequest({
|
|
685
|
+
jsonrpc: "2.0",
|
|
686
|
+
id: 20,
|
|
687
|
+
method: "tools/call",
|
|
688
|
+
params: {
|
|
689
|
+
name: "shadowclaw_set_active_client",
|
|
690
|
+
arguments: { clientId: "c2" },
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
expect(mockControlPlane.setActiveClientId).toHaveBeenCalledWith("c2");
|
|
695
|
+
expect(setRes).not.toBeNull();
|
|
696
|
+
|
|
697
|
+
// 3. Invoking tool with explicit clientId c1 routes to c1
|
|
698
|
+
const callRes1 = await server.handleRequest({
|
|
699
|
+
jsonrpc: "2.0",
|
|
700
|
+
id: 21,
|
|
701
|
+
method: "tools/call",
|
|
702
|
+
params: {
|
|
703
|
+
name: "list_files",
|
|
704
|
+
arguments: { clientId: "c1" },
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
if (callRes1 && "result" in callRes1) {
|
|
709
|
+
expect(callRes1.result.content[0].text).toBe("Output from c1");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// 4. Invoking tool without clientId routes to active client c2
|
|
713
|
+
const callRes2 = await server.handleRequest({
|
|
714
|
+
jsonrpc: "2.0",
|
|
715
|
+
id: 22,
|
|
716
|
+
method: "tools/call",
|
|
717
|
+
params: {
|
|
718
|
+
name: "list_files",
|
|
719
|
+
arguments: {},
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
if (callRes2 && "result" in callRes2) {
|
|
724
|
+
expect(callRes2.result.content[0].text).toBe("Output from c2");
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
});
|
|
588
728
|
});
|
|
@@ -16,6 +16,11 @@ describe("static-files-middleware", () => {
|
|
|
16
16
|
fsMock = {
|
|
17
17
|
stat: jest.fn(),
|
|
18
18
|
access: jest.fn(),
|
|
19
|
+
existsSync: jest.fn(() => false),
|
|
20
|
+
statSync: jest.fn(() => ({
|
|
21
|
+
isFile: () => false,
|
|
22
|
+
isDirectory: () => false,
|
|
23
|
+
})),
|
|
19
24
|
constants: { F_OK: 0 },
|
|
20
25
|
};
|
|
21
26
|
rewriteMock = jest.fn(() => (_req: any, _res: any, next: any) => next());
|
|
@@ -361,4 +366,141 @@ describe("static-files-middleware", () => {
|
|
|
361
366
|
);
|
|
362
367
|
expect(next).not.toHaveBeenCalled();
|
|
363
368
|
});
|
|
369
|
+
|
|
370
|
+
it("serves .well-known files with dotfiles allow option", async () => {
|
|
371
|
+
await register();
|
|
372
|
+
const middleware = app.use.mock.calls[1][0];
|
|
373
|
+
|
|
374
|
+
const req = {
|
|
375
|
+
originalUrl: "/.well-known/agent-skills/index.json",
|
|
376
|
+
url: "/.well-known/agent-skills/index.json",
|
|
377
|
+
};
|
|
378
|
+
const res = { setHeader: jest.fn(), sendFile: jest.fn() };
|
|
379
|
+
const next = jest.fn();
|
|
380
|
+
|
|
381
|
+
fsMock.stat.mockImplementation((_path: string, cb: any) =>
|
|
382
|
+
cb(null, { isDirectory: () => false }),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
middleware(req, res, next);
|
|
386
|
+
|
|
387
|
+
expect(res.sendFile).toHaveBeenCalledWith(
|
|
388
|
+
expect.stringContaining("/root/.well-known/agent-skills/index.json"),
|
|
389
|
+
{ dotfiles: "allow" },
|
|
390
|
+
);
|
|
391
|
+
expect(next).not.toHaveBeenCalled();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("serves .agents files with dotfiles allow option", async () => {
|
|
395
|
+
await register();
|
|
396
|
+
const middleware = app.use.mock.calls[1][0];
|
|
397
|
+
|
|
398
|
+
const req = {
|
|
399
|
+
originalUrl: "/.agents/skills/main/toast/SKILL.md",
|
|
400
|
+
url: "/.agents/skills/main/toast/SKILL.md",
|
|
401
|
+
};
|
|
402
|
+
const res = { setHeader: jest.fn(), sendFile: jest.fn() };
|
|
403
|
+
const next = jest.fn();
|
|
404
|
+
|
|
405
|
+
fsMock.stat.mockImplementation((_path: string, cb: any) =>
|
|
406
|
+
cb(null, { isDirectory: () => false }),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
middleware(req, res, next);
|
|
410
|
+
|
|
411
|
+
expect(res.sendFile).toHaveBeenCalledWith(
|
|
412
|
+
expect.stringContaining("/root/.agents/skills/main/toast/SKILL.md"),
|
|
413
|
+
{ dotfiles: "allow" },
|
|
414
|
+
);
|
|
415
|
+
expect(next).not.toHaveBeenCalled();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("serves index.html in allowed dot directories with dotfiles allow option", async () => {
|
|
419
|
+
await register();
|
|
420
|
+
const middleware = app.use.mock.calls[1][0];
|
|
421
|
+
|
|
422
|
+
const req = {
|
|
423
|
+
originalUrl: "/.well-known/agent-skills/",
|
|
424
|
+
url: "/.well-known/agent-skills/",
|
|
425
|
+
};
|
|
426
|
+
const res = { setHeader: jest.fn(), sendFile: jest.fn() };
|
|
427
|
+
const next = jest.fn();
|
|
428
|
+
|
|
429
|
+
fsMock.stat.mockImplementation((_path: string, cb: any) =>
|
|
430
|
+
cb(null, { isDirectory: () => true }),
|
|
431
|
+
);
|
|
432
|
+
fsMock.access.mockImplementation((_path: string, _mode: any, cb: any) =>
|
|
433
|
+
cb(null),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
middleware(req, res, next);
|
|
437
|
+
|
|
438
|
+
expect(res.sendFile).toHaveBeenCalledWith(
|
|
439
|
+
expect.stringContaining("/root/.well-known/agent-skills/index.html"),
|
|
440
|
+
{ dotfiles: "allow" },
|
|
441
|
+
);
|
|
442
|
+
expect(next).not.toHaveBeenCalled();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("serves allowed dotfiles with subpath prefix and dotfiles allow option", async () => {
|
|
446
|
+
await register();
|
|
447
|
+
const middleware = app.use.mock.calls[1][0];
|
|
448
|
+
|
|
449
|
+
const req = {
|
|
450
|
+
originalUrl: "/shadow-claw/.well-known/agent-skills/index.json",
|
|
451
|
+
url: "/shadow-claw/.well-known/agent-skills/index.json",
|
|
452
|
+
};
|
|
453
|
+
const res = { setHeader: jest.fn(), sendFile: jest.fn() };
|
|
454
|
+
const next = jest.fn();
|
|
455
|
+
|
|
456
|
+
// The direct stat under /root/shadow-claw/... will fail (ENOENT), but stripped subpath stat succeeds
|
|
457
|
+
fsMock.stat.mockImplementation((targetPath: string, cb: any) => {
|
|
458
|
+
if (targetPath.includes("/root/shadow-claw/")) {
|
|
459
|
+
cb(new Error("ENOENT"));
|
|
460
|
+
} else {
|
|
461
|
+
cb(null, { isFile: () => true, isDirectory: () => false });
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
fsMock.existsSync.mockImplementation((p: string) =>
|
|
466
|
+
p.includes(".well-known"),
|
|
467
|
+
);
|
|
468
|
+
fsMock.statSync.mockImplementation(() => ({ isFile: () => true }));
|
|
469
|
+
|
|
470
|
+
middleware(req, res, next);
|
|
471
|
+
|
|
472
|
+
expect(res.sendFile).toHaveBeenCalledWith(
|
|
473
|
+
expect.stringContaining("/root/.well-known/agent-skills/index.json"),
|
|
474
|
+
{ dotfiles: "allow" },
|
|
475
|
+
);
|
|
476
|
+
expect(next).not.toHaveBeenCalled();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("blocks disallowed dotfiles from being served", async () => {
|
|
480
|
+
await register();
|
|
481
|
+
const middleware = app.use.mock.calls[1][0];
|
|
482
|
+
|
|
483
|
+
const disallowedPaths = [
|
|
484
|
+
"/.git/config",
|
|
485
|
+
"/.env",
|
|
486
|
+
"/.cache/db.sqlite",
|
|
487
|
+
"/.well-known/.env",
|
|
488
|
+
"/.agents/.git",
|
|
489
|
+
"/pages/.secret.txt",
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
for (const disallowedPath of disallowedPaths) {
|
|
493
|
+
const req = {
|
|
494
|
+
originalUrl: disallowedPath,
|
|
495
|
+
url: disallowedPath,
|
|
496
|
+
};
|
|
497
|
+
const res = { setHeader: jest.fn(), sendFile: jest.fn() };
|
|
498
|
+
const next = jest.fn();
|
|
499
|
+
|
|
500
|
+
middleware(req, res, next);
|
|
501
|
+
|
|
502
|
+
expect(res.sendFile).not.toHaveBeenCalled();
|
|
503
|
+
expect(next).toHaveBeenCalled();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
364
506
|
});
|