apex-code 0.0.1-alpha.5 → 0.0.1-alpha.7

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +10 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +17 -0
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +46 -3
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/package-manager.d.ts.map +1 -1
  10. package/dist/core/package-manager.js +15 -0
  11. package/dist/core/package-manager.js.map +1 -1
  12. package/dist/core/permissions/gate.d.ts +7 -0
  13. package/dist/core/permissions/gate.d.ts.map +1 -1
  14. package/dist/core/permissions/gate.js.map +1 -1
  15. package/dist/core/permissions/startup.d.ts +12 -0
  16. package/dist/core/permissions/startup.d.ts.map +1 -1
  17. package/dist/core/permissions/startup.js +12 -4
  18. package/dist/core/permissions/startup.js.map +1 -1
  19. package/dist/core/sandbox/cli-launch.d.ts +43 -0
  20. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  21. package/dist/core/sandbox/cli-launch.js +70 -3
  22. package/dist/core/sandbox/cli-launch.js.map +1 -1
  23. package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
  24. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  25. package/dist/core/sandbox/cli-supervisor.js +1 -0
  26. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  27. package/dist/core/skills.d.ts +19 -7
  28. package/dist/core/skills.d.ts.map +1 -1
  29. package/dist/core/skills.js +77 -19
  30. package/dist/core/skills.js.map +1 -1
  31. package/dist/core/slash-commands.d.ts.map +1 -1
  32. package/dist/core/slash-commands.js +1 -1
  33. package/dist/core/slash-commands.js.map +1 -1
  34. package/dist/core/system-prompt.d.ts.map +1 -1
  35. package/dist/core/system-prompt.js +3 -3
  36. package/dist/core/system-prompt.js.map +1 -1
  37. package/dist/core/tools/index.d.ts +5 -1
  38. package/dist/core/tools/index.d.ts.map +1 -1
  39. package/dist/core/tools/index.js +12 -0
  40. package/dist/core/tools/index.js.map +1 -1
  41. package/dist/core/tools/skill-search.d.ts +32 -0
  42. package/dist/core/tools/skill-search.d.ts.map +1 -0
  43. package/dist/core/tools/skill-search.js +56 -0
  44. package/dist/core/tools/skill-search.js.map +1 -0
  45. package/dist/main.d.ts.map +1 -1
  46. package/dist/main.js +1 -0
  47. package/dist/main.js.map +1 -1
  48. package/dist/modes/interactive/components/footer.d.ts +3 -0
  49. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  50. package/dist/modes/interactive/components/footer.js +13 -0
  51. package/dist/modes/interactive/components/footer.js.map +1 -1
  52. package/dist/modes/interactive/components/model-selector.d.ts +19 -6
  53. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  54. package/dist/modes/interactive/components/model-selector.js +172 -89
  55. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  56. package/dist/modes/interactive/components/settings-selector.d.ts +13 -0
  57. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  58. package/dist/modes/interactive/components/settings-selector.js +96 -0
  59. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  60. package/dist/modes/interactive/interactive-mode.d.ts +11 -0
  61. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  62. package/dist/modes/interactive/interactive-mode.js +64 -4
  63. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  64. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  65. package/dist/modes/rpc/rpc-mode.js +2 -1
  66. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  67. package/docs/skills.md +54 -4
  68. package/npm-shrinkwrap.json +5 -5
  69. package/package.json +2 -2
@@ -19,6 +19,13 @@ export interface PermissionGateOptions {
19
19
  getContract: (toolName: string) => ToolContract | undefined;
20
20
  store: PermissionRuleStore;
21
21
  getMode: () => PermissionMode | Promise<PermissionMode>;
22
+ /**
23
+ * The `--permission-mode` value, when one was passed. Not used to decide
24
+ * anything here — `getMode` already folds it in. It is carried so a UI that
25
+ * offers to change the mode can tell the user their write is outranked
26
+ * (permissions/startup.ts, resolveEffectiveModeWithOrigin).
27
+ */
28
+ flagMode?: PermissionMode;
22
29
  /** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */
23
30
  responder?: PermissionResponder;
24
31
  /** Resolves a responder at call time because the interactive UI binds after session construction. */
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtE,MAAM,WAAW,qBAAqB;IACrC,2IAA2I;IAC3I,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,GAAG,SAAS,CAAC;IAC5D,KAAK,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD,yFAAyF;IACzF,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,qGAAqG;IACrG,YAAY,CAAC,EAAE,MAAM,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,oHAAkH;AAClH,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAKrG","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtE,MAAM,WAAW,qBAAqB;IACrC,2IAA2I;IAC3I,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,GAAG,SAAS,CAAC;IAC5D,KAAK,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,yFAAyF;IACzF,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,qGAAqG;IACrG,YAAY,CAAC,EAAE,MAAM,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,oHAAkH;AAClH,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAKrG","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/**\n\t * The `--permission-mode` value, when one was passed. Not used to decide\n\t * anything here — `getMode` already folds it in. It is carried so a UI that\n\t * offers to change the mode can tell the user their write is outranked\n\t * (permissions/startup.ts, resolveEffectiveModeWithOrigin).\n\t */\n\tflagMode?: PermissionMode;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/**\n\t * The `--permission-mode` value, when one was passed. Not used to decide\n\t * anything here — `getMode` already folds it in. It is carried so a UI that\n\t * offers to change the mode can tell the user their write is outranked\n\t * (permissions/startup.ts, resolveEffectiveModeWithOrigin).\n\t */\n\tflagMode?: PermissionMode;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
@@ -24,10 +24,22 @@ export declare function resolvePermissionModeForStartup(options: {
24
24
  interactive: boolean;
25
25
  requestedMode: PermissionMode | undefined;
26
26
  }): PermissionModeStartupResult;
27
+ /** Where an effective mode came from. `"default"` means nothing set one. */
28
+ export type PermissionModeOrigin = "flag" | WritablePermissionSource | "default";
29
+ export interface EffectiveModeResolution {
30
+ mode: PermissionMode;
31
+ origin: PermissionModeOrigin;
32
+ }
27
33
  /**
28
34
  * The `--permission-mode` flag outranks every persisted mode, including a project
29
35
  * rule a team committed — a deliberate per-run operator decision should not be
30
36
  * silently overridden by checked-in config (ADR 0004).
37
+ *
38
+ * Returns the origin alongside the mode so a caller that offers to *change* the
39
+ * mode can tell whether its write would actually take effect. A settings toggle
40
+ * writing `user` while `local` or the flag outranks it is a silent no-op, which is
41
+ * the one failure a UI here must not have.
31
42
  */
43
+ export declare function resolveEffectiveModeWithOrigin(flagMode: PermissionMode | undefined, modesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>): EffectiveModeResolution;
32
44
  export declare function resolveEffectiveMode(flagMode: PermissionMode | undefined, modesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>): PermissionMode;
33
45
  //# sourceMappingURL=startup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/startup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,KAAK,cAAc,EAAE,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAElG,MAAM,MAAM,2BAA2B,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9G;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACxD,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,cAAc,GAAG,SAAS,CAAC;CAC1C,GAAG,2BAA2B,CAW9B;AAYD;;;;GAIG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,aAAa,EAAE,WAAW,CAAC,wBAAwB,EAAE,cAAc,CAAC,GAClE,cAAc,CAUhB","sourcesContent":["/**\n * Startup-time permission mode resolution (ADR 0004). A misconfigured\n * non-interactive run should fail fast, at startup, with an actionable message —\n * not deny every tool call mid-run in a way that looks like a bug (see the ADR's\n * \"ask fails closed, and a session that cannot ask refuses to start\").\n */\n\nimport { PERMISSION_MODES, type PermissionMode, type WritablePermissionSource } from \"./store.ts\";\n\nexport type PermissionModeStartupResult = { ok: true; mode: PermissionMode } | { ok: false; message: string };\n\n/**\n * `requestedMode` is assumed already validated against `PERMISSION_MODES` (args.ts\n * rejects an unknown value at parse time) — this only decides whether a session may\n * start *without* one. Interactive sessions may: an `ask` there has a human to\n * resolve it. Non-interactive sessions may not: with no responder, `ask` fails\n * closed to `deny`, and discovering that per-call, mid-run, is the exact bad\n * failure mode this check exists to avoid.\n */\nexport function resolvePermissionModeForStartup(options: {\n\tinteractive: boolean;\n\trequestedMode: PermissionMode | undefined;\n}): PermissionModeStartupResult {\n\tif (options.requestedMode !== undefined) {\n\t\treturn { ok: true, mode: options.requestedMode };\n\t}\n\tif (!options.interactive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Non-interactive sessions require an explicit --permission-mode. Valid modes: ${PERMISSION_MODES.join(\", \")}.`,\n\t\t};\n\t}\n\treturn { ok: true, mode: \"default\" };\n}\n\n/** Sources a mode can come from, in precedence order — mirrors PERMISSION_SOURCES (rules.ts), restricted to the sources that can plausibly set one. */\nconst MODE_SOURCE_ORDER: readonly [\"flag\", ...WritablePermissionSource[]] = [\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"command\",\n\t\"session\",\n];\n\n/**\n * The `--permission-mode` flag outranks every persisted mode, including a project\n * rule a team committed — a deliberate per-run operator decision should not be\n * silently overridden by checked-in config (ADR 0004).\n */\nexport function resolveEffectiveMode(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): PermissionMode {\n\tfor (const source of MODE_SOURCE_ORDER) {\n\t\tif (source === \"flag\") {\n\t\t\tif (flagMode !== undefined) return flagMode;\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode = modesBySource.get(source);\n\t\tif (mode !== undefined) return mode;\n\t}\n\treturn \"default\";\n}\n"]}
1
+ {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/startup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,KAAK,cAAc,EAAE,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAElG,MAAM,MAAM,2BAA2B,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9G;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACxD,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,cAAc,GAAG,SAAS,CAAC;CAC1C,GAAG,2BAA2B,CAW9B;AAYD,4EAA4E;AAC5E,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,wBAAwB,GAAG,SAAS,CAAC;AAEjF,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,oBAAoB,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC7C,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,aAAa,EAAE,WAAW,CAAC,wBAAwB,EAAE,cAAc,CAAC,GAClE,uBAAuB,CAUzB;AAED,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,aAAa,EAAE,WAAW,CAAC,wBAAwB,EAAE,cAAc,CAAC,GAClE,cAAc,CAEhB","sourcesContent":["/**\n * Startup-time permission mode resolution (ADR 0004). A misconfigured\n * non-interactive run should fail fast, at startup, with an actionable message —\n * not deny every tool call mid-run in a way that looks like a bug (see the ADR's\n * \"ask fails closed, and a session that cannot ask refuses to start\").\n */\n\nimport { PERMISSION_MODES, type PermissionMode, type WritablePermissionSource } from \"./store.ts\";\n\nexport type PermissionModeStartupResult = { ok: true; mode: PermissionMode } | { ok: false; message: string };\n\n/**\n * `requestedMode` is assumed already validated against `PERMISSION_MODES` (args.ts\n * rejects an unknown value at parse time) — this only decides whether a session may\n * start *without* one. Interactive sessions may: an `ask` there has a human to\n * resolve it. Non-interactive sessions may not: with no responder, `ask` fails\n * closed to `deny`, and discovering that per-call, mid-run, is the exact bad\n * failure mode this check exists to avoid.\n */\nexport function resolvePermissionModeForStartup(options: {\n\tinteractive: boolean;\n\trequestedMode: PermissionMode | undefined;\n}): PermissionModeStartupResult {\n\tif (options.requestedMode !== undefined) {\n\t\treturn { ok: true, mode: options.requestedMode };\n\t}\n\tif (!options.interactive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Non-interactive sessions require an explicit --permission-mode. Valid modes: ${PERMISSION_MODES.join(\", \")}.`,\n\t\t};\n\t}\n\treturn { ok: true, mode: \"default\" };\n}\n\n/** Sources a mode can come from, in precedence order — mirrors PERMISSION_SOURCES (rules.ts), restricted to the sources that can plausibly set one. */\nconst MODE_SOURCE_ORDER: readonly [\"flag\", ...WritablePermissionSource[]] = [\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"command\",\n\t\"session\",\n];\n\n/** Where an effective mode came from. `\"default\"` means nothing set one. */\nexport type PermissionModeOrigin = \"flag\" | WritablePermissionSource | \"default\";\n\nexport interface EffectiveModeResolution {\n\tmode: PermissionMode;\n\torigin: PermissionModeOrigin;\n}\n\n/**\n * The `--permission-mode` flag outranks every persisted mode, including a project\n * rule a team committed — a deliberate per-run operator decision should not be\n * silently overridden by checked-in config (ADR 0004).\n *\n * Returns the origin alongside the mode so a caller that offers to *change* the\n * mode can tell whether its write would actually take effect. A settings toggle\n * writing `user` while `local` or the flag outranks it is a silent no-op, which is\n * the one failure a UI here must not have.\n */\nexport function resolveEffectiveModeWithOrigin(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): EffectiveModeResolution {\n\tfor (const source of MODE_SOURCE_ORDER) {\n\t\tif (source === \"flag\") {\n\t\t\tif (flagMode !== undefined) return { mode: flagMode, origin: \"flag\" };\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode = modesBySource.get(source);\n\t\tif (mode !== undefined) return { mode, origin: source };\n\t}\n\treturn { mode: \"default\", origin: \"default\" };\n}\n\nexport function resolveEffectiveMode(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): PermissionMode {\n\treturn resolveEffectiveModeWithOrigin(flagMode, modesBySource).mode;\n}\n"]}
@@ -38,18 +38,26 @@ const MODE_SOURCE_ORDER = [
38
38
  * The `--permission-mode` flag outranks every persisted mode, including a project
39
39
  * rule a team committed — a deliberate per-run operator decision should not be
40
40
  * silently overridden by checked-in config (ADR 0004).
41
+ *
42
+ * Returns the origin alongside the mode so a caller that offers to *change* the
43
+ * mode can tell whether its write would actually take effect. A settings toggle
44
+ * writing `user` while `local` or the flag outranks it is a silent no-op, which is
45
+ * the one failure a UI here must not have.
41
46
  */
42
- export function resolveEffectiveMode(flagMode, modesBySource) {
47
+ export function resolveEffectiveModeWithOrigin(flagMode, modesBySource) {
43
48
  for (const source of MODE_SOURCE_ORDER) {
44
49
  if (source === "flag") {
45
50
  if (flagMode !== undefined)
46
- return flagMode;
51
+ return { mode: flagMode, origin: "flag" };
47
52
  continue;
48
53
  }
49
54
  const mode = modesBySource.get(source);
50
55
  if (mode !== undefined)
51
- return mode;
56
+ return { mode, origin: source };
52
57
  }
53
- return "default";
58
+ return { mode: "default", origin: "default" };
59
+ }
60
+ export function resolveEffectiveMode(flagMode, modesBySource) {
61
+ return resolveEffectiveModeWithOrigin(flagMode, modesBySource).mode;
54
62
  }
55
63
  //# sourceMappingURL=startup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"startup.js","sourceRoot":"","sources":["../../../src/core/permissions/startup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAsD,MAAM,YAAY,CAAC;AAIlG;;;;;;;GAOG;AACH,MAAM,UAAU,+BAA+B,CAAC,OAG/C,EAA+B;IAC/B,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAC1B,OAAO;YACN,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,gFAAgF,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACvH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAAA,CACrC;AAED,yJAAuJ;AACvJ,MAAM,iBAAiB,GAAqD;IAC3E,MAAM;IACN,OAAO;IACP,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;CACT,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CACnC,QAAoC,EACpC,aAAoE,EACnD;IACjB,KAAK,MAAM,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACxC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,QAAQ,CAAC;YAC5C,SAAS;QACV,CAAC;QACD,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;IACrC,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["/**\n * Startup-time permission mode resolution (ADR 0004). A misconfigured\n * non-interactive run should fail fast, at startup, with an actionable message —\n * not deny every tool call mid-run in a way that looks like a bug (see the ADR's\n * \"ask fails closed, and a session that cannot ask refuses to start\").\n */\n\nimport { PERMISSION_MODES, type PermissionMode, type WritablePermissionSource } from \"./store.ts\";\n\nexport type PermissionModeStartupResult = { ok: true; mode: PermissionMode } | { ok: false; message: string };\n\n/**\n * `requestedMode` is assumed already validated against `PERMISSION_MODES` (args.ts\n * rejects an unknown value at parse time) — this only decides whether a session may\n * start *without* one. Interactive sessions may: an `ask` there has a human to\n * resolve it. Non-interactive sessions may not: with no responder, `ask` fails\n * closed to `deny`, and discovering that per-call, mid-run, is the exact bad\n * failure mode this check exists to avoid.\n */\nexport function resolvePermissionModeForStartup(options: {\n\tinteractive: boolean;\n\trequestedMode: PermissionMode | undefined;\n}): PermissionModeStartupResult {\n\tif (options.requestedMode !== undefined) {\n\t\treturn { ok: true, mode: options.requestedMode };\n\t}\n\tif (!options.interactive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Non-interactive sessions require an explicit --permission-mode. Valid modes: ${PERMISSION_MODES.join(\", \")}.`,\n\t\t};\n\t}\n\treturn { ok: true, mode: \"default\" };\n}\n\n/** Sources a mode can come from, in precedence order — mirrors PERMISSION_SOURCES (rules.ts), restricted to the sources that can plausibly set one. */\nconst MODE_SOURCE_ORDER: readonly [\"flag\", ...WritablePermissionSource[]] = [\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"command\",\n\t\"session\",\n];\n\n/**\n * The `--permission-mode` flag outranks every persisted mode, including a project\n * rule a team committed — a deliberate per-run operator decision should not be\n * silently overridden by checked-in config (ADR 0004).\n */\nexport function resolveEffectiveMode(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): PermissionMode {\n\tfor (const source of MODE_SOURCE_ORDER) {\n\t\tif (source === \"flag\") {\n\t\t\tif (flagMode !== undefined) return flagMode;\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode = modesBySource.get(source);\n\t\tif (mode !== undefined) return mode;\n\t}\n\treturn \"default\";\n}\n"]}
1
+ {"version":3,"file":"startup.js","sourceRoot":"","sources":["../../../src/core/permissions/startup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAsD,MAAM,YAAY,CAAC;AAIlG;;;;;;;GAOG;AACH,MAAM,UAAU,+BAA+B,CAAC,OAG/C,EAA+B;IAC/B,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAC1B,OAAO;YACN,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,gFAAgF,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACvH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAAA,CACrC;AAED,yJAAuJ;AACvJ,MAAM,iBAAiB,GAAqD;IAC3E,MAAM;IACN,OAAO;IACP,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;CACT,CAAC;AAUF;;;;;;;;;GASG;AACH,MAAM,UAAU,8BAA8B,CAC7C,QAAoC,EACpC,aAAoE,EAC1C;IAC1B,KAAK,MAAM,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACxC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;YACtE,SAAS;QACV,CAAC;QACD,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACzD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAAA,CAC9C;AAED,MAAM,UAAU,oBAAoB,CACnC,QAAoC,EACpC,aAAoE,EACnD;IACjB,OAAO,8BAA8B,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,IAAI,CAAC;AAAA,CACpE","sourcesContent":["/**\n * Startup-time permission mode resolution (ADR 0004). A misconfigured\n * non-interactive run should fail fast, at startup, with an actionable message —\n * not deny every tool call mid-run in a way that looks like a bug (see the ADR's\n * \"ask fails closed, and a session that cannot ask refuses to start\").\n */\n\nimport { PERMISSION_MODES, type PermissionMode, type WritablePermissionSource } from \"./store.ts\";\n\nexport type PermissionModeStartupResult = { ok: true; mode: PermissionMode } | { ok: false; message: string };\n\n/**\n * `requestedMode` is assumed already validated against `PERMISSION_MODES` (args.ts\n * rejects an unknown value at parse time) — this only decides whether a session may\n * start *without* one. Interactive sessions may: an `ask` there has a human to\n * resolve it. Non-interactive sessions may not: with no responder, `ask` fails\n * closed to `deny`, and discovering that per-call, mid-run, is the exact bad\n * failure mode this check exists to avoid.\n */\nexport function resolvePermissionModeForStartup(options: {\n\tinteractive: boolean;\n\trequestedMode: PermissionMode | undefined;\n}): PermissionModeStartupResult {\n\tif (options.requestedMode !== undefined) {\n\t\treturn { ok: true, mode: options.requestedMode };\n\t}\n\tif (!options.interactive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Non-interactive sessions require an explicit --permission-mode. Valid modes: ${PERMISSION_MODES.join(\", \")}.`,\n\t\t};\n\t}\n\treturn { ok: true, mode: \"default\" };\n}\n\n/** Sources a mode can come from, in precedence order — mirrors PERMISSION_SOURCES (rules.ts), restricted to the sources that can plausibly set one. */\nconst MODE_SOURCE_ORDER: readonly [\"flag\", ...WritablePermissionSource[]] = [\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"command\",\n\t\"session\",\n];\n\n/** Where an effective mode came from. `\"default\"` means nothing set one. */\nexport type PermissionModeOrigin = \"flag\" | WritablePermissionSource | \"default\";\n\nexport interface EffectiveModeResolution {\n\tmode: PermissionMode;\n\torigin: PermissionModeOrigin;\n}\n\n/**\n * The `--permission-mode` flag outranks every persisted mode, including a project\n * rule a team committed — a deliberate per-run operator decision should not be\n * silently overridden by checked-in config (ADR 0004).\n *\n * Returns the origin alongside the mode so a caller that offers to *change* the\n * mode can tell whether its write would actually take effect. A settings toggle\n * writing `user` while `local` or the flag outranks it is a silent no-op, which is\n * the one failure a UI here must not have.\n */\nexport function resolveEffectiveModeWithOrigin(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): EffectiveModeResolution {\n\tfor (const source of MODE_SOURCE_ORDER) {\n\t\tif (source === \"flag\") {\n\t\t\tif (flagMode !== undefined) return { mode: flagMode, origin: \"flag\" };\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode = modesBySource.get(source);\n\t\tif (mode !== undefined) return { mode, origin: source };\n\t}\n\treturn { mode: \"default\", origin: \"default\" };\n}\n\nexport function resolveEffectiveMode(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): PermissionMode {\n\treturn resolveEffectiveModeWithOrigin(flagMode, modesBySource).mode;\n}\n"]}
@@ -18,6 +18,41 @@ export interface SandboxedCliLaunch extends SandboxLaunch {
18
18
  * `allowDefaultHosts: false` restores the strict behaviour for anyone who wants it.
19
19
  */
20
20
  export declare function resolveSupervisorAllowedHosts(cwd: string, agentDir: string): readonly string[] | undefined;
21
+ /**
22
+ * The host's two user-scope skill roots, each present only when it exists on the
23
+ * host. Kept as two named slots rather than one list because
24
+ * `core/package-manager.ts` discovers them in different modes -- root `.md` files
25
+ * count as skills under `agentSkills` ("pi" mode) and are ignored under
26
+ * `agentsHomeSkills` ("agents" mode), per `docs/skills.md` -- and a flat list of 0-2
27
+ * paths cannot tell the child which root a lone survivor was.
28
+ */
29
+ export interface HostSkillPaths {
30
+ /** Host `<agentDir>/skills`. */
31
+ readonly agentSkills?: string;
32
+ /** Host `<home>/.agents/skills`. */
33
+ readonly agentsHomeSkills?: string;
34
+ }
35
+ /** A candidate skill root that exists but was excluded, and why. */
36
+ export interface HostSkillPathRefusal {
37
+ readonly root: keyof HostSkillPaths;
38
+ readonly path: string;
39
+ readonly reason: string;
40
+ }
41
+ export interface ResolvedHostSkillPaths {
42
+ readonly paths: HostSkillPaths;
43
+ readonly refusals: readonly HostSkillPathRefusal[];
44
+ }
45
+ /**
46
+ * Resolve the host's user-scope skill directories, before the sandbox exists to hide
47
+ * them. Read only from the runtime environment and the host agent directory --
48
+ * never from project files -- matching ADR 0016's rule that supervisor policy is
49
+ * trust-first. Mirrors `core/package-manager.ts`'s own user-scope roots so the two
50
+ * sides agree on where a skill lives. A candidate that resolves (directly or via a
51
+ * symlink) onto the host home or an ancestor of it is refused rather than mounted --
52
+ * see `isHomeOrAncestorOfHome` -- and reported so the caller can surface a startup
53
+ * diagnostic instead of silently mounting or silently skipping it.
54
+ */
55
+ export declare function resolveHostSkillPaths(agentDir: string, homeDir: string): ResolvedHostSkillPaths;
21
56
  /**
22
57
  * Allocate agent-owned state under the sole writable workspace mount. The child does
23
58
  * not inherit a host home or a host session/config directory, preventing the sandbox
@@ -32,5 +67,13 @@ export declare function buildSandboxedCliLaunch(options: {
32
67
  readOnlyPaths?: readonly string[];
33
68
  authPath?: string;
34
69
  toolBinaries?: readonly HostToolBinary[];
70
+ /**
71
+ * Host user-scope skill directories, pre-filtered by the caller to those that
72
+ * exist and pass the host-home escape check (SKILL.4). Mounted read-only at their
73
+ * original host location -- Seatbelt cannot remap a path, so this must hold for
74
+ * both backends -- and named to the child via `APEX_CODE_SKILL_PATH_*` so its
75
+ * discovery can find them under the sandbox's own repointed `HOME`/agent dir.
76
+ */
77
+ skillPaths?: HostSkillPaths;
35
78
  }): SandboxedCliLaunch;
36
79
  //# sourceMappingURL=cli-launch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-launch.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/cli-launch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKrD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAKvE;AAED,MAAM,WAAW,kBAAmB,SAAQ,aAAa;IACxD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAK1G;AAmGD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACzC,GAAG,kBAAkB,CA+CrB","sourcesContent":["import { mkdirSync, readdirSync, rmSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { SettingsManager } from \"../settings-manager.ts\";\nimport { resolveDefaultAllowedHosts } from \"./default-hosts.ts\";\nimport type { SandboxLaunch } from \"./supervisor.ts\";\n\nconst NON_SESSION_COMMANDS = new Set([\"auth\", \"config\", \"install\", \"remove\", \"uninstall\", \"update\", \"list\"]);\nconst METADATA_FLAGS = new Set([\"--version\", \"-v\", \"--export\", \"--list-models\"]);\n\n/**\n * OS containment is the normal startup path for every command that can construct an\n * agent session. Commands that only inspect or maintain host configuration do not\n * create a runtime and therefore remain outside this child boundary.\n */\nexport function requiresSandboxedChild(args: readonly string[]): boolean {\n\tif (NON_SESSION_COMMANDS.has(args[0] ?? \"\")) return false;\n\tif (args.some((argument) => METADATA_FLAGS.has(argument))) return false;\n\tif (args.includes(\"--help\") || args.includes(\"-h\")) return false;\n\treturn true;\n}\n\nexport interface SandboxedCliLaunch extends SandboxLaunch {\n\treadonly environment: NodeJS.ProcessEnv;\n}\n\n/**\n * Read supervisor policy only from global settings; project settings are untrusted here.\n *\n * The built-in provider hosts are added unless explicitly refused, because a deny-all\n * default made a fresh install unable to reach any model while giving the user no way to\n * learn which host to permit. Configured hosts are additive on top, and\n * `allowDefaultHosts: false` restores the strict behaviour for anyone who wants it.\n */\nexport function resolveSupervisorAllowedHosts(cwd: string, agentDir: string): readonly string[] | undefined {\n\tconst network = SettingsManager.create(cwd, agentDir, { projectTrusted: false }).getNetworkSettings();\n\tconst configured = network?.allowedHosts ?? [];\n\tif (network?.allowDefaultHosts === false) return configured;\n\treturn [...new Set([...resolveDefaultAllowedHosts(), ...configured])];\n}\n\nconst SAFE_CHILD_ENVIRONMENT_KEYS = new Set([\n\t\"PATH\",\n\t\"LANG\",\n\t\"LC_ALL\",\n\t\"LC_CTYPE\",\n\t\"TERM\",\n\t\"COLORTERM\",\n\t\"NO_COLOR\",\n\t\"FORCE_COLOR\",\n\t\"TSX_TSCONFIG_PATH\",\n\t\"APEX_CODE_OFFLINE\",\n\t\"APEX_CODE_SKIP_VERSION_CHECK\",\n\t\"APEX_CODE_EXPERIMENTAL\",\n\t\"APEX_CODE_STARTUP_BENCHMARK\",\n\t\"APEX_CODE_TIMING\",\n\t\"APEX_CODE_CLEAR_ON_SHRINK\",\n\t\"APEX_CODE_HARDWARE_CURSOR\",\n\t\"APEX_CODE_MODEL_CATALOG_URL\",\n\t\"APEX_CODE_SHARE_VIEWER_URL\",\n\t\"VISUAL\",\n\t\"EDITOR\",\n]);\n\n// Provider API keys are explicit credential inputs, unlike arbitrary ambient variables.\n// Keep this list in sync with the documented provider environment-variable reference.\nconst SAFE_PROVIDER_CREDENTIAL_KEYS = new Set([\n\t\"ANTHROPIC_API_KEY\",\n\t\"ANTHROPIC_AUTH_TOKEN\",\n\t\"ANTHROPIC_OAUTH_TOKEN\",\n\t\"ANT_LING_API_KEY\",\n\t\"OPENAI_API_KEY\",\n\t\"AZURE_OPENAI_API_KEY\",\n\t\"AZURE_OPENAI_BASE_URL\",\n\t\"AZURE_OPENAI_RESOURCE_NAME\",\n\t\"AZURE_OPENAI_API_VERSION\",\n\t\"AZURE_OPENAI_DEPLOYMENT_NAME_MAP\",\n\t\"DEEPSEEK_API_KEY\",\n\t\"NVIDIA_API_KEY\",\n\t\"GEMINI_API_KEY\",\n\t\"GROQ_API_KEY\",\n\t\"CEREBRAS_API_KEY\",\n\t\"XAI_API_KEY\",\n\t\"FIREWORKS_API_KEY\",\n\t\"TOGETHER_API_KEY\",\n\t\"BASETEN_API_KEY\",\n\t\"OPENROUTER_API_KEY\",\n\t\"AI_GATEWAY_API_KEY\",\n\t\"ZAI_API_KEY\",\n\t\"ZAI_CODING_CN_API_KEY\",\n\t\"MISTRAL_API_KEY\",\n\t\"MINIMAX_API_KEY\",\n\t\"MOONSHOT_API_KEY\",\n\t\"OPENCODE_API_KEY\",\n\t\"KIMI_API_KEY\",\n\t\"CLOUDFLARE_API_KEY\",\n\t\"CLOUDFLARE_ACCOUNT_ID\",\n\t\"CLOUDFLARE_GATEWAY_ID\",\n\t\"QWEN_TOKEN_PLAN_API_KEY\",\n\t\"QWEN_TOKEN_PLAN_CN_API_KEY\",\n\t\"XIAOMI_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_CN_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_AMS_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_SGP_API_KEY\",\n\t\"AWS_PROFILE\",\n\t\"AWS_ACCESS_KEY_ID\",\n\t\"AWS_SECRET_ACCESS_KEY\",\n\t\"AWS_BEARER_TOKEN_BEDROCK\",\n\t\"AWS_REGION\",\n]);\n\nfunction buildChildEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n\treturn Object.fromEntries(\n\t\tObject.entries(environment).filter(\n\t\t\t([key]) => SAFE_CHILD_ENVIRONMENT_KEYS.has(key) || SAFE_PROVIDER_CREDENTIAL_KEYS.has(key),\n\t\t),\n\t);\n}\n\n/**\n * Drop empty files left in the child's tools directory by a previous launch.\n *\n * A projected tool is bind-mounted over a file there, and bwrap materialises that\n * mountpoint as an empty file on the host which outlives the namespace. If the host\n * tool later disappears, nothing is projected over the stub and the child would\n * otherwise find a 0-byte file where its binary should be. A real downloaded binary\n * is never empty, so size is a safe discriminator.\n */\nfunction clearStaleToolMountpoints(toolsDirectory: string): void {\n\tfor (const entry of readdirSync(toolsDirectory, { withFileTypes: true })) {\n\t\tif (!entry.isFile()) continue;\n\t\tconst entryPath = join(toolsDirectory, entry.name);\n\t\tif (statSync(entryPath).size === 0) {\n\t\t\trmSync(entryPath, { force: true });\n\t\t}\n\t}\n}\n\n/**\n * Allocate agent-owned state under the sole writable workspace mount. The child does\n * not inherit a host home or a host session/config directory, preventing the sandbox\n * from presenting a write boundary while its own state quietly escapes it.\n */\nexport function buildSandboxedCliLaunch(options: {\n\tworkspace: string;\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n}): SandboxedCliLaunch {\n\tconst stateDirectory = join(options.workspace, \".apex-code\", \"sandbox-state\");\n\tconst agentDirectory = join(options.workspace, \".apex-code\", \"sandbox-agent\");\n\tconst sessionDirectory = join(options.workspace, \".apex-code\", \"sandbox-sessions\");\n\t// Mirrors getBinDir() as the child will compute it from APEX_CODE_CODING_AGENT_DIR,\n\t// so a projected tool lands exactly where the child's own lookup already checks.\n\tconst toolsDirectory = join(agentDirectory, \"bin\");\n\tconst xdgDirectories = {\n\t\tXDG_CONFIG_HOME: join(stateDirectory, \"config\"),\n\t\tXDG_CACHE_HOME: join(stateDirectory, \"cache\"),\n\t\tXDG_DATA_HOME: join(stateDirectory, \"data\"),\n\t\tXDG_STATE_HOME: join(stateDirectory, \"state\"),\n\t};\n\tfor (const directory of [\n\t\tstateDirectory,\n\t\tagentDirectory,\n\t\tsessionDirectory,\n\t\ttoolsDirectory,\n\t\t...Object.values(xdgDirectories),\n\t]) {\n\t\tmkdirSync(directory, { recursive: true });\n\t}\n\tclearStaleToolMountpoints(toolsDirectory);\n\tconst childEnvironment = buildChildEnvironment(options.environment);\n\tconst readOnlyPaths = [...(options.readOnlyPaths ?? [])];\n\tconst readOnlyFiles = options.authPath ? [options.authPath] : [];\n\tconst readOnlyBinaries = (options.toolBinaries ?? []).map((binary) => ({\n\t\tsource: binary.path,\n\t\tdestination: join(toolsDirectory, binary.name),\n\t}));\n\treturn {\n\t\tcommand: options.command,\n\t\targs: [...options.args],\n\t\tpolicy: { workspace: options.workspace, allowedHosts: options.allowedHosts ?? [] },\n\t\treadOnlyPaths,\n\t\treadOnlyFiles,\n\t\treadOnlyBinaries,\n\t\tenvironment: {\n\t\t\t...childEnvironment,\n\t\t\t...(options.authPath ? { APEX_CODE_AUTH_PATH: options.authPath } : {}),\n\t\t\tAPEX_CODE_CODING_AGENT_DIR: agentDirectory,\n\t\t\tAPEX_CODE_CODING_AGENT_SESSION_DIR: sessionDirectory,\n\t\t\tHOME: stateDirectory,\n\t\t\tTMPDIR: stateDirectory,\n\t\t\t...xdgDirectories,\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"cli-launch.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/cli-launch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKrD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAKvE;AAED,MAAM,WAAW,kBAAmB,SAAQ,aAAa;IACxD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAK1G;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC9B,gCAAgC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,oCAAoC;IACpC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,oEAAoE;AACpE,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,cAAc,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACnD;AA4CD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,sBAAsB,CAY/F;AAmGD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B,GAAG,kBAAkB,CAsDrB","sourcesContent":["import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync } from \"node:fs\";\nimport { join, sep } from \"node:path\";\nimport type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { SettingsManager } from \"../settings-manager.ts\";\nimport { resolveDefaultAllowedHosts } from \"./default-hosts.ts\";\nimport type { SandboxLaunch } from \"./supervisor.ts\";\n\nconst NON_SESSION_COMMANDS = new Set([\"auth\", \"config\", \"install\", \"remove\", \"uninstall\", \"update\", \"list\"]);\nconst METADATA_FLAGS = new Set([\"--version\", \"-v\", \"--export\", \"--list-models\"]);\n\n/**\n * OS containment is the normal startup path for every command that can construct an\n * agent session. Commands that only inspect or maintain host configuration do not\n * create a runtime and therefore remain outside this child boundary.\n */\nexport function requiresSandboxedChild(args: readonly string[]): boolean {\n\tif (NON_SESSION_COMMANDS.has(args[0] ?? \"\")) return false;\n\tif (args.some((argument) => METADATA_FLAGS.has(argument))) return false;\n\tif (args.includes(\"--help\") || args.includes(\"-h\")) return false;\n\treturn true;\n}\n\nexport interface SandboxedCliLaunch extends SandboxLaunch {\n\treadonly environment: NodeJS.ProcessEnv;\n}\n\n/**\n * Read supervisor policy only from global settings; project settings are untrusted here.\n *\n * The built-in provider hosts are added unless explicitly refused, because a deny-all\n * default made a fresh install unable to reach any model while giving the user no way to\n * learn which host to permit. Configured hosts are additive on top, and\n * `allowDefaultHosts: false` restores the strict behaviour for anyone who wants it.\n */\nexport function resolveSupervisorAllowedHosts(cwd: string, agentDir: string): readonly string[] | undefined {\n\tconst network = SettingsManager.create(cwd, agentDir, { projectTrusted: false }).getNetworkSettings();\n\tconst configured = network?.allowedHosts ?? [];\n\tif (network?.allowDefaultHosts === false) return configured;\n\treturn [...new Set([...resolveDefaultAllowedHosts(), ...configured])];\n}\n\n/**\n * The host's two user-scope skill roots, each present only when it exists on the\n * host. Kept as two named slots rather than one list because\n * `core/package-manager.ts` discovers them in different modes -- root `.md` files\n * count as skills under `agentSkills` (\"pi\" mode) and are ignored under\n * `agentsHomeSkills` (\"agents\" mode), per `docs/skills.md` -- and a flat list of 0-2\n * paths cannot tell the child which root a lone survivor was.\n */\nexport interface HostSkillPaths {\n\t/** Host `<agentDir>/skills`. */\n\treadonly agentSkills?: string;\n\t/** Host `<home>/.agents/skills`. */\n\treadonly agentsHomeSkills?: string;\n}\n\n/** A candidate skill root that exists but was excluded, and why. */\nexport interface HostSkillPathRefusal {\n\treadonly root: keyof HostSkillPaths;\n\treadonly path: string;\n\treadonly reason: string;\n}\n\nexport interface ResolvedHostSkillPaths {\n\treadonly paths: HostSkillPaths;\n\treadonly refusals: readonly HostSkillPathRefusal[];\n}\n\n/**\n * True when `candidate` is the host home directory itself, or an ancestor of it.\n * Both paths must already be resolved (`realpathSync`), so a symlink can't disguise\n * either side. Mounting such a candidate read-only would re-expose the entire home\n * tree the sandbox's `--tmpfs /home` (Linux) / `(deny file-read* USER_HOME)` (macOS)\n * deliberately hides -- SSH keys, cloud credentials, other projects -- not just a\n * skills subtree.\n */\nfunction isHomeOrAncestorOfHome(candidate: string, home: string): boolean {\n\tif (candidate === home) return true;\n\tconst prefix = candidate.endsWith(sep) ? candidate : candidate + sep;\n\treturn home.startsWith(prefix);\n}\n\n/** Resolve one candidate root: absent, refused (symlinked onto the host home), or usable. */\nfunction resolveHostSkillRoot(\n\troot: keyof HostSkillPaths,\n\tcandidate: string,\n\thomeDir: string,\n): { path?: string; refusal?: HostSkillPathRefusal } {\n\tif (!existsSync(candidate)) return {};\n\tlet realCandidate: string;\n\tlet realHome: string;\n\ttry {\n\t\trealCandidate = realpathSync(candidate);\n\t\trealHome = realpathSync(homeDir);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { refusal: { root, path: candidate, reason: `could not resolve real path: ${message}` } };\n\t}\n\tif (isHomeOrAncestorOfHome(realCandidate, realHome)) {\n\t\treturn {\n\t\t\trefusal: {\n\t\t\t\troot,\n\t\t\t\tpath: candidate,\n\t\t\t\treason: \"resolves to the host home directory or an ancestor of it\",\n\t\t\t},\n\t\t};\n\t}\n\treturn { path: candidate };\n}\n\n/**\n * Resolve the host's user-scope skill directories, before the sandbox exists to hide\n * them. Read only from the runtime environment and the host agent directory --\n * never from project files -- matching ADR 0016's rule that supervisor policy is\n * trust-first. Mirrors `core/package-manager.ts`'s own user-scope roots so the two\n * sides agree on where a skill lives. A candidate that resolves (directly or via a\n * symlink) onto the host home or an ancestor of it is refused rather than mounted --\n * see `isHomeOrAncestorOfHome` -- and reported so the caller can surface a startup\n * diagnostic instead of silently mounting or silently skipping it.\n */\nexport function resolveHostSkillPaths(agentDir: string, homeDir: string): ResolvedHostSkillPaths {\n\tconst agentSkills = resolveHostSkillRoot(\"agentSkills\", join(agentDir, \"skills\"), homeDir);\n\tconst agentsHomeSkills = resolveHostSkillRoot(\"agentsHomeSkills\", join(homeDir, \".agents\", \"skills\"), homeDir);\n\treturn {\n\t\tpaths: {\n\t\t\t...(agentSkills.path ? { agentSkills: agentSkills.path } : {}),\n\t\t\t...(agentsHomeSkills.path ? { agentsHomeSkills: agentsHomeSkills.path } : {}),\n\t\t},\n\t\trefusals: [agentSkills.refusal, agentsHomeSkills.refusal].filter(\n\t\t\t(refusal): refusal is HostSkillPathRefusal => refusal !== undefined,\n\t\t),\n\t};\n}\n\nconst SAFE_CHILD_ENVIRONMENT_KEYS = new Set([\n\t\"PATH\",\n\t\"LANG\",\n\t\"LC_ALL\",\n\t\"LC_CTYPE\",\n\t\"TERM\",\n\t\"COLORTERM\",\n\t\"NO_COLOR\",\n\t\"FORCE_COLOR\",\n\t\"TSX_TSCONFIG_PATH\",\n\t\"APEX_CODE_OFFLINE\",\n\t\"APEX_CODE_SKIP_VERSION_CHECK\",\n\t\"APEX_CODE_EXPERIMENTAL\",\n\t\"APEX_CODE_STARTUP_BENCHMARK\",\n\t\"APEX_CODE_TIMING\",\n\t\"APEX_CODE_CLEAR_ON_SHRINK\",\n\t\"APEX_CODE_HARDWARE_CURSOR\",\n\t\"APEX_CODE_MODEL_CATALOG_URL\",\n\t\"APEX_CODE_SHARE_VIEWER_URL\",\n\t\"VISUAL\",\n\t\"EDITOR\",\n]);\n\n// Provider API keys are explicit credential inputs, unlike arbitrary ambient variables.\n// Keep this list in sync with the documented provider environment-variable reference.\nconst SAFE_PROVIDER_CREDENTIAL_KEYS = new Set([\n\t\"ANTHROPIC_API_KEY\",\n\t\"ANTHROPIC_AUTH_TOKEN\",\n\t\"ANTHROPIC_OAUTH_TOKEN\",\n\t\"ANT_LING_API_KEY\",\n\t\"OPENAI_API_KEY\",\n\t\"AZURE_OPENAI_API_KEY\",\n\t\"AZURE_OPENAI_BASE_URL\",\n\t\"AZURE_OPENAI_RESOURCE_NAME\",\n\t\"AZURE_OPENAI_API_VERSION\",\n\t\"AZURE_OPENAI_DEPLOYMENT_NAME_MAP\",\n\t\"DEEPSEEK_API_KEY\",\n\t\"NVIDIA_API_KEY\",\n\t\"GEMINI_API_KEY\",\n\t\"GROQ_API_KEY\",\n\t\"CEREBRAS_API_KEY\",\n\t\"XAI_API_KEY\",\n\t\"FIREWORKS_API_KEY\",\n\t\"TOGETHER_API_KEY\",\n\t\"BASETEN_API_KEY\",\n\t\"OPENROUTER_API_KEY\",\n\t\"AI_GATEWAY_API_KEY\",\n\t\"ZAI_API_KEY\",\n\t\"ZAI_CODING_CN_API_KEY\",\n\t\"MISTRAL_API_KEY\",\n\t\"MINIMAX_API_KEY\",\n\t\"MOONSHOT_API_KEY\",\n\t\"OPENCODE_API_KEY\",\n\t\"KIMI_API_KEY\",\n\t\"CLOUDFLARE_API_KEY\",\n\t\"CLOUDFLARE_ACCOUNT_ID\",\n\t\"CLOUDFLARE_GATEWAY_ID\",\n\t\"QWEN_TOKEN_PLAN_API_KEY\",\n\t\"QWEN_TOKEN_PLAN_CN_API_KEY\",\n\t\"XIAOMI_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_CN_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_AMS_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_SGP_API_KEY\",\n\t\"AWS_PROFILE\",\n\t\"AWS_ACCESS_KEY_ID\",\n\t\"AWS_SECRET_ACCESS_KEY\",\n\t\"AWS_BEARER_TOKEN_BEDROCK\",\n\t\"AWS_REGION\",\n]);\n\nfunction buildChildEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n\treturn Object.fromEntries(\n\t\tObject.entries(environment).filter(\n\t\t\t([key]) => SAFE_CHILD_ENVIRONMENT_KEYS.has(key) || SAFE_PROVIDER_CREDENTIAL_KEYS.has(key),\n\t\t),\n\t);\n}\n\n/**\n * Drop empty files left in the child's tools directory by a previous launch.\n *\n * A projected tool is bind-mounted over a file there, and bwrap materialises that\n * mountpoint as an empty file on the host which outlives the namespace. If the host\n * tool later disappears, nothing is projected over the stub and the child would\n * otherwise find a 0-byte file where its binary should be. A real downloaded binary\n * is never empty, so size is a safe discriminator.\n */\nfunction clearStaleToolMountpoints(toolsDirectory: string): void {\n\tfor (const entry of readdirSync(toolsDirectory, { withFileTypes: true })) {\n\t\tif (!entry.isFile()) continue;\n\t\tconst entryPath = join(toolsDirectory, entry.name);\n\t\tif (statSync(entryPath).size === 0) {\n\t\t\trmSync(entryPath, { force: true });\n\t\t}\n\t}\n}\n\n/**\n * Allocate agent-owned state under the sole writable workspace mount. The child does\n * not inherit a host home or a host session/config directory, preventing the sandbox\n * from presenting a write boundary while its own state quietly escapes it.\n */\nexport function buildSandboxedCliLaunch(options: {\n\tworkspace: string;\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n\t/**\n\t * Host user-scope skill directories, pre-filtered by the caller to those that\n\t * exist and pass the host-home escape check (SKILL.4). Mounted read-only at their\n\t * original host location -- Seatbelt cannot remap a path, so this must hold for\n\t * both backends -- and named to the child via `APEX_CODE_SKILL_PATH_*` so its\n\t * discovery can find them under the sandbox's own repointed `HOME`/agent dir.\n\t */\n\tskillPaths?: HostSkillPaths;\n}): SandboxedCliLaunch {\n\tconst stateDirectory = join(options.workspace, \".apex-code\", \"sandbox-state\");\n\tconst agentDirectory = join(options.workspace, \".apex-code\", \"sandbox-agent\");\n\tconst sessionDirectory = join(options.workspace, \".apex-code\", \"sandbox-sessions\");\n\t// Mirrors getBinDir() as the child will compute it from APEX_CODE_CODING_AGENT_DIR,\n\t// so a projected tool lands exactly where the child's own lookup already checks.\n\tconst toolsDirectory = join(agentDirectory, \"bin\");\n\tconst xdgDirectories = {\n\t\tXDG_CONFIG_HOME: join(stateDirectory, \"config\"),\n\t\tXDG_CACHE_HOME: join(stateDirectory, \"cache\"),\n\t\tXDG_DATA_HOME: join(stateDirectory, \"data\"),\n\t\tXDG_STATE_HOME: join(stateDirectory, \"state\"),\n\t};\n\tfor (const directory of [\n\t\tstateDirectory,\n\t\tagentDirectory,\n\t\tsessionDirectory,\n\t\ttoolsDirectory,\n\t\t...Object.values(xdgDirectories),\n\t]) {\n\t\tmkdirSync(directory, { recursive: true });\n\t}\n\tclearStaleToolMountpoints(toolsDirectory);\n\tconst childEnvironment = buildChildEnvironment(options.environment);\n\tconst { agentSkills, agentsHomeSkills } = options.skillPaths ?? {};\n\tconst skillMountPaths = [agentSkills, agentsHomeSkills].filter((path): path is string => path !== undefined);\n\tconst readOnlyPaths = [...(options.readOnlyPaths ?? []), ...skillMountPaths];\n\tconst readOnlyFiles = options.authPath ? [options.authPath] : [];\n\tconst readOnlyBinaries = (options.toolBinaries ?? []).map((binary) => ({\n\t\tsource: binary.path,\n\t\tdestination: join(toolsDirectory, binary.name),\n\t}));\n\treturn {\n\t\tcommand: options.command,\n\t\targs: [...options.args],\n\t\tpolicy: { workspace: options.workspace, allowedHosts: options.allowedHosts ?? [] },\n\t\treadOnlyPaths,\n\t\treadOnlyFiles,\n\t\treadOnlyBinaries,\n\t\tenvironment: {\n\t\t\t...childEnvironment,\n\t\t\t...(options.authPath ? { APEX_CODE_AUTH_PATH: options.authPath } : {}),\n\t\t\t// After childEnvironment: these come only from the supervisor's own\n\t\t\t// resolution, never from the invoking shell, so their value always wins over\n\t\t\t// anything of the same name that childEnvironment's allowlist let through.\n\t\t\t...(agentSkills ? { APEX_CODE_SKILL_PATH_AGENT: agentSkills } : {}),\n\t\t\t...(agentsHomeSkills ? { APEX_CODE_SKILL_PATH_AGENTS_HOME: agentsHomeSkills } : {}),\n\t\t\tAPEX_CODE_CODING_AGENT_DIR: agentDirectory,\n\t\t\tAPEX_CODE_CODING_AGENT_SESSION_DIR: sessionDirectory,\n\t\t\tHOME: stateDirectory,\n\t\t\tTMPDIR: stateDirectory,\n\t\t\t...xdgDirectories,\n\t\t},\n\t};\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
2
+ import { join, sep } from "node:path";
3
3
  import { SettingsManager } from "../settings-manager.js";
4
4
  import { resolveDefaultAllowedHosts } from "./default-hosts.js";
5
5
  const NON_SESSION_COMMANDS = new Set(["auth", "config", "install", "remove", "uninstall", "update", "list"]);
@@ -33,6 +33,66 @@ export function resolveSupervisorAllowedHosts(cwd, agentDir) {
33
33
  return configured;
34
34
  return [...new Set([...resolveDefaultAllowedHosts(), ...configured])];
35
35
  }
36
+ /**
37
+ * True when `candidate` is the host home directory itself, or an ancestor of it.
38
+ * Both paths must already be resolved (`realpathSync`), so a symlink can't disguise
39
+ * either side. Mounting such a candidate read-only would re-expose the entire home
40
+ * tree the sandbox's `--tmpfs /home` (Linux) / `(deny file-read* USER_HOME)` (macOS)
41
+ * deliberately hides -- SSH keys, cloud credentials, other projects -- not just a
42
+ * skills subtree.
43
+ */
44
+ function isHomeOrAncestorOfHome(candidate, home) {
45
+ if (candidate === home)
46
+ return true;
47
+ const prefix = candidate.endsWith(sep) ? candidate : candidate + sep;
48
+ return home.startsWith(prefix);
49
+ }
50
+ /** Resolve one candidate root: absent, refused (symlinked onto the host home), or usable. */
51
+ function resolveHostSkillRoot(root, candidate, homeDir) {
52
+ if (!existsSync(candidate))
53
+ return {};
54
+ let realCandidate;
55
+ let realHome;
56
+ try {
57
+ realCandidate = realpathSync(candidate);
58
+ realHome = realpathSync(homeDir);
59
+ }
60
+ catch (error) {
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ return { refusal: { root, path: candidate, reason: `could not resolve real path: ${message}` } };
63
+ }
64
+ if (isHomeOrAncestorOfHome(realCandidate, realHome)) {
65
+ return {
66
+ refusal: {
67
+ root,
68
+ path: candidate,
69
+ reason: "resolves to the host home directory or an ancestor of it",
70
+ },
71
+ };
72
+ }
73
+ return { path: candidate };
74
+ }
75
+ /**
76
+ * Resolve the host's user-scope skill directories, before the sandbox exists to hide
77
+ * them. Read only from the runtime environment and the host agent directory --
78
+ * never from project files -- matching ADR 0016's rule that supervisor policy is
79
+ * trust-first. Mirrors `core/package-manager.ts`'s own user-scope roots so the two
80
+ * sides agree on where a skill lives. A candidate that resolves (directly or via a
81
+ * symlink) onto the host home or an ancestor of it is refused rather than mounted --
82
+ * see `isHomeOrAncestorOfHome` -- and reported so the caller can surface a startup
83
+ * diagnostic instead of silently mounting or silently skipping it.
84
+ */
85
+ export function resolveHostSkillPaths(agentDir, homeDir) {
86
+ const agentSkills = resolveHostSkillRoot("agentSkills", join(agentDir, "skills"), homeDir);
87
+ const agentsHomeSkills = resolveHostSkillRoot("agentsHomeSkills", join(homeDir, ".agents", "skills"), homeDir);
88
+ return {
89
+ paths: {
90
+ ...(agentSkills.path ? { agentSkills: agentSkills.path } : {}),
91
+ ...(agentsHomeSkills.path ? { agentsHomeSkills: agentsHomeSkills.path } : {}),
92
+ },
93
+ refusals: [agentSkills.refusal, agentsHomeSkills.refusal].filter((refusal) => refusal !== undefined),
94
+ };
95
+ }
36
96
  const SAFE_CHILD_ENVIRONMENT_KEYS = new Set([
37
97
  "PATH",
38
98
  "LANG",
@@ -152,7 +212,9 @@ export function buildSandboxedCliLaunch(options) {
152
212
  }
153
213
  clearStaleToolMountpoints(toolsDirectory);
154
214
  const childEnvironment = buildChildEnvironment(options.environment);
155
- const readOnlyPaths = [...(options.readOnlyPaths ?? [])];
215
+ const { agentSkills, agentsHomeSkills } = options.skillPaths ?? {};
216
+ const skillMountPaths = [agentSkills, agentsHomeSkills].filter((path) => path !== undefined);
217
+ const readOnlyPaths = [...(options.readOnlyPaths ?? []), ...skillMountPaths];
156
218
  const readOnlyFiles = options.authPath ? [options.authPath] : [];
157
219
  const readOnlyBinaries = (options.toolBinaries ?? []).map((binary) => ({
158
220
  source: binary.path,
@@ -168,6 +230,11 @@ export function buildSandboxedCliLaunch(options) {
168
230
  environment: {
169
231
  ...childEnvironment,
170
232
  ...(options.authPath ? { APEX_CODE_AUTH_PATH: options.authPath } : {}),
233
+ // After childEnvironment: these come only from the supervisor's own
234
+ // resolution, never from the invoking shell, so their value always wins over
235
+ // anything of the same name that childEnvironment's allowlist let through.
236
+ ...(agentSkills ? { APEX_CODE_SKILL_PATH_AGENT: agentSkills } : {}),
237
+ ...(agentsHomeSkills ? { APEX_CODE_SKILL_PATH_AGENTS_HOME: agentsHomeSkills } : {}),
171
238
  APEX_CODE_CODING_AGENT_DIR: agentDirectory,
172
239
  APEX_CODE_CODING_AGENT_SESSION_DIR: sessionDirectory,
173
240
  HOME: stateDirectory,
@@ -1 +1 @@
1
- {"version":3,"file":"cli-launch.js","sourceRoot":"","sources":["../../../src/core/sandbox/cli-launch.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, readdirSync, rmSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { SettingsManager } from \"../settings-manager.ts\";\nimport { resolveDefaultAllowedHosts } from \"./default-hosts.ts\";\nimport type { SandboxLaunch } from \"./supervisor.ts\";\n\nconst NON_SESSION_COMMANDS = new Set([\"auth\", \"config\", \"install\", \"remove\", \"uninstall\", \"update\", \"list\"]);\nconst METADATA_FLAGS = new Set([\"--version\", \"-v\", \"--export\", \"--list-models\"]);\n\n/**\n * OS containment is the normal startup path for every command that can construct an\n * agent session. Commands that only inspect or maintain host configuration do not\n * create a runtime and therefore remain outside this child boundary.\n */\nexport function requiresSandboxedChild(args: readonly string[]): boolean {\n\tif (NON_SESSION_COMMANDS.has(args[0] ?? \"\")) return false;\n\tif (args.some((argument) => METADATA_FLAGS.has(argument))) return false;\n\tif (args.includes(\"--help\") || args.includes(\"-h\")) return false;\n\treturn true;\n}\n\nexport interface SandboxedCliLaunch extends SandboxLaunch {\n\treadonly environment: NodeJS.ProcessEnv;\n}\n\n/**\n * Read supervisor policy only from global settings; project settings are untrusted here.\n *\n * The built-in provider hosts are added unless explicitly refused, because a deny-all\n * default made a fresh install unable to reach any model while giving the user no way to\n * learn which host to permit. Configured hosts are additive on top, and\n * `allowDefaultHosts: false` restores the strict behaviour for anyone who wants it.\n */\nexport function resolveSupervisorAllowedHosts(cwd: string, agentDir: string): readonly string[] | undefined {\n\tconst network = SettingsManager.create(cwd, agentDir, { projectTrusted: false }).getNetworkSettings();\n\tconst configured = network?.allowedHosts ?? [];\n\tif (network?.allowDefaultHosts === false) return configured;\n\treturn [...new Set([...resolveDefaultAllowedHosts(), ...configured])];\n}\n\nconst SAFE_CHILD_ENVIRONMENT_KEYS = new Set([\n\t\"PATH\",\n\t\"LANG\",\n\t\"LC_ALL\",\n\t\"LC_CTYPE\",\n\t\"TERM\",\n\t\"COLORTERM\",\n\t\"NO_COLOR\",\n\t\"FORCE_COLOR\",\n\t\"TSX_TSCONFIG_PATH\",\n\t\"APEX_CODE_OFFLINE\",\n\t\"APEX_CODE_SKIP_VERSION_CHECK\",\n\t\"APEX_CODE_EXPERIMENTAL\",\n\t\"APEX_CODE_STARTUP_BENCHMARK\",\n\t\"APEX_CODE_TIMING\",\n\t\"APEX_CODE_CLEAR_ON_SHRINK\",\n\t\"APEX_CODE_HARDWARE_CURSOR\",\n\t\"APEX_CODE_MODEL_CATALOG_URL\",\n\t\"APEX_CODE_SHARE_VIEWER_URL\",\n\t\"VISUAL\",\n\t\"EDITOR\",\n]);\n\n// Provider API keys are explicit credential inputs, unlike arbitrary ambient variables.\n// Keep this list in sync with the documented provider environment-variable reference.\nconst SAFE_PROVIDER_CREDENTIAL_KEYS = new Set([\n\t\"ANTHROPIC_API_KEY\",\n\t\"ANTHROPIC_AUTH_TOKEN\",\n\t\"ANTHROPIC_OAUTH_TOKEN\",\n\t\"ANT_LING_API_KEY\",\n\t\"OPENAI_API_KEY\",\n\t\"AZURE_OPENAI_API_KEY\",\n\t\"AZURE_OPENAI_BASE_URL\",\n\t\"AZURE_OPENAI_RESOURCE_NAME\",\n\t\"AZURE_OPENAI_API_VERSION\",\n\t\"AZURE_OPENAI_DEPLOYMENT_NAME_MAP\",\n\t\"DEEPSEEK_API_KEY\",\n\t\"NVIDIA_API_KEY\",\n\t\"GEMINI_API_KEY\",\n\t\"GROQ_API_KEY\",\n\t\"CEREBRAS_API_KEY\",\n\t\"XAI_API_KEY\",\n\t\"FIREWORKS_API_KEY\",\n\t\"TOGETHER_API_KEY\",\n\t\"BASETEN_API_KEY\",\n\t\"OPENROUTER_API_KEY\",\n\t\"AI_GATEWAY_API_KEY\",\n\t\"ZAI_API_KEY\",\n\t\"ZAI_CODING_CN_API_KEY\",\n\t\"MISTRAL_API_KEY\",\n\t\"MINIMAX_API_KEY\",\n\t\"MOONSHOT_API_KEY\",\n\t\"OPENCODE_API_KEY\",\n\t\"KIMI_API_KEY\",\n\t\"CLOUDFLARE_API_KEY\",\n\t\"CLOUDFLARE_ACCOUNT_ID\",\n\t\"CLOUDFLARE_GATEWAY_ID\",\n\t\"QWEN_TOKEN_PLAN_API_KEY\",\n\t\"QWEN_TOKEN_PLAN_CN_API_KEY\",\n\t\"XIAOMI_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_CN_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_AMS_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_SGP_API_KEY\",\n\t\"AWS_PROFILE\",\n\t\"AWS_ACCESS_KEY_ID\",\n\t\"AWS_SECRET_ACCESS_KEY\",\n\t\"AWS_BEARER_TOKEN_BEDROCK\",\n\t\"AWS_REGION\",\n]);\n\nfunction buildChildEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n\treturn Object.fromEntries(\n\t\tObject.entries(environment).filter(\n\t\t\t([key]) => SAFE_CHILD_ENVIRONMENT_KEYS.has(key) || SAFE_PROVIDER_CREDENTIAL_KEYS.has(key),\n\t\t),\n\t);\n}\n\n/**\n * Drop empty files left in the child's tools directory by a previous launch.\n *\n * A projected tool is bind-mounted over a file there, and bwrap materialises that\n * mountpoint as an empty file on the host which outlives the namespace. If the host\n * tool later disappears, nothing is projected over the stub and the child would\n * otherwise find a 0-byte file where its binary should be. A real downloaded binary\n * is never empty, so size is a safe discriminator.\n */\nfunction clearStaleToolMountpoints(toolsDirectory: string): void {\n\tfor (const entry of readdirSync(toolsDirectory, { withFileTypes: true })) {\n\t\tif (!entry.isFile()) continue;\n\t\tconst entryPath = join(toolsDirectory, entry.name);\n\t\tif (statSync(entryPath).size === 0) {\n\t\t\trmSync(entryPath, { force: true });\n\t\t}\n\t}\n}\n\n/**\n * Allocate agent-owned state under the sole writable workspace mount. The child does\n * not inherit a host home or a host session/config directory, preventing the sandbox\n * from presenting a write boundary while its own state quietly escapes it.\n */\nexport function buildSandboxedCliLaunch(options: {\n\tworkspace: string;\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n}): SandboxedCliLaunch {\n\tconst stateDirectory = join(options.workspace, \".apex-code\", \"sandbox-state\");\n\tconst agentDirectory = join(options.workspace, \".apex-code\", \"sandbox-agent\");\n\tconst sessionDirectory = join(options.workspace, \".apex-code\", \"sandbox-sessions\");\n\t// Mirrors getBinDir() as the child will compute it from APEX_CODE_CODING_AGENT_DIR,\n\t// so a projected tool lands exactly where the child's own lookup already checks.\n\tconst toolsDirectory = join(agentDirectory, \"bin\");\n\tconst xdgDirectories = {\n\t\tXDG_CONFIG_HOME: join(stateDirectory, \"config\"),\n\t\tXDG_CACHE_HOME: join(stateDirectory, \"cache\"),\n\t\tXDG_DATA_HOME: join(stateDirectory, \"data\"),\n\t\tXDG_STATE_HOME: join(stateDirectory, \"state\"),\n\t};\n\tfor (const directory of [\n\t\tstateDirectory,\n\t\tagentDirectory,\n\t\tsessionDirectory,\n\t\ttoolsDirectory,\n\t\t...Object.values(xdgDirectories),\n\t]) {\n\t\tmkdirSync(directory, { recursive: true });\n\t}\n\tclearStaleToolMountpoints(toolsDirectory);\n\tconst childEnvironment = buildChildEnvironment(options.environment);\n\tconst readOnlyPaths = [...(options.readOnlyPaths ?? [])];\n\tconst readOnlyFiles = options.authPath ? [options.authPath] : [];\n\tconst readOnlyBinaries = (options.toolBinaries ?? []).map((binary) => ({\n\t\tsource: binary.path,\n\t\tdestination: join(toolsDirectory, binary.name),\n\t}));\n\treturn {\n\t\tcommand: options.command,\n\t\targs: [...options.args],\n\t\tpolicy: { workspace: options.workspace, allowedHosts: options.allowedHosts ?? [] },\n\t\treadOnlyPaths,\n\t\treadOnlyFiles,\n\t\treadOnlyBinaries,\n\t\tenvironment: {\n\t\t\t...childEnvironment,\n\t\t\t...(options.authPath ? { APEX_CODE_AUTH_PATH: options.authPath } : {}),\n\t\t\tAPEX_CODE_CODING_AGENT_DIR: agentDirectory,\n\t\t\tAPEX_CODE_CODING_AGENT_SESSION_DIR: sessionDirectory,\n\t\t\tHOME: stateDirectory,\n\t\t\tTMPDIR: stateDirectory,\n\t\t\t...xdgDirectories,\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"cli-launch.js","sourceRoot":"","sources":["../../../src/core/sandbox/cli-launch.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync } from \"node:fs\";\nimport { join, sep } from \"node:path\";\nimport type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { SettingsManager } from \"../settings-manager.ts\";\nimport { resolveDefaultAllowedHosts } from \"./default-hosts.ts\";\nimport type { SandboxLaunch } from \"./supervisor.ts\";\n\nconst NON_SESSION_COMMANDS = new Set([\"auth\", \"config\", \"install\", \"remove\", \"uninstall\", \"update\", \"list\"]);\nconst METADATA_FLAGS = new Set([\"--version\", \"-v\", \"--export\", \"--list-models\"]);\n\n/**\n * OS containment is the normal startup path for every command that can construct an\n * agent session. Commands that only inspect or maintain host configuration do not\n * create a runtime and therefore remain outside this child boundary.\n */\nexport function requiresSandboxedChild(args: readonly string[]): boolean {\n\tif (NON_SESSION_COMMANDS.has(args[0] ?? \"\")) return false;\n\tif (args.some((argument) => METADATA_FLAGS.has(argument))) return false;\n\tif (args.includes(\"--help\") || args.includes(\"-h\")) return false;\n\treturn true;\n}\n\nexport interface SandboxedCliLaunch extends SandboxLaunch {\n\treadonly environment: NodeJS.ProcessEnv;\n}\n\n/**\n * Read supervisor policy only from global settings; project settings are untrusted here.\n *\n * The built-in provider hosts are added unless explicitly refused, because a deny-all\n * default made a fresh install unable to reach any model while giving the user no way to\n * learn which host to permit. Configured hosts are additive on top, and\n * `allowDefaultHosts: false` restores the strict behaviour for anyone who wants it.\n */\nexport function resolveSupervisorAllowedHosts(cwd: string, agentDir: string): readonly string[] | undefined {\n\tconst network = SettingsManager.create(cwd, agentDir, { projectTrusted: false }).getNetworkSettings();\n\tconst configured = network?.allowedHosts ?? [];\n\tif (network?.allowDefaultHosts === false) return configured;\n\treturn [...new Set([...resolveDefaultAllowedHosts(), ...configured])];\n}\n\n/**\n * The host's two user-scope skill roots, each present only when it exists on the\n * host. Kept as two named slots rather than one list because\n * `core/package-manager.ts` discovers them in different modes -- root `.md` files\n * count as skills under `agentSkills` (\"pi\" mode) and are ignored under\n * `agentsHomeSkills` (\"agents\" mode), per `docs/skills.md` -- and a flat list of 0-2\n * paths cannot tell the child which root a lone survivor was.\n */\nexport interface HostSkillPaths {\n\t/** Host `<agentDir>/skills`. */\n\treadonly agentSkills?: string;\n\t/** Host `<home>/.agents/skills`. */\n\treadonly agentsHomeSkills?: string;\n}\n\n/** A candidate skill root that exists but was excluded, and why. */\nexport interface HostSkillPathRefusal {\n\treadonly root: keyof HostSkillPaths;\n\treadonly path: string;\n\treadonly reason: string;\n}\n\nexport interface ResolvedHostSkillPaths {\n\treadonly paths: HostSkillPaths;\n\treadonly refusals: readonly HostSkillPathRefusal[];\n}\n\n/**\n * True when `candidate` is the host home directory itself, or an ancestor of it.\n * Both paths must already be resolved (`realpathSync`), so a symlink can't disguise\n * either side. Mounting such a candidate read-only would re-expose the entire home\n * tree the sandbox's `--tmpfs /home` (Linux) / `(deny file-read* USER_HOME)` (macOS)\n * deliberately hides -- SSH keys, cloud credentials, other projects -- not just a\n * skills subtree.\n */\nfunction isHomeOrAncestorOfHome(candidate: string, home: string): boolean {\n\tif (candidate === home) return true;\n\tconst prefix = candidate.endsWith(sep) ? candidate : candidate + sep;\n\treturn home.startsWith(prefix);\n}\n\n/** Resolve one candidate root: absent, refused (symlinked onto the host home), or usable. */\nfunction resolveHostSkillRoot(\n\troot: keyof HostSkillPaths,\n\tcandidate: string,\n\thomeDir: string,\n): { path?: string; refusal?: HostSkillPathRefusal } {\n\tif (!existsSync(candidate)) return {};\n\tlet realCandidate: string;\n\tlet realHome: string;\n\ttry {\n\t\trealCandidate = realpathSync(candidate);\n\t\trealHome = realpathSync(homeDir);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { refusal: { root, path: candidate, reason: `could not resolve real path: ${message}` } };\n\t}\n\tif (isHomeOrAncestorOfHome(realCandidate, realHome)) {\n\t\treturn {\n\t\t\trefusal: {\n\t\t\t\troot,\n\t\t\t\tpath: candidate,\n\t\t\t\treason: \"resolves to the host home directory or an ancestor of it\",\n\t\t\t},\n\t\t};\n\t}\n\treturn { path: candidate };\n}\n\n/**\n * Resolve the host's user-scope skill directories, before the sandbox exists to hide\n * them. Read only from the runtime environment and the host agent directory --\n * never from project files -- matching ADR 0016's rule that supervisor policy is\n * trust-first. Mirrors `core/package-manager.ts`'s own user-scope roots so the two\n * sides agree on where a skill lives. A candidate that resolves (directly or via a\n * symlink) onto the host home or an ancestor of it is refused rather than mounted --\n * see `isHomeOrAncestorOfHome` -- and reported so the caller can surface a startup\n * diagnostic instead of silently mounting or silently skipping it.\n */\nexport function resolveHostSkillPaths(agentDir: string, homeDir: string): ResolvedHostSkillPaths {\n\tconst agentSkills = resolveHostSkillRoot(\"agentSkills\", join(agentDir, \"skills\"), homeDir);\n\tconst agentsHomeSkills = resolveHostSkillRoot(\"agentsHomeSkills\", join(homeDir, \".agents\", \"skills\"), homeDir);\n\treturn {\n\t\tpaths: {\n\t\t\t...(agentSkills.path ? { agentSkills: agentSkills.path } : {}),\n\t\t\t...(agentsHomeSkills.path ? { agentsHomeSkills: agentsHomeSkills.path } : {}),\n\t\t},\n\t\trefusals: [agentSkills.refusal, agentsHomeSkills.refusal].filter(\n\t\t\t(refusal): refusal is HostSkillPathRefusal => refusal !== undefined,\n\t\t),\n\t};\n}\n\nconst SAFE_CHILD_ENVIRONMENT_KEYS = new Set([\n\t\"PATH\",\n\t\"LANG\",\n\t\"LC_ALL\",\n\t\"LC_CTYPE\",\n\t\"TERM\",\n\t\"COLORTERM\",\n\t\"NO_COLOR\",\n\t\"FORCE_COLOR\",\n\t\"TSX_TSCONFIG_PATH\",\n\t\"APEX_CODE_OFFLINE\",\n\t\"APEX_CODE_SKIP_VERSION_CHECK\",\n\t\"APEX_CODE_EXPERIMENTAL\",\n\t\"APEX_CODE_STARTUP_BENCHMARK\",\n\t\"APEX_CODE_TIMING\",\n\t\"APEX_CODE_CLEAR_ON_SHRINK\",\n\t\"APEX_CODE_HARDWARE_CURSOR\",\n\t\"APEX_CODE_MODEL_CATALOG_URL\",\n\t\"APEX_CODE_SHARE_VIEWER_URL\",\n\t\"VISUAL\",\n\t\"EDITOR\",\n]);\n\n// Provider API keys are explicit credential inputs, unlike arbitrary ambient variables.\n// Keep this list in sync with the documented provider environment-variable reference.\nconst SAFE_PROVIDER_CREDENTIAL_KEYS = new Set([\n\t\"ANTHROPIC_API_KEY\",\n\t\"ANTHROPIC_AUTH_TOKEN\",\n\t\"ANTHROPIC_OAUTH_TOKEN\",\n\t\"ANT_LING_API_KEY\",\n\t\"OPENAI_API_KEY\",\n\t\"AZURE_OPENAI_API_KEY\",\n\t\"AZURE_OPENAI_BASE_URL\",\n\t\"AZURE_OPENAI_RESOURCE_NAME\",\n\t\"AZURE_OPENAI_API_VERSION\",\n\t\"AZURE_OPENAI_DEPLOYMENT_NAME_MAP\",\n\t\"DEEPSEEK_API_KEY\",\n\t\"NVIDIA_API_KEY\",\n\t\"GEMINI_API_KEY\",\n\t\"GROQ_API_KEY\",\n\t\"CEREBRAS_API_KEY\",\n\t\"XAI_API_KEY\",\n\t\"FIREWORKS_API_KEY\",\n\t\"TOGETHER_API_KEY\",\n\t\"BASETEN_API_KEY\",\n\t\"OPENROUTER_API_KEY\",\n\t\"AI_GATEWAY_API_KEY\",\n\t\"ZAI_API_KEY\",\n\t\"ZAI_CODING_CN_API_KEY\",\n\t\"MISTRAL_API_KEY\",\n\t\"MINIMAX_API_KEY\",\n\t\"MOONSHOT_API_KEY\",\n\t\"OPENCODE_API_KEY\",\n\t\"KIMI_API_KEY\",\n\t\"CLOUDFLARE_API_KEY\",\n\t\"CLOUDFLARE_ACCOUNT_ID\",\n\t\"CLOUDFLARE_GATEWAY_ID\",\n\t\"QWEN_TOKEN_PLAN_API_KEY\",\n\t\"QWEN_TOKEN_PLAN_CN_API_KEY\",\n\t\"XIAOMI_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_CN_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_AMS_API_KEY\",\n\t\"XIAOMI_TOKEN_PLAN_SGP_API_KEY\",\n\t\"AWS_PROFILE\",\n\t\"AWS_ACCESS_KEY_ID\",\n\t\"AWS_SECRET_ACCESS_KEY\",\n\t\"AWS_BEARER_TOKEN_BEDROCK\",\n\t\"AWS_REGION\",\n]);\n\nfunction buildChildEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n\treturn Object.fromEntries(\n\t\tObject.entries(environment).filter(\n\t\t\t([key]) => SAFE_CHILD_ENVIRONMENT_KEYS.has(key) || SAFE_PROVIDER_CREDENTIAL_KEYS.has(key),\n\t\t),\n\t);\n}\n\n/**\n * Drop empty files left in the child's tools directory by a previous launch.\n *\n * A projected tool is bind-mounted over a file there, and bwrap materialises that\n * mountpoint as an empty file on the host which outlives the namespace. If the host\n * tool later disappears, nothing is projected over the stub and the child would\n * otherwise find a 0-byte file where its binary should be. A real downloaded binary\n * is never empty, so size is a safe discriminator.\n */\nfunction clearStaleToolMountpoints(toolsDirectory: string): void {\n\tfor (const entry of readdirSync(toolsDirectory, { withFileTypes: true })) {\n\t\tif (!entry.isFile()) continue;\n\t\tconst entryPath = join(toolsDirectory, entry.name);\n\t\tif (statSync(entryPath).size === 0) {\n\t\t\trmSync(entryPath, { force: true });\n\t\t}\n\t}\n}\n\n/**\n * Allocate agent-owned state under the sole writable workspace mount. The child does\n * not inherit a host home or a host session/config directory, preventing the sandbox\n * from presenting a write boundary while its own state quietly escapes it.\n */\nexport function buildSandboxedCliLaunch(options: {\n\tworkspace: string;\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n\t/**\n\t * Host user-scope skill directories, pre-filtered by the caller to those that\n\t * exist and pass the host-home escape check (SKILL.4). Mounted read-only at their\n\t * original host location -- Seatbelt cannot remap a path, so this must hold for\n\t * both backends -- and named to the child via `APEX_CODE_SKILL_PATH_*` so its\n\t * discovery can find them under the sandbox's own repointed `HOME`/agent dir.\n\t */\n\tskillPaths?: HostSkillPaths;\n}): SandboxedCliLaunch {\n\tconst stateDirectory = join(options.workspace, \".apex-code\", \"sandbox-state\");\n\tconst agentDirectory = join(options.workspace, \".apex-code\", \"sandbox-agent\");\n\tconst sessionDirectory = join(options.workspace, \".apex-code\", \"sandbox-sessions\");\n\t// Mirrors getBinDir() as the child will compute it from APEX_CODE_CODING_AGENT_DIR,\n\t// so a projected tool lands exactly where the child's own lookup already checks.\n\tconst toolsDirectory = join(agentDirectory, \"bin\");\n\tconst xdgDirectories = {\n\t\tXDG_CONFIG_HOME: join(stateDirectory, \"config\"),\n\t\tXDG_CACHE_HOME: join(stateDirectory, \"cache\"),\n\t\tXDG_DATA_HOME: join(stateDirectory, \"data\"),\n\t\tXDG_STATE_HOME: join(stateDirectory, \"state\"),\n\t};\n\tfor (const directory of [\n\t\tstateDirectory,\n\t\tagentDirectory,\n\t\tsessionDirectory,\n\t\ttoolsDirectory,\n\t\t...Object.values(xdgDirectories),\n\t]) {\n\t\tmkdirSync(directory, { recursive: true });\n\t}\n\tclearStaleToolMountpoints(toolsDirectory);\n\tconst childEnvironment = buildChildEnvironment(options.environment);\n\tconst { agentSkills, agentsHomeSkills } = options.skillPaths ?? {};\n\tconst skillMountPaths = [agentSkills, agentsHomeSkills].filter((path): path is string => path !== undefined);\n\tconst readOnlyPaths = [...(options.readOnlyPaths ?? []), ...skillMountPaths];\n\tconst readOnlyFiles = options.authPath ? [options.authPath] : [];\n\tconst readOnlyBinaries = (options.toolBinaries ?? []).map((binary) => ({\n\t\tsource: binary.path,\n\t\tdestination: join(toolsDirectory, binary.name),\n\t}));\n\treturn {\n\t\tcommand: options.command,\n\t\targs: [...options.args],\n\t\tpolicy: { workspace: options.workspace, allowedHosts: options.allowedHosts ?? [] },\n\t\treadOnlyPaths,\n\t\treadOnlyFiles,\n\t\treadOnlyBinaries,\n\t\tenvironment: {\n\t\t\t...childEnvironment,\n\t\t\t...(options.authPath ? { APEX_CODE_AUTH_PATH: options.authPath } : {}),\n\t\t\t// After childEnvironment: these come only from the supervisor's own\n\t\t\t// resolution, never from the invoking shell, so their value always wins over\n\t\t\t// anything of the same name that childEnvironment's allowlist let through.\n\t\t\t...(agentSkills ? { APEX_CODE_SKILL_PATH_AGENT: agentSkills } : {}),\n\t\t\t...(agentsHomeSkills ? { APEX_CODE_SKILL_PATH_AGENTS_HOME: agentsHomeSkills } : {}),\n\t\t\tAPEX_CODE_CODING_AGENT_DIR: agentDirectory,\n\t\t\tAPEX_CODE_CODING_AGENT_SESSION_DIR: sessionDirectory,\n\t\t\tHOME: stateDirectory,\n\t\t\tTMPDIR: stateDirectory,\n\t\t\t...xdgDirectories,\n\t\t},\n\t};\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { HostToolBinary } from "../../utils/tools-manager.ts";
2
+ import { type HostSkillPaths } from "./cli-launch.ts";
2
3
  import { type SandboxBackend } from "./supervisor.ts";
3
4
  import { SandboxViolationStore } from "./violations.ts";
4
5
  export interface CliSandboxDependencies {
@@ -22,6 +23,7 @@ export declare function launchSandboxedCli(options: {
22
23
  readOnlyPaths?: readonly string[];
23
24
  authPath?: string;
24
25
  toolBinaries?: readonly HostToolBinary[];
26
+ skillPaths?: HostSkillPaths;
25
27
  dependencies?: Partial<CliSandboxDependencies>;
26
28
  }): Promise<number>;
27
29
  //# sourceMappingURL=cli-supervisor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-supervisor.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/cli-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAKnE,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,OAAO,EAAE;QAAE,cAAc,EAAE,qBAAqB,CAAA;KAAE,KAAK,cAAc,CAAC;IACtF,MAAM,EAAE;QAAE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;CAC5C;AAeD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,YAAY,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC/C,GAAG,OAAO,CAAC,MAAM,CAAC,CAkClB","sourcesContent":["import type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { buildSandboxedCliLaunch } from \"./cli-launch.ts\";\nimport { createLinuxSandboxBackend } from \"./linux-backend.ts\";\nimport { createMacosSandboxBackend } from \"./macos-backend.ts\";\nimport { createSandboxPolicy } from \"./policy.ts\";\nimport { createSandboxSupervisor, type SandboxBackend } from \"./supervisor.ts\";\nimport { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface CliSandboxDependencies {\n\tcreateBackend: (options: { violationStore: SandboxViolationStore }) => SandboxBackend;\n\tstderr: { write(message: string): boolean };\n}\n\n/** Routes to the platform adapter for the running OS; each adapter self-reports\n * `unavailable` on any other platform, so this only needs to pick the one whose\n * enforcement is actually reachable here. */\nfunction createDefaultSandboxBackend(options: { violationStore: SandboxViolationStore }): SandboxBackend {\n\tif (process.platform === \"darwin\") return createMacosSandboxBackend(options);\n\treturn createLinuxSandboxBackend(options);\n}\n\nconst defaultDependencies: CliSandboxDependencies = {\n\tcreateBackend: createDefaultSandboxBackend,\n\tstderr: process.stderr,\n};\n\n/**\n * Start a normal CLI runtime beneath the whole-process boundary. The caller returns\n * the result directly so the outer process never continues into `main()` afterward.\n */\nexport async function launchSandboxedCli(options: {\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n\tdependencies?: Partial<CliSandboxDependencies>;\n}): Promise<number> {\n\tconst dependencies = { ...defaultDependencies, ...options.dependencies };\n\tconst policyResult = createSandboxPolicy({ workspace: options.workspace, allowedHosts: options.allowedHosts });\n\tif (policyResult.kind === \"invalid\") {\n\t\tdependencies.stderr.write(`Error: OS sandbox is not enforcing this agent session: ${policyResult.reason}\\n`);\n\t\treturn 1;\n\t}\n\tconst violationStore = new SandboxViolationStore();\n\tconst backend = dependencies.createBackend({ violationStore });\n\tconst supervisor = createSandboxSupervisor({ backend, policy: policyResult.policy });\n\tconst launch = buildSandboxedCliLaunch({\n\t\tworkspace: policyResult.policy.workspace,\n\t\tcommand: options.command,\n\t\targs: options.args,\n\t\tenvironment: options.environment,\n\t\tallowedHosts: options.allowedHosts,\n\t\treadOnlyPaths: options.readOnlyPaths,\n\t\tauthPath: options.authPath,\n\t\ttoolBinaries: options.toolBinaries,\n\t});\n\ttry {\n\t\treturn await supervisor.launch(launch);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : \"Failed to start OS sandbox.\";\n\t\tdependencies.stderr.write(`Error: ${message}\\n`);\n\t\treturn 1;\n\t} finally {\n\t\tfor (const violation of violationStore.list()) {\n\t\t\tdependencies.stderr.write(\n\t\t\t\t`Sandbox violation (${violation.kind}): ${violation.command} — ${violation.detail}\\n`,\n\t\t\t);\n\t\t}\n\t\tawait supervisor.close();\n\t}\n}\n"]}
1
+ {"version":3,"file":"cli-supervisor.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/cli-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAI/E,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,OAAO,EAAE;QAAE,cAAc,EAAE,qBAAqB,CAAA;KAAE,KAAK,cAAc,CAAC;IACtF,MAAM,EAAE;QAAE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;CAC5C;AAeD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC/C,GAAG,OAAO,CAAC,MAAM,CAAC,CAmClB","sourcesContent":["import type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { buildSandboxedCliLaunch, type HostSkillPaths } from \"./cli-launch.ts\";\nimport { createLinuxSandboxBackend } from \"./linux-backend.ts\";\nimport { createMacosSandboxBackend } from \"./macos-backend.ts\";\nimport { createSandboxPolicy } from \"./policy.ts\";\nimport { createSandboxSupervisor, type SandboxBackend } from \"./supervisor.ts\";\nimport { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface CliSandboxDependencies {\n\tcreateBackend: (options: { violationStore: SandboxViolationStore }) => SandboxBackend;\n\tstderr: { write(message: string): boolean };\n}\n\n/** Routes to the platform adapter for the running OS; each adapter self-reports\n * `unavailable` on any other platform, so this only needs to pick the one whose\n * enforcement is actually reachable here. */\nfunction createDefaultSandboxBackend(options: { violationStore: SandboxViolationStore }): SandboxBackend {\n\tif (process.platform === \"darwin\") return createMacosSandboxBackend(options);\n\treturn createLinuxSandboxBackend(options);\n}\n\nconst defaultDependencies: CliSandboxDependencies = {\n\tcreateBackend: createDefaultSandboxBackend,\n\tstderr: process.stderr,\n};\n\n/**\n * Start a normal CLI runtime beneath the whole-process boundary. The caller returns\n * the result directly so the outer process never continues into `main()` afterward.\n */\nexport async function launchSandboxedCli(options: {\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n\tskillPaths?: HostSkillPaths;\n\tdependencies?: Partial<CliSandboxDependencies>;\n}): Promise<number> {\n\tconst dependencies = { ...defaultDependencies, ...options.dependencies };\n\tconst policyResult = createSandboxPolicy({ workspace: options.workspace, allowedHosts: options.allowedHosts });\n\tif (policyResult.kind === \"invalid\") {\n\t\tdependencies.stderr.write(`Error: OS sandbox is not enforcing this agent session: ${policyResult.reason}\\n`);\n\t\treturn 1;\n\t}\n\tconst violationStore = new SandboxViolationStore();\n\tconst backend = dependencies.createBackend({ violationStore });\n\tconst supervisor = createSandboxSupervisor({ backend, policy: policyResult.policy });\n\tconst launch = buildSandboxedCliLaunch({\n\t\tworkspace: policyResult.policy.workspace,\n\t\tcommand: options.command,\n\t\targs: options.args,\n\t\tenvironment: options.environment,\n\t\tallowedHosts: options.allowedHosts,\n\t\treadOnlyPaths: options.readOnlyPaths,\n\t\tauthPath: options.authPath,\n\t\ttoolBinaries: options.toolBinaries,\n\t\tskillPaths: options.skillPaths,\n\t});\n\ttry {\n\t\treturn await supervisor.launch(launch);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : \"Failed to start OS sandbox.\";\n\t\tdependencies.stderr.write(`Error: ${message}\\n`);\n\t\treturn 1;\n\t} finally {\n\t\tfor (const violation of violationStore.list()) {\n\t\t\tdependencies.stderr.write(\n\t\t\t\t`Sandbox violation (${violation.kind}): ${violation.command} — ${violation.detail}\\n`,\n\t\t\t);\n\t\t}\n\t\tawait supervisor.close();\n\t}\n}\n"]}
@@ -39,6 +39,7 @@ export async function launchSandboxedCli(options) {
39
39
  readOnlyPaths: options.readOnlyPaths,
40
40
  authPath: options.authPath,
41
41
  toolBinaries: options.toolBinaries,
42
+ skillPaths: options.skillPaths,
42
43
  });
43
44
  try {
44
45
  return await supervisor.launch(launch);
@@ -1 +1 @@
1
- {"version":3,"file":"cli-supervisor.js","sourceRoot":"","sources":["../../../src/core/sandbox/cli-supervisor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAuB,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAOxD;;6CAE6C;AAC7C,SAAS,2BAA2B,CAAC,OAAkD,EAAkB;IACxG,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAC7E,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;AAAA,CAC1C;AAED,MAAM,mBAAmB,GAA2B;IACnD,aAAa,EAAE,2BAA2B;IAC1C,MAAM,EAAE,OAAO,CAAC,MAAM;CACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAUxC,EAAmB;IACnB,MAAM,YAAY,GAAG,EAAE,GAAG,mBAAmB,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IACzE,MAAM,YAAY,GAAG,mBAAmB,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC/G,IAAI,YAAY,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACrC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,0DAA0D,YAAY,CAAC,MAAM,IAAI,CAAC,CAAC;QAC7G,OAAO,CAAC,CAAC;IACV,CAAC;IACD,MAAM,cAAc,GAAG,IAAI,qBAAqB,EAAE,CAAC;IACnD,MAAM,OAAO,GAAG,YAAY,CAAC,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC;IAC/D,MAAM,UAAU,GAAG,uBAAuB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IACrF,MAAM,MAAM,GAAG,uBAAuB,CAAC;QACtC,SAAS,EAAE,YAAY,CAAC,MAAM,CAAC,SAAS;QACxC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;KAClC,CAAC,CAAC;IACH,IAAI,CAAC;QACJ,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC;QACvF,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,OAAO,IAAI,CAAC,CAAC;QACjD,OAAO,CAAC,CAAC;IACV,CAAC;YAAS,CAAC;QACV,KAAK,MAAM,SAAS,IAAI,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/C,YAAY,CAAC,MAAM,CAAC,KAAK,CACxB,sBAAsB,SAAS,CAAC,IAAI,MAAM,SAAS,CAAC,OAAO,QAAM,SAAS,CAAC,MAAM,IAAI,CACrF,CAAC;QACH,CAAC;QACD,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;AAAA,CACD","sourcesContent":["import type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { buildSandboxedCliLaunch } from \"./cli-launch.ts\";\nimport { createLinuxSandboxBackend } from \"./linux-backend.ts\";\nimport { createMacosSandboxBackend } from \"./macos-backend.ts\";\nimport { createSandboxPolicy } from \"./policy.ts\";\nimport { createSandboxSupervisor, type SandboxBackend } from \"./supervisor.ts\";\nimport { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface CliSandboxDependencies {\n\tcreateBackend: (options: { violationStore: SandboxViolationStore }) => SandboxBackend;\n\tstderr: { write(message: string): boolean };\n}\n\n/** Routes to the platform adapter for the running OS; each adapter self-reports\n * `unavailable` on any other platform, so this only needs to pick the one whose\n * enforcement is actually reachable here. */\nfunction createDefaultSandboxBackend(options: { violationStore: SandboxViolationStore }): SandboxBackend {\n\tif (process.platform === \"darwin\") return createMacosSandboxBackend(options);\n\treturn createLinuxSandboxBackend(options);\n}\n\nconst defaultDependencies: CliSandboxDependencies = {\n\tcreateBackend: createDefaultSandboxBackend,\n\tstderr: process.stderr,\n};\n\n/**\n * Start a normal CLI runtime beneath the whole-process boundary. The caller returns\n * the result directly so the outer process never continues into `main()` afterward.\n */\nexport async function launchSandboxedCli(options: {\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n\tdependencies?: Partial<CliSandboxDependencies>;\n}): Promise<number> {\n\tconst dependencies = { ...defaultDependencies, ...options.dependencies };\n\tconst policyResult = createSandboxPolicy({ workspace: options.workspace, allowedHosts: options.allowedHosts });\n\tif (policyResult.kind === \"invalid\") {\n\t\tdependencies.stderr.write(`Error: OS sandbox is not enforcing this agent session: ${policyResult.reason}\\n`);\n\t\treturn 1;\n\t}\n\tconst violationStore = new SandboxViolationStore();\n\tconst backend = dependencies.createBackend({ violationStore });\n\tconst supervisor = createSandboxSupervisor({ backend, policy: policyResult.policy });\n\tconst launch = buildSandboxedCliLaunch({\n\t\tworkspace: policyResult.policy.workspace,\n\t\tcommand: options.command,\n\t\targs: options.args,\n\t\tenvironment: options.environment,\n\t\tallowedHosts: options.allowedHosts,\n\t\treadOnlyPaths: options.readOnlyPaths,\n\t\tauthPath: options.authPath,\n\t\ttoolBinaries: options.toolBinaries,\n\t});\n\ttry {\n\t\treturn await supervisor.launch(launch);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : \"Failed to start OS sandbox.\";\n\t\tdependencies.stderr.write(`Error: ${message}\\n`);\n\t\treturn 1;\n\t} finally {\n\t\tfor (const violation of violationStore.list()) {\n\t\t\tdependencies.stderr.write(\n\t\t\t\t`Sandbox violation (${violation.kind}): ${violation.command} — ${violation.detail}\\n`,\n\t\t\t);\n\t\t}\n\t\tawait supervisor.close();\n\t}\n}\n"]}
1
+ {"version":3,"file":"cli-supervisor.js","sourceRoot":"","sources":["../../../src/core/sandbox/cli-supervisor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAuB,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAuB,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAOxD;;6CAE6C;AAC7C,SAAS,2BAA2B,CAAC,OAAkD,EAAkB;IACxG,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAC7E,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;AAAA,CAC1C;AAED,MAAM,mBAAmB,GAA2B;IACnD,aAAa,EAAE,2BAA2B;IAC1C,MAAM,EAAE,OAAO,CAAC,MAAM;CACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAWxC,EAAmB;IACnB,MAAM,YAAY,GAAG,EAAE,GAAG,mBAAmB,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IACzE,MAAM,YAAY,GAAG,mBAAmB,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC/G,IAAI,YAAY,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACrC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,0DAA0D,YAAY,CAAC,MAAM,IAAI,CAAC,CAAC;QAC7G,OAAO,CAAC,CAAC;IACV,CAAC;IACD,MAAM,cAAc,GAAG,IAAI,qBAAqB,EAAE,CAAC;IACnD,MAAM,OAAO,GAAG,YAAY,CAAC,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC;IAC/D,MAAM,UAAU,GAAG,uBAAuB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IACrF,MAAM,MAAM,GAAG,uBAAuB,CAAC;QACtC,SAAS,EAAE,YAAY,CAAC,MAAM,CAAC,SAAS;QACxC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,UAAU,EAAE,OAAO,CAAC,UAAU;KAC9B,CAAC,CAAC;IACH,IAAI,CAAC;QACJ,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC;QACvF,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,OAAO,IAAI,CAAC,CAAC;QACjD,OAAO,CAAC,CAAC;IACV,CAAC;YAAS,CAAC;QACV,KAAK,MAAM,SAAS,IAAI,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/C,YAAY,CAAC,MAAM,CAAC,KAAK,CACxB,sBAAsB,SAAS,CAAC,IAAI,MAAM,SAAS,CAAC,OAAO,QAAM,SAAS,CAAC,MAAM,IAAI,CACrF,CAAC;QACH,CAAC;QACD,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;AAAA,CACD","sourcesContent":["import type { HostToolBinary } from \"../../utils/tools-manager.ts\";\nimport { buildSandboxedCliLaunch, type HostSkillPaths } from \"./cli-launch.ts\";\nimport { createLinuxSandboxBackend } from \"./linux-backend.ts\";\nimport { createMacosSandboxBackend } from \"./macos-backend.ts\";\nimport { createSandboxPolicy } from \"./policy.ts\";\nimport { createSandboxSupervisor, type SandboxBackend } from \"./supervisor.ts\";\nimport { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface CliSandboxDependencies {\n\tcreateBackend: (options: { violationStore: SandboxViolationStore }) => SandboxBackend;\n\tstderr: { write(message: string): boolean };\n}\n\n/** Routes to the platform adapter for the running OS; each adapter self-reports\n * `unavailable` on any other platform, so this only needs to pick the one whose\n * enforcement is actually reachable here. */\nfunction createDefaultSandboxBackend(options: { violationStore: SandboxViolationStore }): SandboxBackend {\n\tif (process.platform === \"darwin\") return createMacosSandboxBackend(options);\n\treturn createLinuxSandboxBackend(options);\n}\n\nconst defaultDependencies: CliSandboxDependencies = {\n\tcreateBackend: createDefaultSandboxBackend,\n\tstderr: process.stderr,\n};\n\n/**\n * Start a normal CLI runtime beneath the whole-process boundary. The caller returns\n * the result directly so the outer process never continues into `main()` afterward.\n */\nexport async function launchSandboxedCli(options: {\n\tcommand: string;\n\targs: readonly string[];\n\tenvironment: NodeJS.ProcessEnv;\n\tworkspace: string;\n\tallowedHosts?: readonly string[];\n\treadOnlyPaths?: readonly string[];\n\tauthPath?: string;\n\ttoolBinaries?: readonly HostToolBinary[];\n\tskillPaths?: HostSkillPaths;\n\tdependencies?: Partial<CliSandboxDependencies>;\n}): Promise<number> {\n\tconst dependencies = { ...defaultDependencies, ...options.dependencies };\n\tconst policyResult = createSandboxPolicy({ workspace: options.workspace, allowedHosts: options.allowedHosts });\n\tif (policyResult.kind === \"invalid\") {\n\t\tdependencies.stderr.write(`Error: OS sandbox is not enforcing this agent session: ${policyResult.reason}\\n`);\n\t\treturn 1;\n\t}\n\tconst violationStore = new SandboxViolationStore();\n\tconst backend = dependencies.createBackend({ violationStore });\n\tconst supervisor = createSandboxSupervisor({ backend, policy: policyResult.policy });\n\tconst launch = buildSandboxedCliLaunch({\n\t\tworkspace: policyResult.policy.workspace,\n\t\tcommand: options.command,\n\t\targs: options.args,\n\t\tenvironment: options.environment,\n\t\tallowedHosts: options.allowedHosts,\n\t\treadOnlyPaths: options.readOnlyPaths,\n\t\tauthPath: options.authPath,\n\t\ttoolBinaries: options.toolBinaries,\n\t\tskillPaths: options.skillPaths,\n\t});\n\ttry {\n\t\treturn await supervisor.launch(launch);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : \"Failed to start OS sandbox.\";\n\t\tdependencies.stderr.write(`Error: ${message}\\n`);\n\t\treturn 1;\n\t} finally {\n\t\tfor (const violation of violationStore.list()) {\n\t\t\tdependencies.stderr.write(\n\t\t\t\t`Sandbox violation (${violation.kind}): ${violation.command} — ${violation.detail}\\n`,\n\t\t\t);\n\t\t}\n\t\tawait supervisor.close();\n\t}\n}\n"]}
@@ -34,14 +34,26 @@ export interface LoadSkillsFromDirOptions {
34
34
  */
35
35
  export declare function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult;
36
36
  /**
37
- * Format skills for inclusion in a system prompt.
38
- * Uses XML format per Agent Skills standard.
39
- * See: https://agentskills.io/integrate-skills
40
- *
41
- * Skills with disableModelInvocation=true are excluded from the prompt
42
- * (they can only be invoked explicitly via /skill:name commands).
37
+ * Derive a `/skill:<token>` command token from a skill's frontmatter `name`, which
38
+ * `docs/skills.md` § Validation deliberately allows to contain characters a slash
39
+ * command cannot (spaces, capitals) -- lenient loading is a considered divergence
40
+ * from the Agent Skills standard, not a gap to close, so the skill still loads and
41
+ * only its command token changes here. Identity for an already command-safe name
42
+ * (`^[a-z0-9-]+$` with no leading/trailing/consecutive hyphen, matching
43
+ * `validateName`'s own rule), so this is a no-op for the common case.
44
+ */
45
+ export declare function slugifySkillCommandName(name: string): string;
46
+ /**
47
+ * Initial catalog budget (ADR 0021). Measured against a real 115-skill library: name
48
+ * and full description cost 6,742 tokens against 128 tokens of prefix headroom, so the
49
+ * catalog carries names only and stops at this budget rather than growing with the
50
+ * user's library size. SKILL.8 re-measures this against the enforced production
51
+ * prefix and finalizes it -- matching `ENFORCED_PRODUCTION_PREFIX_BUDGET`'s own
52
+ * precedent of "fixed by measurement, not assumed" -- so this starting value is not
53
+ * final.
43
54
  */
44
- export declare function formatSkillsForPrompt(skills: Skill[]): string;
55
+ export declare const SKILL_CATALOG_PREFIX_BUDGET_TOKENS = 600;
56
+ export declare function formatSkillsForPrompt(skills: Skill[], budgetTokens?: number): string;
45
57
  export interface LoadSkillsOptions {
46
58
  /** Working directory for project-local skills. */
47
59
  cwd: string;