apex-code 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +7 -8
  3. package/dist/cli/agent-lifecycle.d.ts +24 -0
  4. package/dist/cli/agent-lifecycle.d.ts.map +1 -0
  5. package/dist/cli/agent-lifecycle.js +126 -0
  6. package/dist/cli/agent-lifecycle.js.map +1 -0
  7. package/dist/cli/args.d.ts +13 -12
  8. package/dist/cli/args.d.ts.map +1 -1
  9. package/dist/cli/args.js +80 -30
  10. package/dist/cli/args.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +4 -130
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +1 -1
  17. package/dist/config.js.map +1 -1
  18. package/dist/core/agent-session-services.d.ts.map +1 -1
  19. package/dist/core/agent-session-services.js +0 -8
  20. package/dist/core/agent-session-services.js.map +1 -1
  21. package/dist/core/agent-session.d.ts +160 -0
  22. package/dist/core/agent-session.d.ts.map +1 -1
  23. package/dist/core/agent-session.js +265 -0
  24. package/dist/core/agent-session.js.map +1 -1
  25. package/dist/core/delegation/agents.d.ts.map +1 -1
  26. package/dist/core/delegation/agents.js +2 -1
  27. package/dist/core/delegation/agents.js.map +1 -1
  28. package/dist/core/delegation/runtime.d.ts +674 -1
  29. package/dist/core/delegation/runtime.d.ts.map +1 -1
  30. package/dist/core/delegation/runtime.js +1338 -66
  31. package/dist/core/delegation/runtime.js.map +1 -1
  32. package/dist/core/extensions/loader.d.ts.map +1 -1
  33. package/dist/core/extensions/loader.js +3 -4
  34. package/dist/core/extensions/loader.js.map +1 -1
  35. package/dist/core/extensions/source-runtime.d.ts +4 -5
  36. package/dist/core/extensions/source-runtime.d.ts.map +1 -1
  37. package/dist/core/extensions/source-runtime.js +4 -5
  38. package/dist/core/extensions/source-runtime.js.map +1 -1
  39. package/dist/core/formatter-lifecycle.d.ts +2 -1
  40. package/dist/core/formatter-lifecycle.d.ts.map +1 -1
  41. package/dist/core/formatter-lifecycle.js +2 -1
  42. package/dist/core/formatter-lifecycle.js.map +1 -1
  43. package/dist/core/hooks/types.d.ts +2 -3
  44. package/dist/core/hooks/types.d.ts.map +1 -1
  45. package/dist/core/hooks/types.js +2 -3
  46. package/dist/core/hooks/types.js.map +1 -1
  47. package/dist/core/http-idle-timeout.d.ts +3 -5
  48. package/dist/core/http-idle-timeout.d.ts.map +1 -1
  49. package/dist/core/http-idle-timeout.js +3 -5
  50. package/dist/core/http-idle-timeout.js.map +1 -1
  51. package/dist/core/mcp/config.d.ts +18 -1
  52. package/dist/core/mcp/config.d.ts.map +1 -1
  53. package/dist/core/mcp/config.js +24 -7
  54. package/dist/core/mcp/config.js.map +1 -1
  55. package/dist/core/mcp/connector.d.ts +2 -3
  56. package/dist/core/mcp/connector.d.ts.map +1 -1
  57. package/dist/core/mcp/connector.js.map +1 -1
  58. package/dist/core/mcp/oauth/authorize.d.ts +2 -0
  59. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
  60. package/dist/core/mcp/oauth/authorize.js +5 -2
  61. package/dist/core/mcp/oauth/authorize.js.map +1 -1
  62. package/dist/core/mcp/oauth/mcp-token.d.ts +4 -6
  63. package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -1
  64. package/dist/core/mcp/oauth/mcp-token.js +5 -7
  65. package/dist/core/mcp/oauth/mcp-token.js.map +1 -1
  66. package/dist/core/mcp/runtime.d.ts +1 -0
  67. package/dist/core/mcp/runtime.d.ts.map +1 -1
  68. package/dist/core/mcp/runtime.js +2 -1
  69. package/dist/core/mcp/runtime.js.map +1 -1
  70. package/dist/core/package-manager.d.ts.map +1 -1
  71. package/dist/core/package-manager.js +0 -15
  72. package/dist/core/package-manager.js.map +1 -1
  73. package/dist/core/permissions/store.d.ts +6 -11
  74. package/dist/core/permissions/store.d.ts.map +1 -1
  75. package/dist/core/permissions/store.js +23 -51
  76. package/dist/core/permissions/store.js.map +1 -1
  77. package/dist/core/project-resources.d.ts +62 -0
  78. package/dist/core/project-resources.d.ts.map +1 -0
  79. package/dist/core/project-resources.js +42 -0
  80. package/dist/core/project-resources.js.map +1 -0
  81. package/dist/core/sdk.d.ts +62 -12
  82. package/dist/core/sdk.d.ts.map +1 -1
  83. package/dist/core/sdk.js +313 -44
  84. package/dist/core/sdk.js.map +1 -1
  85. package/dist/core/session-share.d.ts +6 -8
  86. package/dist/core/session-share.d.ts.map +1 -1
  87. package/dist/core/session-share.js +9 -12
  88. package/dist/core/session-share.js.map +1 -1
  89. package/dist/core/settings-manager.d.ts +0 -21
  90. package/dist/core/settings-manager.d.ts.map +1 -1
  91. package/dist/core/settings-manager.js +4 -12
  92. package/dist/core/settings-manager.js.map +1 -1
  93. package/dist/core/tools/bash.d.ts.map +1 -1
  94. package/dist/core/tools/bash.js +0 -47
  95. package/dist/core/tools/bash.js.map +1 -1
  96. package/dist/core/tools/delegate.d.ts +8 -0
  97. package/dist/core/tools/delegate.d.ts.map +1 -1
  98. package/dist/core/tools/delegate.js +33 -3
  99. package/dist/core/tools/delegate.js.map +1 -1
  100. package/dist/core/tools/web-fetch.d.ts.map +1 -1
  101. package/dist/core/tools/web-fetch.js +4 -8
  102. package/dist/core/tools/web-fetch.js.map +1 -1
  103. package/dist/core/tools/web-search-exa.d.ts +3 -6
  104. package/dist/core/tools/web-search-exa.d.ts.map +1 -1
  105. package/dist/core/tools/web-search-exa.js +5 -8
  106. package/dist/core/tools/web-search-exa.js.map +1 -1
  107. package/dist/core/trust-manager.d.ts +6 -5
  108. package/dist/core/trust-manager.d.ts.map +1 -1
  109. package/dist/core/trust-manager.js +42 -19
  110. package/dist/core/trust-manager.js.map +1 -1
  111. package/dist/core/web-search-provider.d.ts +0 -10
  112. package/dist/core/web-search-provider.d.ts.map +1 -1
  113. package/dist/core/web-search-provider.js +2 -16
  114. package/dist/core/web-search-provider.js.map +1 -1
  115. package/dist/core/workspace/git-observer.d.ts +16 -0
  116. package/dist/core/workspace/git-observer.d.ts.map +1 -1
  117. package/dist/core/workspace/git-observer.js +8 -1
  118. package/dist/core/workspace/git-observer.js.map +1 -1
  119. package/dist/core/workspace/git-worktree-owner.d.ts +89 -0
  120. package/dist/core/workspace/git-worktree-owner.d.ts.map +1 -0
  121. package/dist/core/workspace/git-worktree-owner.js +240 -0
  122. package/dist/core/workspace/git-worktree-owner.js.map +1 -0
  123. package/dist/main.d.ts +0 -1
  124. package/dist/main.d.ts.map +1 -1
  125. package/dist/main.js +22 -3
  126. package/dist/main.js.map +1 -1
  127. package/dist/modes/acp/server.d.ts +43 -1
  128. package/dist/modes/acp/server.d.ts.map +1 -1
  129. package/dist/modes/acp/server.js +78 -0
  130. package/dist/modes/acp/server.js.map +1 -1
  131. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  132. package/dist/modes/interactive/components/settings-selector.js +1 -1
  133. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  134. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  135. package/dist/modes/interactive/interactive-mode.js +0 -29
  136. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  137. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  138. package/dist/modes/rpc/rpc-mode.js +38 -0
  139. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  140. package/dist/modes/rpc/rpc-types.d.ts +110 -0
  141. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  142. package/dist/modes/rpc/rpc-types.js.map +1 -1
  143. package/dist/utils/tools-manager.d.ts +0 -25
  144. package/dist/utils/tools-manager.d.ts.map +1 -1
  145. package/dist/utils/tools-manager.js +3 -52
  146. package/dist/utils/tools-manager.js.map +1 -1
  147. package/dist/utils/version-check.d.ts +0 -2
  148. package/dist/utils/version-check.d.ts.map +1 -1
  149. package/dist/utils/version-check.js +0 -2
  150. package/dist/utils/version-check.js.map +1 -1
  151. package/docs/settings.md +2 -7
  152. package/npm-shrinkwrap.json +5 -5
  153. package/package.json +2 -2
  154. package/dist/core/sandbox/bwrap-arguments.d.ts +0 -41
  155. package/dist/core/sandbox/bwrap-arguments.d.ts.map +0 -1
  156. package/dist/core/sandbox/bwrap-arguments.js +0 -136
  157. package/dist/core/sandbox/bwrap-arguments.js.map +0 -1
  158. package/dist/core/sandbox/child-entry.d.ts +0 -2
  159. package/dist/core/sandbox/child-entry.d.ts.map +0 -1
  160. package/dist/core/sandbox/child-entry.js +0 -24
  161. package/dist/core/sandbox/child-entry.js.map +0 -1
  162. package/dist/core/sandbox/cli-launch.d.ts +0 -162
  163. package/dist/core/sandbox/cli-launch.d.ts.map +0 -1
  164. package/dist/core/sandbox/cli-launch.js +0 -366
  165. package/dist/core/sandbox/cli-launch.js.map +0 -1
  166. package/dist/core/sandbox/cli-supervisor.d.ts +0 -31
  167. package/dist/core/sandbox/cli-supervisor.d.ts.map +0 -1
  168. package/dist/core/sandbox/cli-supervisor.js +0 -147
  169. package/dist/core/sandbox/cli-supervisor.js.map +0 -1
  170. package/dist/core/sandbox/default-hosts.d.ts +0 -28
  171. package/dist/core/sandbox/default-hosts.d.ts.map +0 -1
  172. package/dist/core/sandbox/default-hosts.js +0 -69
  173. package/dist/core/sandbox/default-hosts.js.map +0 -1
  174. package/dist/core/sandbox/full-access.d.ts +0 -28
  175. package/dist/core/sandbox/full-access.d.ts.map +0 -1
  176. package/dist/core/sandbox/full-access.js +0 -63
  177. package/dist/core/sandbox/full-access.js.map +0 -1
  178. package/dist/core/sandbox/git-identity.d.ts +0 -57
  179. package/dist/core/sandbox/git-identity.d.ts.map +0 -1
  180. package/dist/core/sandbox/git-identity.js +0 -78
  181. package/dist/core/sandbox/git-identity.js.map +0 -1
  182. package/dist/core/sandbox/host-approval.d.ts +0 -42
  183. package/dist/core/sandbox/host-approval.d.ts.map +0 -1
  184. package/dist/core/sandbox/host-approval.js +0 -99
  185. package/dist/core/sandbox/host-approval.js.map +0 -1
  186. package/dist/core/sandbox/linux-backend.d.ts +0 -48
  187. package/dist/core/sandbox/linux-backend.d.ts.map +0 -1
  188. package/dist/core/sandbox/linux-backend.js +0 -328
  189. package/dist/core/sandbox/linux-backend.js.map +0 -1
  190. package/dist/core/sandbox/macos-backend.d.ts +0 -30
  191. package/dist/core/sandbox/macos-backend.d.ts.map +0 -1
  192. package/dist/core/sandbox/macos-backend.js +0 -385
  193. package/dist/core/sandbox/macos-backend.js.map +0 -1
  194. package/dist/core/sandbox/network-proxy.d.ts +0 -38
  195. package/dist/core/sandbox/network-proxy.d.ts.map +0 -1
  196. package/dist/core/sandbox/network-proxy.js +0 -153
  197. package/dist/core/sandbox/network-proxy.js.map +0 -1
  198. package/dist/core/sandbox/network-refusal.d.ts +0 -24
  199. package/dist/core/sandbox/network-refusal.d.ts.map +0 -1
  200. package/dist/core/sandbox/network-refusal.js +0 -72
  201. package/dist/core/sandbox/network-refusal.js.map +0 -1
  202. package/dist/core/sandbox/policy.d.ts +0 -40
  203. package/dist/core/sandbox/policy.d.ts.map +0 -1
  204. package/dist/core/sandbox/policy.js +0 -55
  205. package/dist/core/sandbox/policy.js.map +0 -1
  206. package/dist/core/sandbox/profiles.d.ts +0 -25
  207. package/dist/core/sandbox/profiles.d.ts.map +0 -1
  208. package/dist/core/sandbox/profiles.js +0 -23
  209. package/dist/core/sandbox/profiles.js.map +0 -1
  210. package/dist/core/sandbox/rpc/command-client.d.ts +0 -28
  211. package/dist/core/sandbox/rpc/command-client.d.ts.map +0 -1
  212. package/dist/core/sandbox/rpc/command-client.js +0 -86
  213. package/dist/core/sandbox/rpc/command-client.js.map +0 -1
  214. package/dist/core/sandbox/rpc/command-proxy.d.ts +0 -48
  215. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +0 -1
  216. package/dist/core/sandbox/rpc/command-proxy.js +0 -120
  217. package/dist/core/sandbox/rpc/command-proxy.js.map +0 -1
  218. package/dist/core/sandbox/rpc/credential-client.d.ts +0 -20
  219. package/dist/core/sandbox/rpc/credential-client.d.ts.map +0 -1
  220. package/dist/core/sandbox/rpc/credential-client.js +0 -170
  221. package/dist/core/sandbox/rpc/credential-client.js.map +0 -1
  222. package/dist/core/sandbox/rpc/credential-proxy.d.ts +0 -44
  223. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +0 -1
  224. package/dist/core/sandbox/rpc/credential-proxy.js +0 -287
  225. package/dist/core/sandbox/rpc/credential-proxy.js.map +0 -1
  226. package/dist/core/sandbox/rpc/framing.d.ts +0 -35
  227. package/dist/core/sandbox/rpc/framing.d.ts.map +0 -1
  228. package/dist/core/sandbox/rpc/framing.js +0 -114
  229. package/dist/core/sandbox/rpc/framing.js.map +0 -1
  230. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +0 -47
  231. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +0 -1
  232. package/dist/core/sandbox/rpc/git-credential-helper.js +0 -222
  233. package/dist/core/sandbox/rpc/git-credential-helper.js.map +0 -1
  234. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +0 -66
  235. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +0 -1
  236. package/dist/core/sandbox/rpc/git-credential-proxy.js +0 -209
  237. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +0 -1
  238. package/dist/core/sandbox/supervisor-temp.d.ts +0 -18
  239. package/dist/core/sandbox/supervisor-temp.d.ts.map +0 -1
  240. package/dist/core/sandbox/supervisor-temp.js +0 -21
  241. package/dist/core/sandbox/supervisor-temp.js.map +0 -1
  242. package/dist/core/sandbox/supervisor.d.ts +0 -62
  243. package/dist/core/sandbox/supervisor.d.ts.map +0 -1
  244. package/dist/core/sandbox/supervisor.js +0 -32
  245. package/dist/core/sandbox/supervisor.js.map +0 -1
  246. package/dist/core/sandbox/terminal-handoff.d.ts +0 -68
  247. package/dist/core/sandbox/terminal-handoff.d.ts.map +0 -1
  248. package/dist/core/sandbox/terminal-handoff.js +0 -221
  249. package/dist/core/sandbox/terminal-handoff.js.map +0 -1
  250. package/dist/core/sandbox/terminal-size.d.ts +0 -53
  251. package/dist/core/sandbox/terminal-size.d.ts.map +0 -1
  252. package/dist/core/sandbox/terminal-size.js +0 -126
  253. package/dist/core/sandbox/terminal-size.js.map +0 -1
  254. package/dist/core/sandbox/violations.d.ts +0 -23
  255. package/dist/core/sandbox/violations.d.ts.map +0 -1
  256. package/dist/core/sandbox/violations.js +0 -30
  257. package/dist/core/sandbox/violations.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"delegate.d.ts","sourceRoot":"","sources":["../../../src/core/tools/delegate.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAEzE,OAAO,EAAE,KAAK,kBAAkB,EAAkC,MAAM,eAAe,CAAC;AAgBxF,QAAA,MAAM,cAAc;;;;;;;IAUnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,wBAAwB,GAC/B,kBAAkB,CAAC,OAAO,cAAc,EAAE,eAAe,CAAC,CAiD5D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;uBAEnE","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport { minimatch } from \"minimatch\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { DelegationRuntimeOptions } from \"../delegation/runtime.ts\";\nimport { retrieveDelegationResult, runDelegation } from \"../delegation/runtime.ts\";\nimport { type ApexToolDefinition, type EvidenceRecord, toolUnion } from \"./contract.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst delegateSchemaProperties = {\n\tagentType: Type.String({\n\t\tdescription: \"The subagent type. Required both to launch a task and to retrieve a background result.\",\n\t}),\n\ttask: Type.String({ description: \"The task to delegate. Supply it to launch a delegation.\" }),\n\tbackground: Type.Optional(\n\t\tType.Boolean({ description: \"Return a handle immediately instead of waiting for the child.\" }),\n\t),\n\thandle: Type.String({\n\t\tdescription: \"A background delegation handle returned by an earlier call. Supply it to retrieve the result.\",\n\t}),\n};\n\nconst delegateSchema = toolUnion(\n\t[\n\t\tType.Object({\n\t\t\tagentType: Type.String(),\n\t\t\ttask: Type.String(),\n\t\t\tbackground: Type.Optional(Type.Boolean()),\n\t\t}),\n\t\tType.Object({ agentType: Type.String(), handle: Type.String() }),\n\t],\n\tdelegateSchemaProperties,\n);\n\nexport type DelegateInput = Static<typeof delegateSchema>;\n\nexport interface DelegateDetails {\n\tagentType: string;\n\ttask: string;\n\toutput: string;\n\thandle?: string;\n}\n\nfunction formatDelegateCall(input: DelegateInput, theme: Theme): string {\n\tif (\"handle\" in input) {\n\t\treturn `${theme.fg(\"toolTitle\", theme.bold(\"delegate\"))} ${theme.fg(\"accent\", input.agentType)} · retrieve`;\n\t}\n\tconst task = input.task.replace(/\\s+/g, \" \").trim();\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"delegate\"))} ${theme.fg(\"accent\", input.agentType)} · ${theme.fg(\"toolOutput\", task)}`;\n}\n\nfunction formatDelegateResult(details: DelegateDetails, expanded: boolean, theme: Theme): string {\n\tif (expanded) return theme.fg(\"toolOutput\", details.output);\n\tconst lineCount = details.output ? details.output.split(\"\\n\").length : 0;\n\treturn `${theme.fg(\"accent\", details.agentType)} · ${lineCount} ${lineCount === 1 ? \"line\" : \"lines\"}`;\n}\n\n/**\n * The delegation entry point (task 4.6's contract, task 5.2's execution). Runs a\n * real child through the injected `DelegationRuntimeOptions` -- capability ceiling,\n * derived permission store, and child-session construction all live behind that\n * injection (ADR 0008), matching the `todo_write`/`tool_schema` convention of\n * injecting collaborators rather than reaching for global state. Recursion\n * depth, artifact isolation, and real agent discovery are wired:\n * `../delegation/runtime.ts` bounds depth and roots per-child artifact\n * directories, and `../delegation/agents.ts` supplies production markdown\n * discovery through the injected resolver.\n */\nexport function createDelegateToolDefinition(\n\truntime: DelegationRuntimeOptions,\n): ApexToolDefinition<typeof delegateSchema, DelegateDetails> {\n\treturn {\n\t\tname: \"delegate\",\n\t\tlabel: \"delegate\",\n\t\tdescription: \"Delegate a task to a subagent.\",\n\t\tparameters: delegateSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"delegate\"]),\n\t\t\tpermission: {\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tmatches: (ruleContent, params) => minimatch(params.agentType, ruleContent),\n\t\t\t\tdescribe: (ruleContent) => `Delegate to agent types matching \"${ruleContent}\"`,\n\t\t\t\truleForCall: (params) => params.agentType,\n\t\t\t},\n\t\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\t\tevidence: {\n\t\t\t\temits: new Set([\"workflow\"]),\n\t\t\t\tcapture: (params): EvidenceRecord[] =>\n\t\t\t\t\t\"task\" in params\n\t\t\t\t\t\t? [{ kind: \"workflow\", agentType: params.agentType, task: params.task }]\n\t\t\t\t\t\t: [{ kind: \"workflow\", agentType: params.agentType, handle: params.handle }],\n\t\t\t},\n\t\t},\n\t\tasync execute(_toolCallId, input: DelegateInput): Promise<AgentToolResult<DelegateDetails>> {\n\t\t\tconst result =\n\t\t\t\t\"task\" in input\n\t\t\t\t\t? await runDelegation(runtime, input.agentType, input.task, { background: input.background })\n\t\t\t\t\t: await retrieveDelegationResult(runtime, input.handle, input.agentType);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: result.output }],\n\t\t\t\tdetails: {\n\t\t\t\t\tagentType: result.agentType,\n\t\t\t\t\ttask: result.task,\n\t\t\t\t\toutput: result.output,\n\t\t\t\t\thandle: result.handleId,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t\trenderCall(input, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDelegateCall(input, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDelegateResult(result.details, options.expanded, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDelegateTool(runtime: DelegationRuntimeOptions) {\n\treturn wrapToolDefinition(createDelegateToolDefinition(runtime));\n}\n"]}
1
+ {"version":3,"file":"delegate.d.ts","sourceRoot":"","sources":["../../../src/core/tools/delegate.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAEzE,OAAO,EAAE,KAAK,kBAAkB,EAAkC,MAAM,eAAe,CAAC;AAsCxF,QAAA,MAAM,cAAc;;;;;;;;;;;IAWnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,wBAAwB,GAC/B,kBAAkB,CAAC,OAAO,cAAc,EAAE,eAAe,CAAC,CAmE5D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;uBAEnE","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport { minimatch } from \"minimatch\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { DelegationRuntimeOptions } from \"../delegation/runtime.ts\";\nimport { retrieveDelegationResult, runDelegation } from \"../delegation/runtime.ts\";\nimport { type ApexToolDefinition, type EvidenceRecord, toolUnion } from \"./contract.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst workspaceSchema = Type.Object({\n\tisolation: Type.Union([Type.Literal(\"shared-read\"), Type.Literal(\"worktree\")], {\n\t\tdescription:\n\t\t\t'Child workspace isolation: \"shared-read\" runs in the parent checkout; \"worktree\" gives the child its own git worktree.',\n\t}),\n\townedPaths: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription:\n\t\t\t\t\"Workspace-relative paths the child owns for writes. Advisory claims; overlapping claims are refused.\",\n\t\t}),\n\t),\n});\n\nconst delegateSchemaProperties = {\n\tagentType: Type.String({\n\t\tdescription: \"The subagent type. Required both to launch a task and to retrieve a background result.\",\n\t}),\n\ttask: Type.String({ description: \"The task to delegate. Supply it to launch a delegation.\" }),\n\tworkspace: Type.Optional(\n\t\tType.Object(\n\t\t\t{\n\t\t\t\tisolation: workspaceSchema.properties.isolation,\n\t\t\t\townedPaths: workspaceSchema.properties.ownedPaths,\n\t\t\t},\n\t\t\t{ description: \"The child's requested workspace authority. Defaults to shared-read with no owned paths.\" },\n\t\t),\n\t),\n\tbackground: Type.Optional(\n\t\tType.Boolean({ description: \"Return a handle immediately instead of waiting for the child.\" }),\n\t),\n\thandle: Type.String({\n\t\tdescription: \"A background delegation handle returned by an earlier call. Supply it to retrieve the result.\",\n\t}),\n};\n\nconst delegateSchema = toolUnion(\n\t[\n\t\tType.Object({\n\t\t\tagentType: Type.String(),\n\t\t\ttask: Type.String(),\n\t\t\tworkspace: Type.Optional(workspaceSchema),\n\t\t\tbackground: Type.Optional(Type.Boolean()),\n\t\t}),\n\t\tType.Object({ agentType: Type.String(), handle: Type.String() }),\n\t],\n\tdelegateSchemaProperties,\n);\n\nexport type DelegateInput = Static<typeof delegateSchema>;\n\nexport interface DelegateDetails {\n\tagentType: string;\n\ttask: string;\n\toutput: string;\n\thandle?: string;\n}\n\nfunction formatDelegateCall(input: DelegateInput, theme: Theme): string {\n\tif (\"handle\" in input) {\n\t\treturn `${theme.fg(\"toolTitle\", theme.bold(\"delegate\"))} ${theme.fg(\"accent\", input.agentType)} · retrieve`;\n\t}\n\tconst task = input.task.replace(/\\s+/g, \" \").trim();\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"delegate\"))} ${theme.fg(\"accent\", input.agentType)} · ${theme.fg(\"toolOutput\", task)}`;\n}\n\nfunction formatDelegateResult(details: DelegateDetails, expanded: boolean, theme: Theme): string {\n\tif (expanded) return theme.fg(\"toolOutput\", details.output);\n\tconst lineCount = details.output ? details.output.split(\"\\n\").length : 0;\n\treturn `${theme.fg(\"accent\", details.agentType)} · ${lineCount} ${lineCount === 1 ? \"line\" : \"lines\"}`;\n}\n\n/**\n * The delegation entry point (task 4.6's contract, task 5.2's execution). Runs a\n * real child through the injected `DelegationRuntimeOptions` -- capability ceiling,\n * derived permission store, and child-session construction all live behind that\n * injection (ADR 0008), matching the `todo_write`/`tool_schema` convention of\n * injecting collaborators rather than reaching for global state. Recursion\n * depth, artifact isolation, and real agent discovery are wired:\n * `../delegation/runtime.ts` bounds depth and roots per-child artifact\n * directories, and `../delegation/agents.ts` supplies production markdown\n * discovery through the injected resolver.\n */\nexport function createDelegateToolDefinition(\n\truntime: DelegationRuntimeOptions,\n): ApexToolDefinition<typeof delegateSchema, DelegateDetails> {\n\treturn {\n\t\tname: \"delegate\",\n\t\tlabel: \"delegate\",\n\t\tdescription: \"Delegate a task to a subagent.\",\n\t\tparameters: delegateSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"delegate\"]),\n\t\t\tpermission: {\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tmatches: (ruleContent, params) => minimatch(params.agentType, ruleContent),\n\t\t\t\tdescribe: (ruleContent) => `Delegate to agent types matching \"${ruleContent}\"`,\n\t\t\t\truleForCall: (params) => params.agentType,\n\t\t\t},\n\t\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\t\tevidence: {\n\t\t\t\temits: new Set([\"workflow\"]),\n\t\t\t\t// capture runs in afterToolCall with the completed result, so a launch\n\t\t\t\t// can carry its handle even though the params could not know it: the\n\t\t\t\t// handle is the child's session id, which names the child's session\n\t\t\t\t// file and artifact dir under the parent's delegations/. This reuses\n\t\t\t\t// the existing WorkflowEvidenceRecord.handle field -- no contract or\n\t\t\t\t// sink shape change (the per-child session linkage is the evidence\n\t\t\t\t// model; see docs/plans/2026-09-09-run-and-child-session-architecture.md).\n\t\t\t\tcapture: (params, result): EvidenceRecord[] =>\n\t\t\t\t\t\"task\" in params\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tkind: \"workflow\",\n\t\t\t\t\t\t\t\t\tagentType: params.agentType,\n\t\t\t\t\t\t\t\t\ttask: params.task,\n\t\t\t\t\t\t\t\t\thandle: result.details.handle,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: [{ kind: \"workflow\", agentType: params.agentType, handle: params.handle }],\n\t\t\t},\n\t\t},\n\t\tasync execute(_toolCallId, input: DelegateInput): Promise<AgentToolResult<DelegateDetails>> {\n\t\t\tconst workspace =\n\t\t\t\t\"task\" in input && input.workspace\n\t\t\t\t\t? { isolation: input.workspace.isolation, ownedPaths: input.workspace.ownedPaths ?? [] }\n\t\t\t\t\t: undefined;\n\t\t\tconst result =\n\t\t\t\t\"task\" in input\n\t\t\t\t\t? await runDelegation(runtime, input.agentType, input.task, { background: input.background, workspace })\n\t\t\t\t\t: await retrieveDelegationResult(runtime, input.handle, input.agentType);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: result.output }],\n\t\t\t\tdetails: {\n\t\t\t\t\tagentType: result.agentType,\n\t\t\t\t\ttask: result.task,\n\t\t\t\t\toutput: result.output,\n\t\t\t\t\thandle: result.handleId,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t\trenderCall(input, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDelegateCall(input, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDelegateResult(result.details, options.expanded, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDelegateTool(runtime: DelegationRuntimeOptions) {\n\treturn wrapToolDefinition(createDelegateToolDefinition(runtime));\n}\n"]}
@@ -4,11 +4,23 @@ import { Type } from "typebox";
4
4
  import { retrieveDelegationResult, runDelegation } from "../delegation/runtime.js";
5
5
  import { toolUnion } from "./contract.js";
6
6
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
7
+ const workspaceSchema = Type.Object({
8
+ isolation: Type.Union([Type.Literal("shared-read"), Type.Literal("worktree")], {
9
+ description: 'Child workspace isolation: "shared-read" runs in the parent checkout; "worktree" gives the child its own git worktree.',
10
+ }),
11
+ ownedPaths: Type.Optional(Type.Array(Type.String(), {
12
+ description: "Workspace-relative paths the child owns for writes. Advisory claims; overlapping claims are refused.",
13
+ })),
14
+ });
7
15
  const delegateSchemaProperties = {
8
16
  agentType: Type.String({
9
17
  description: "The subagent type. Required both to launch a task and to retrieve a background result.",
10
18
  }),
11
19
  task: Type.String({ description: "The task to delegate. Supply it to launch a delegation." }),
20
+ workspace: Type.Optional(Type.Object({
21
+ isolation: workspaceSchema.properties.isolation,
22
+ ownedPaths: workspaceSchema.properties.ownedPaths,
23
+ }, { description: "The child's requested workspace authority. Defaults to shared-read with no owned paths." })),
12
24
  background: Type.Optional(Type.Boolean({ description: "Return a handle immediately instead of waiting for the child." })),
13
25
  handle: Type.String({
14
26
  description: "A background delegation handle returned by an earlier call. Supply it to retrieve the result.",
@@ -18,6 +30,7 @@ const delegateSchema = toolUnion([
18
30
  Type.Object({
19
31
  agentType: Type.String(),
20
32
  task: Type.String(),
33
+ workspace: Type.Optional(workspaceSchema),
21
34
  background: Type.Optional(Type.Boolean()),
22
35
  }),
23
36
  Type.Object({ agentType: Type.String(), handle: Type.String() }),
@@ -63,14 +76,31 @@ export function createDelegateToolDefinition(runtime) {
63
76
  context: { resultRecoverable: false, deferSchema: true },
64
77
  evidence: {
65
78
  emits: new Set(["workflow"]),
66
- capture: (params) => "task" in params
67
- ? [{ kind: "workflow", agentType: params.agentType, task: params.task }]
79
+ // capture runs in afterToolCall with the completed result, so a launch
80
+ // can carry its handle even though the params could not know it: the
81
+ // handle is the child's session id, which names the child's session
82
+ // file and artifact dir under the parent's delegations/. This reuses
83
+ // the existing WorkflowEvidenceRecord.handle field -- no contract or
84
+ // sink shape change (the per-child session linkage is the evidence
85
+ // model; see docs/plans/2026-09-09-run-and-child-session-architecture.md).
86
+ capture: (params, result) => "task" in params
87
+ ? [
88
+ {
89
+ kind: "workflow",
90
+ agentType: params.agentType,
91
+ task: params.task,
92
+ handle: result.details.handle,
93
+ },
94
+ ]
68
95
  : [{ kind: "workflow", agentType: params.agentType, handle: params.handle }],
69
96
  },
70
97
  },
71
98
  async execute(_toolCallId, input) {
99
+ const workspace = "task" in input && input.workspace
100
+ ? { isolation: input.workspace.isolation, ownedPaths: input.workspace.ownedPaths ?? [] }
101
+ : undefined;
72
102
  const result = "task" in input
73
- ? await runDelegation(runtime, input.agentType, input.task, { background: input.background })
103
+ ? await runDelegation(runtime, input.agentType, input.task, { background: input.background, workspace })
74
104
  : await retrieveDelegationResult(runtime, input.handle, input.agentType);
75
105
  return {
76
106
  content: [{ type: "text", text: result.output }],
@@ -1 +1 @@
1
- {"version":3,"file":"delegate.js","sourceRoot":"","sources":["../../../src/core/tools/delegate.ts"],"names":[],"mappings":"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","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport { minimatch } from \"minimatch\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { DelegationRuntimeOptions } from \"../delegation/runtime.ts\";\nimport { retrieveDelegationResult, runDelegation } from \"../delegation/runtime.ts\";\nimport { type ApexToolDefinition, type EvidenceRecord, toolUnion } from \"./contract.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst delegateSchemaProperties = {\n\tagentType: Type.String({\n\t\tdescription: \"The subagent type. Required both to launch a task and to retrieve a background result.\",\n\t}),\n\ttask: Type.String({ description: \"The task to delegate. Supply it to launch a delegation.\" }),\n\tbackground: Type.Optional(\n\t\tType.Boolean({ description: \"Return a handle immediately instead of waiting for the child.\" }),\n\t),\n\thandle: Type.String({\n\t\tdescription: \"A background delegation handle returned by an earlier call. Supply it to retrieve the result.\",\n\t}),\n};\n\nconst delegateSchema = toolUnion(\n\t[\n\t\tType.Object({\n\t\t\tagentType: Type.String(),\n\t\t\ttask: Type.String(),\n\t\t\tbackground: Type.Optional(Type.Boolean()),\n\t\t}),\n\t\tType.Object({ agentType: Type.String(), handle: Type.String() }),\n\t],\n\tdelegateSchemaProperties,\n);\n\nexport type DelegateInput = Static<typeof delegateSchema>;\n\nexport interface DelegateDetails {\n\tagentType: string;\n\ttask: string;\n\toutput: string;\n\thandle?: string;\n}\n\nfunction formatDelegateCall(input: DelegateInput, theme: Theme): string {\n\tif (\"handle\" in input) {\n\t\treturn `${theme.fg(\"toolTitle\", theme.bold(\"delegate\"))} ${theme.fg(\"accent\", input.agentType)} · retrieve`;\n\t}\n\tconst task = input.task.replace(/\\s+/g, \" \").trim();\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"delegate\"))} ${theme.fg(\"accent\", input.agentType)} · ${theme.fg(\"toolOutput\", task)}`;\n}\n\nfunction formatDelegateResult(details: DelegateDetails, expanded: boolean, theme: Theme): string {\n\tif (expanded) return theme.fg(\"toolOutput\", details.output);\n\tconst lineCount = details.output ? details.output.split(\"\\n\").length : 0;\n\treturn `${theme.fg(\"accent\", details.agentType)} · ${lineCount} ${lineCount === 1 ? \"line\" : \"lines\"}`;\n}\n\n/**\n * The delegation entry point (task 4.6's contract, task 5.2's execution). Runs a\n * real child through the injected `DelegationRuntimeOptions` -- capability ceiling,\n * derived permission store, and child-session construction all live behind that\n * injection (ADR 0008), matching the `todo_write`/`tool_schema` convention of\n * injecting collaborators rather than reaching for global state. Recursion\n * depth, artifact isolation, and real agent discovery are wired:\n * `../delegation/runtime.ts` bounds depth and roots per-child artifact\n * directories, and `../delegation/agents.ts` supplies production markdown\n * discovery through the injected resolver.\n */\nexport function createDelegateToolDefinition(\n\truntime: DelegationRuntimeOptions,\n): ApexToolDefinition<typeof delegateSchema, DelegateDetails> {\n\treturn {\n\t\tname: \"delegate\",\n\t\tlabel: \"delegate\",\n\t\tdescription: \"Delegate a task to a subagent.\",\n\t\tparameters: delegateSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"delegate\"]),\n\t\t\tpermission: {\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tmatches: (ruleContent, params) => minimatch(params.agentType, ruleContent),\n\t\t\t\tdescribe: (ruleContent) => `Delegate to agent types matching \"${ruleContent}\"`,\n\t\t\t\truleForCall: (params) => params.agentType,\n\t\t\t},\n\t\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\t\tevidence: {\n\t\t\t\temits: new Set([\"workflow\"]),\n\t\t\t\tcapture: (params): EvidenceRecord[] =>\n\t\t\t\t\t\"task\" in params\n\t\t\t\t\t\t? [{ kind: \"workflow\", agentType: params.agentType, task: params.task }]\n\t\t\t\t\t\t: [{ kind: \"workflow\", agentType: params.agentType, handle: params.handle }],\n\t\t\t},\n\t\t},\n\t\tasync execute(_toolCallId, input: DelegateInput): Promise<AgentToolResult<DelegateDetails>> {\n\t\t\tconst result =\n\t\t\t\t\"task\" in input\n\t\t\t\t\t? await runDelegation(runtime, input.agentType, input.task, { background: input.background })\n\t\t\t\t\t: await retrieveDelegationResult(runtime, input.handle, input.agentType);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: result.output }],\n\t\t\t\tdetails: {\n\t\t\t\t\tagentType: result.agentType,\n\t\t\t\t\ttask: result.task,\n\t\t\t\t\toutput: result.output,\n\t\t\t\t\thandle: result.handleId,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t\trenderCall(input, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDelegateCall(input, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDelegateResult(result.details, options.expanded, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDelegateTool(runtime: DelegationRuntimeOptions) {\n\treturn wrapToolDefinition(createDelegateToolDefinition(runtime));\n}\n"]}
1
+ {"version":3,"file":"delegate.js","sourceRoot":"","sources":["../../../src/core/tools/delegate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAE9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAgD,SAAS,EAAE,MAAM,eAAe,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,CAAC;IACnC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE;QAC9E,WAAW,EACV,wHAAwH;KACzH,CAAC;IACF,UAAU,EAAE,IAAI,CAAC,QAAQ,CACxB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE;QACzB,WAAW,EACV,sGAAsG;KACvG,CAAC,CACF;CACD,CAAC,CAAC;AAEH,MAAM,wBAAwB,GAAG;IAChC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC;QACtB,WAAW,EAAE,wFAAwF;KACrG,CAAC;IACF,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,yDAAyD,EAAE,CAAC;IAC7F,SAAS,EAAE,IAAI,CAAC,QAAQ,CACvB,IAAI,CAAC,MAAM,CACV;QACC,SAAS,EAAE,eAAe,CAAC,UAAU,CAAC,SAAS;QAC/C,UAAU,EAAE,eAAe,CAAC,UAAU,CAAC,UAAU;KACjD,EACD,EAAE,WAAW,EAAE,yFAAyF,EAAE,CAC1G,CACD;IACD,UAAU,EAAE,IAAI,CAAC,QAAQ,CACxB,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC,CAC9F;IACD,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;QACnB,WAAW,EAAE,+FAA+F;KAC5G,CAAC;CACF,CAAC;AAEF,MAAM,cAAc,GAAG,SAAS,CAC/B;IACC,IAAI,CAAC,MAAM,CAAC;QACX,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE;QACxB,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE;QACnB,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC;QACzC,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;KACzC,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;CAChE,EACD,wBAAwB,CACxB,CAAC;AAWF,SAAS,kBAAkB,CAAC,KAAoB,EAAE,KAAY,EAAU;IACvE,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAC;QACvB,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,cAAa,CAAC;IAC7G,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,OAAM,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,CACnI;AAED,SAAS,oBAAoB,CAAC,OAAwB,EAAE,QAAiB,EAAE,KAAY,EAAU;IAChG,IAAI,QAAQ;QAAE,OAAO,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,SAAS,CAAC,OAAM,SAAS,IAAI,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;AAAA,CACvG;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,4BAA4B,CAC3C,OAAiC,EAC4B;IAC7D,OAAO;QACN,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,UAAU;QACjB,WAAW,EAAE,gCAAgC;QAC7C,UAAU,EAAE,cAAc;QAC1B,QAAQ,EAAE;YACT,YAAY,EAAE,IAAI,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;YACnC,UAAU,EAAE;gBACX,eAAe,EAAE,KAAK;gBACtB,OAAO,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC;gBAC1E,QAAQ,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,qCAAqC,WAAW,GAAG;gBAC9E,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;aACzC;YACD,OAAO,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE;YACxD,QAAQ,EAAE;gBACT,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;gBAC5B,uEAAuE;gBACvE,qEAAqE;gBACrE,oEAAoE;gBACpE,qEAAqE;gBACrE,qEAAqE;gBACrE,mEAAmE;gBACnE,2EAA2E;gBAC3E,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAoB,EAAE,CAC7C,MAAM,IAAI,MAAM;oBACf,CAAC,CAAC;wBACA;4BACC,IAAI,EAAE,UAAU;4BAChB,SAAS,EAAE,MAAM,CAAC,SAAS;4BAC3B,IAAI,EAAE,MAAM,CAAC,IAAI;4BACjB,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM;yBAC7B;qBACD;oBACF,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;aAC9E;SACD;QACD,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,KAAoB,EAA6C;YAC3F,MAAM,SAAS,GACd,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,SAAS;gBACjC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,IAAI,EAAE,EAAE;gBACxF,CAAC,CAAC,SAAS,CAAC;YACd,MAAM,MAAM,GACX,MAAM,IAAI,KAAK;gBACd,CAAC,CAAC,MAAM,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,SAAS,EAAE,CAAC;gBACxG,CAAC,CAAC,MAAM,wBAAwB,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;YAC3E,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;gBAChD,OAAO,EAAE;oBACR,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,IAAI,EAAE,MAAM,CAAC,IAAI;oBACjB,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,MAAM,EAAE,MAAM,CAAC,QAAQ;iBACvB;aACD,CAAC;QAAA,CACF;QACD,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE;YACjC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;YAC/C,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;YAC7C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;YAC5E,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAiC,EAAE;IACrE,OAAO,kBAAkB,CAAC,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC;AAAA,CACjE","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport { minimatch } from \"minimatch\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { DelegationRuntimeOptions } from \"../delegation/runtime.ts\";\nimport { retrieveDelegationResult, runDelegation } from \"../delegation/runtime.ts\";\nimport { type ApexToolDefinition, type EvidenceRecord, toolUnion } from \"./contract.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst workspaceSchema = Type.Object({\n\tisolation: Type.Union([Type.Literal(\"shared-read\"), Type.Literal(\"worktree\")], {\n\t\tdescription:\n\t\t\t'Child workspace isolation: \"shared-read\" runs in the parent checkout; \"worktree\" gives the child its own git worktree.',\n\t}),\n\townedPaths: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription:\n\t\t\t\t\"Workspace-relative paths the child owns for writes. Advisory claims; overlapping claims are refused.\",\n\t\t}),\n\t),\n});\n\nconst delegateSchemaProperties = {\n\tagentType: Type.String({\n\t\tdescription: \"The subagent type. Required both to launch a task and to retrieve a background result.\",\n\t}),\n\ttask: Type.String({ description: \"The task to delegate. Supply it to launch a delegation.\" }),\n\tworkspace: Type.Optional(\n\t\tType.Object(\n\t\t\t{\n\t\t\t\tisolation: workspaceSchema.properties.isolation,\n\t\t\t\townedPaths: workspaceSchema.properties.ownedPaths,\n\t\t\t},\n\t\t\t{ description: \"The child's requested workspace authority. Defaults to shared-read with no owned paths.\" },\n\t\t),\n\t),\n\tbackground: Type.Optional(\n\t\tType.Boolean({ description: \"Return a handle immediately instead of waiting for the child.\" }),\n\t),\n\thandle: Type.String({\n\t\tdescription: \"A background delegation handle returned by an earlier call. Supply it to retrieve the result.\",\n\t}),\n};\n\nconst delegateSchema = toolUnion(\n\t[\n\t\tType.Object({\n\t\t\tagentType: Type.String(),\n\t\t\ttask: Type.String(),\n\t\t\tworkspace: Type.Optional(workspaceSchema),\n\t\t\tbackground: Type.Optional(Type.Boolean()),\n\t\t}),\n\t\tType.Object({ agentType: Type.String(), handle: Type.String() }),\n\t],\n\tdelegateSchemaProperties,\n);\n\nexport type DelegateInput = Static<typeof delegateSchema>;\n\nexport interface DelegateDetails {\n\tagentType: string;\n\ttask: string;\n\toutput: string;\n\thandle?: string;\n}\n\nfunction formatDelegateCall(input: DelegateInput, theme: Theme): string {\n\tif (\"handle\" in input) {\n\t\treturn `${theme.fg(\"toolTitle\", theme.bold(\"delegate\"))} ${theme.fg(\"accent\", input.agentType)} · retrieve`;\n\t}\n\tconst task = input.task.replace(/\\s+/g, \" \").trim();\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"delegate\"))} ${theme.fg(\"accent\", input.agentType)} · ${theme.fg(\"toolOutput\", task)}`;\n}\n\nfunction formatDelegateResult(details: DelegateDetails, expanded: boolean, theme: Theme): string {\n\tif (expanded) return theme.fg(\"toolOutput\", details.output);\n\tconst lineCount = details.output ? details.output.split(\"\\n\").length : 0;\n\treturn `${theme.fg(\"accent\", details.agentType)} · ${lineCount} ${lineCount === 1 ? \"line\" : \"lines\"}`;\n}\n\n/**\n * The delegation entry point (task 4.6's contract, task 5.2's execution). Runs a\n * real child through the injected `DelegationRuntimeOptions` -- capability ceiling,\n * derived permission store, and child-session construction all live behind that\n * injection (ADR 0008), matching the `todo_write`/`tool_schema` convention of\n * injecting collaborators rather than reaching for global state. Recursion\n * depth, artifact isolation, and real agent discovery are wired:\n * `../delegation/runtime.ts` bounds depth and roots per-child artifact\n * directories, and `../delegation/agents.ts` supplies production markdown\n * discovery through the injected resolver.\n */\nexport function createDelegateToolDefinition(\n\truntime: DelegationRuntimeOptions,\n): ApexToolDefinition<typeof delegateSchema, DelegateDetails> {\n\treturn {\n\t\tname: \"delegate\",\n\t\tlabel: \"delegate\",\n\t\tdescription: \"Delegate a task to a subagent.\",\n\t\tparameters: delegateSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"delegate\"]),\n\t\t\tpermission: {\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tmatches: (ruleContent, params) => minimatch(params.agentType, ruleContent),\n\t\t\t\tdescribe: (ruleContent) => `Delegate to agent types matching \"${ruleContent}\"`,\n\t\t\t\truleForCall: (params) => params.agentType,\n\t\t\t},\n\t\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\t\tevidence: {\n\t\t\t\temits: new Set([\"workflow\"]),\n\t\t\t\t// capture runs in afterToolCall with the completed result, so a launch\n\t\t\t\t// can carry its handle even though the params could not know it: the\n\t\t\t\t// handle is the child's session id, which names the child's session\n\t\t\t\t// file and artifact dir under the parent's delegations/. This reuses\n\t\t\t\t// the existing WorkflowEvidenceRecord.handle field -- no contract or\n\t\t\t\t// sink shape change (the per-child session linkage is the evidence\n\t\t\t\t// model; see docs/plans/2026-09-09-run-and-child-session-architecture.md).\n\t\t\t\tcapture: (params, result): EvidenceRecord[] =>\n\t\t\t\t\t\"task\" in params\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tkind: \"workflow\",\n\t\t\t\t\t\t\t\t\tagentType: params.agentType,\n\t\t\t\t\t\t\t\t\ttask: params.task,\n\t\t\t\t\t\t\t\t\thandle: result.details.handle,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: [{ kind: \"workflow\", agentType: params.agentType, handle: params.handle }],\n\t\t\t},\n\t\t},\n\t\tasync execute(_toolCallId, input: DelegateInput): Promise<AgentToolResult<DelegateDetails>> {\n\t\t\tconst workspace =\n\t\t\t\t\"task\" in input && input.workspace\n\t\t\t\t\t? { isolation: input.workspace.isolation, ownedPaths: input.workspace.ownedPaths ?? [] }\n\t\t\t\t\t: undefined;\n\t\t\tconst result =\n\t\t\t\t\"task\" in input\n\t\t\t\t\t? await runDelegation(runtime, input.agentType, input.task, { background: input.background, workspace })\n\t\t\t\t\t: await retrieveDelegationResult(runtime, input.handle, input.agentType);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: result.output }],\n\t\t\t\tdetails: {\n\t\t\t\t\tagentType: result.agentType,\n\t\t\t\t\ttask: result.task,\n\t\t\t\t\toutput: result.output,\n\t\t\t\t\thandle: result.handleId,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t\trenderCall(input, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDelegateCall(input, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDelegateResult(result.details, options.expanded, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDelegateTool(runtime: DelegationRuntimeOptions) {\n\treturn wrapToolDefinition(createDelegateToolDefinition(runtime));\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"web-fetch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web-fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAIxD,QAAA,MAAM,cAAc;;EAElB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpF;AAwBD,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,wBAAgB,4BAA4B,CAC3C,OAAO,CAAC,EAAE,mBAAmB,GAC3B,kBAAkB,CAAC,OAAO,cAAc,EAAE,eAAe,CAAC,CAkC5D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,mBAAmB;;qBAE/D","sourcesContent":["import type { AgentToolResult } from \"apex-code-agent-core\";\nimport { minimatch } from \"minimatch\";\nimport { type Static, Type } from \"typebox\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, truncateHead } from \"./truncate.ts\";\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The absolute URL to fetch (including scheme, e.g. https://).\" }),\n});\n\nexport type WebFetchInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchDetails {\n\turl: string;\n\tstatus: number;\n\ttruncated: boolean;\n}\n\nexport interface WebFetchOperations {\n\tfetch(url: string, signal?: AbortSignal): Promise<{ status: number; text: string }>;\n}\n\n/**\n * Runs in the same process as every other tool -- the sandboxed child, not the\n * supervisor. `globalThis.fetch` is undici's global fetch, made proxy-aware by\n * `configureHttpDispatcher` (an `EnvHttpProxyAgent` reading HTTP_PROXY/HTTPS_PROXY)\n * at CLI startup, including in `core/sandbox/child-entry.ts`'s sandboxed launch.\n * That is what routes an allowed host through the sandbox's allowlist proxy and\n * leaves a disallowed one with no route at all -- this tool does not implement or\n * bypass that boundary itself; it only participates in it via the ordinary fetch\n * path, deliberately never opening a raw socket that could sidestep the proxy.\n */\nconst defaultWebFetchOperations: WebFetchOperations = {\n\tasync fetch(url, signal) {\n\t\tconst response = await fetch(url, { signal });\n\t\treturn { status: response.status, text: await response.text() };\n\t},\n};\n\nfunction hostAndPath(url: string): string {\n\tconst parsed = new URL(url);\n\treturn `${parsed.hostname}${parsed.pathname}`;\n}\n\nexport interface WebFetchToolOptions {\n\toperations?: WebFetchOperations;\n}\n\nexport function createWebFetchToolDefinition(\n\toptions?: WebFetchToolOptions,\n): ApexToolDefinition<typeof webFetchSchema, WebFetchDetails> {\n\tconst ops = options?.operations ?? defaultWebFetchOperations;\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: \"Fetch the text contents of a URL over HTTP(S).\",\n\t\tparameters: webFetchSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"net\"]),\n\t\t\tpermission: {\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tmatches: (ruleContent, params) => minimatch(hostAndPath(params.url), ruleContent, { dot: true }),\n\t\t\t\tdescribe: (ruleContent) => `Fetch URLs matching \"${ruleContent}\"`,\n\t\t\t\truleForCall: (params) => hostAndPath(params.url),\n\t\t\t},\n\t\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ url }: WebFetchInput,\n\t\t\tsignal?: AbortSignal,\n\t\t): Promise<AgentToolResult<WebFetchDetails>> {\n\t\t\tconst { status, text } = await ops.fetch(url, signal);\n\t\t\tconst truncation = truncateHead(text, { maxBytes: DEFAULT_MAX_BYTES });\n\t\t\tconst suffix = truncation.truncated\n\t\t\t\t? `\\n\\n[truncated: showing ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}]`\n\t\t\t\t: \"\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: `${truncation.content}${suffix}` }],\n\t\t\t\tdetails: { url, status, truncated: truncation.truncated },\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(options?: WebFetchToolOptions) {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(options));\n}\n"]}
1
+ {"version":3,"file":"web-fetch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web-fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAIxD,QAAA,MAAM,cAAc;;EAElB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpF;AAoBD,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,wBAAgB,4BAA4B,CAC3C,OAAO,CAAC,EAAE,mBAAmB,GAC3B,kBAAkB,CAAC,OAAO,cAAc,EAAE,eAAe,CAAC,CAkC5D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,mBAAmB;;qBAE/D","sourcesContent":["import type { AgentToolResult } from \"apex-code-agent-core\";\nimport { minimatch } from \"minimatch\";\nimport { type Static, Type } from \"typebox\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, truncateHead } from \"./truncate.ts\";\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The absolute URL to fetch (including scheme, e.g. https://).\" }),\n});\n\nexport type WebFetchInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchDetails {\n\turl: string;\n\tstatus: number;\n\ttruncated: boolean;\n}\n\nexport interface WebFetchOperations {\n\tfetch(url: string, signal?: AbortSignal): Promise<{ status: number; text: string }>;\n}\n\n/**\n * Runs in the same process as every other tool. `globalThis.fetch` is undici's global\n * fetch, made proxy-aware by `configureHttpDispatcher` (an `EnvHttpProxyAgent` reading\n * HTTP_PROXY/HTTPS_PROXY) at CLI startup. The tool deliberately never opens a raw\n * socket, so any transport policy the environment imposes still applies to it.\n */\nconst defaultWebFetchOperations: WebFetchOperations = {\n\tasync fetch(url, signal) {\n\t\tconst response = await fetch(url, { signal });\n\t\treturn { status: response.status, text: await response.text() };\n\t},\n};\n\nfunction hostAndPath(url: string): string {\n\tconst parsed = new URL(url);\n\treturn `${parsed.hostname}${parsed.pathname}`;\n}\n\nexport interface WebFetchToolOptions {\n\toperations?: WebFetchOperations;\n}\n\nexport function createWebFetchToolDefinition(\n\toptions?: WebFetchToolOptions,\n): ApexToolDefinition<typeof webFetchSchema, WebFetchDetails> {\n\tconst ops = options?.operations ?? defaultWebFetchOperations;\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: \"Fetch the text contents of a URL over HTTP(S).\",\n\t\tparameters: webFetchSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"net\"]),\n\t\t\tpermission: {\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tmatches: (ruleContent, params) => minimatch(hostAndPath(params.url), ruleContent, { dot: true }),\n\t\t\t\tdescribe: (ruleContent) => `Fetch URLs matching \"${ruleContent}\"`,\n\t\t\t\truleForCall: (params) => hostAndPath(params.url),\n\t\t\t},\n\t\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ url }: WebFetchInput,\n\t\t\tsignal?: AbortSignal,\n\t\t): Promise<AgentToolResult<WebFetchDetails>> {\n\t\t\tconst { status, text } = await ops.fetch(url, signal);\n\t\t\tconst truncation = truncateHead(text, { maxBytes: DEFAULT_MAX_BYTES });\n\t\t\tconst suffix = truncation.truncated\n\t\t\t\t? `\\n\\n[truncated: showing ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}]`\n\t\t\t\t: \"\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: `${truncation.content}${suffix}` }],\n\t\t\t\tdetails: { url, status, truncated: truncation.truncated },\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(options?: WebFetchToolOptions) {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(options));\n}\n"]}
@@ -6,14 +6,10 @@ const webFetchSchema = Type.Object({
6
6
  url: Type.String({ description: "The absolute URL to fetch (including scheme, e.g. https://)." }),
7
7
  });
8
8
  /**
9
- * Runs in the same process as every other tool -- the sandboxed child, not the
10
- * supervisor. `globalThis.fetch` is undici's global fetch, made proxy-aware by
11
- * `configureHttpDispatcher` (an `EnvHttpProxyAgent` reading HTTP_PROXY/HTTPS_PROXY)
12
- * at CLI startup, including in `core/sandbox/child-entry.ts`'s sandboxed launch.
13
- * That is what routes an allowed host through the sandbox's allowlist proxy and
14
- * leaves a disallowed one with no route at all -- this tool does not implement or
15
- * bypass that boundary itself; it only participates in it via the ordinary fetch
16
- * path, deliberately never opening a raw socket that could sidestep the proxy.
9
+ * Runs in the same process as every other tool. `globalThis.fetch` is undici's global
10
+ * fetch, made proxy-aware by `configureHttpDispatcher` (an `EnvHttpProxyAgent` reading
11
+ * HTTP_PROXY/HTTPS_PROXY) at CLI startup. The tool deliberately never opens a raw
12
+ * socket, so any transport policy the environment imposes still applies to it.
17
13
  */
18
14
  const defaultWebFetchOperations = {
19
15
  async fetch(url, signal) {
@@ -1 +1 @@
1
- {"version":3,"file":"web-fetch.js","sourceRoot":"","sources":["../../../src/core/tools/web-fetch.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentToolResult } from \"apex-code-agent-core\";\nimport { minimatch } from \"minimatch\";\nimport { type Static, Type } from \"typebox\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, truncateHead } from \"./truncate.ts\";\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The absolute URL to fetch (including scheme, e.g. https://).\" }),\n});\n\nexport type WebFetchInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchDetails {\n\turl: string;\n\tstatus: number;\n\ttruncated: boolean;\n}\n\nexport interface WebFetchOperations {\n\tfetch(url: string, signal?: AbortSignal): Promise<{ status: number; text: string }>;\n}\n\n/**\n * Runs in the same process as every other tool -- the sandboxed child, not the\n * supervisor. `globalThis.fetch` is undici's global fetch, made proxy-aware by\n * `configureHttpDispatcher` (an `EnvHttpProxyAgent` reading HTTP_PROXY/HTTPS_PROXY)\n * at CLI startup, including in `core/sandbox/child-entry.ts`'s sandboxed launch.\n * That is what routes an allowed host through the sandbox's allowlist proxy and\n * leaves a disallowed one with no route at all -- this tool does not implement or\n * bypass that boundary itself; it only participates in it via the ordinary fetch\n * path, deliberately never opening a raw socket that could sidestep the proxy.\n */\nconst defaultWebFetchOperations: WebFetchOperations = {\n\tasync fetch(url, signal) {\n\t\tconst response = await fetch(url, { signal });\n\t\treturn { status: response.status, text: await response.text() };\n\t},\n};\n\nfunction hostAndPath(url: string): string {\n\tconst parsed = new URL(url);\n\treturn `${parsed.hostname}${parsed.pathname}`;\n}\n\nexport interface WebFetchToolOptions {\n\toperations?: WebFetchOperations;\n}\n\nexport function createWebFetchToolDefinition(\n\toptions?: WebFetchToolOptions,\n): ApexToolDefinition<typeof webFetchSchema, WebFetchDetails> {\n\tconst ops = options?.operations ?? defaultWebFetchOperations;\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: \"Fetch the text contents of a URL over HTTP(S).\",\n\t\tparameters: webFetchSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"net\"]),\n\t\t\tpermission: {\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tmatches: (ruleContent, params) => minimatch(hostAndPath(params.url), ruleContent, { dot: true }),\n\t\t\t\tdescribe: (ruleContent) => `Fetch URLs matching \"${ruleContent}\"`,\n\t\t\t\truleForCall: (params) => hostAndPath(params.url),\n\t\t\t},\n\t\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ url }: WebFetchInput,\n\t\t\tsignal?: AbortSignal,\n\t\t): Promise<AgentToolResult<WebFetchDetails>> {\n\t\t\tconst { status, text } = await ops.fetch(url, signal);\n\t\t\tconst truncation = truncateHead(text, { maxBytes: DEFAULT_MAX_BYTES });\n\t\t\tconst suffix = truncation.truncated\n\t\t\t\t? `\\n\\n[truncated: showing ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}]`\n\t\t\t\t: \"\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: `${truncation.content}${suffix}` }],\n\t\t\t\tdetails: { url, status, truncated: truncation.truncated },\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(options?: WebFetchToolOptions) {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(options));\n}\n"]}
1
+ {"version":3,"file":"web-fetch.js","sourceRoot":"","sources":["../../../src/core/tools/web-fetch.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentToolResult } from \"apex-code-agent-core\";\nimport { minimatch } from \"minimatch\";\nimport { type Static, Type } from \"typebox\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, truncateHead } from \"./truncate.ts\";\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The absolute URL to fetch (including scheme, e.g. https://).\" }),\n});\n\nexport type WebFetchInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchDetails {\n\turl: string;\n\tstatus: number;\n\ttruncated: boolean;\n}\n\nexport interface WebFetchOperations {\n\tfetch(url: string, signal?: AbortSignal): Promise<{ status: number; text: string }>;\n}\n\n/**\n * Runs in the same process as every other tool. `globalThis.fetch` is undici's global\n * fetch, made proxy-aware by `configureHttpDispatcher` (an `EnvHttpProxyAgent` reading\n * HTTP_PROXY/HTTPS_PROXY) at CLI startup. The tool deliberately never opens a raw\n * socket, so any transport policy the environment imposes still applies to it.\n */\nconst defaultWebFetchOperations: WebFetchOperations = {\n\tasync fetch(url, signal) {\n\t\tconst response = await fetch(url, { signal });\n\t\treturn { status: response.status, text: await response.text() };\n\t},\n};\n\nfunction hostAndPath(url: string): string {\n\tconst parsed = new URL(url);\n\treturn `${parsed.hostname}${parsed.pathname}`;\n}\n\nexport interface WebFetchToolOptions {\n\toperations?: WebFetchOperations;\n}\n\nexport function createWebFetchToolDefinition(\n\toptions?: WebFetchToolOptions,\n): ApexToolDefinition<typeof webFetchSchema, WebFetchDetails> {\n\tconst ops = options?.operations ?? defaultWebFetchOperations;\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: \"Fetch the text contents of a URL over HTTP(S).\",\n\t\tparameters: webFetchSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"net\"]),\n\t\t\tpermission: {\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tmatches: (ruleContent, params) => minimatch(hostAndPath(params.url), ruleContent, { dot: true }),\n\t\t\t\tdescribe: (ruleContent) => `Fetch URLs matching \"${ruleContent}\"`,\n\t\t\t\truleForCall: (params) => hostAndPath(params.url),\n\t\t\t},\n\t\t\tcontext: { resultRecoverable: false, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ url }: WebFetchInput,\n\t\t\tsignal?: AbortSignal,\n\t\t): Promise<AgentToolResult<WebFetchDetails>> {\n\t\t\tconst { status, text } = await ops.fetch(url, signal);\n\t\t\tconst truncation = truncateHead(text, { maxBytes: DEFAULT_MAX_BYTES });\n\t\t\tconst suffix = truncation.truncated\n\t\t\t\t? `\\n\\n[truncated: showing ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}]`\n\t\t\t\t: \"\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: `${truncation.content}${suffix}` }],\n\t\t\t\tdetails: { url, status, truncated: truncation.truncated },\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(options?: WebFetchToolOptions) {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(options));\n}\n"]}
@@ -1,8 +1,6 @@
1
1
  import type { WebSearchOperations } from "./web-search.ts";
2
2
  /** Exa's hosted search endpoint. */
3
3
  export declare const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
4
- /** Host of {@link EXA_SEARCH_ENDPOINT}, for the sandbox network allowlist. */
5
- export declare const EXA_SEARCH_HOST = "api.exa.ai";
6
4
  /** Exa's own default is 10; matching it keeps an unconfigured session predictable. */
7
5
  export declare const DEFAULT_EXA_RESULT_COUNT = 10;
8
6
  /**
@@ -24,10 +22,9 @@ export interface ExaWebSearchOptions {
24
22
  * Exa-backed `web_search` backend.
25
23
  *
26
24
  * Reaches the network through `globalThis.fetch` for the same reason `web_fetch`
27
- * does: that is the dispatcher `configureHttpDispatcher` made proxy-aware, so an
28
- * allowlisted host routes through the sandbox proxy and a disallowed one has no
29
- * route at all. Opening a raw socket here would sidestep a boundary this tool is
30
- * supposed to sit behind.
25
+ * does: that is the dispatcher `configureHttpDispatcher` made proxy-aware, so any
26
+ * transport policy the environment imposes still applies. Opening a raw socket here
27
+ * would sidestep it.
31
28
  */
32
29
  export declare function createExaWebSearchOperations(options: ExaWebSearchOptions): WebSearchOperations;
33
30
  //# sourceMappingURL=web-search-exa.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"web-search-exa.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web-search-exa.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,iBAAiB,CAAC;AAE5E,oCAAoC;AACpC,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,8EAA8E;AAC9E,eAAO,MAAM,eAAe,eAAe,CAAC;AAE5C,sFAAsF;AACtF,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,MAAM,WAAW,mBAAmB;IACnC,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAwED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,mBAAmB,CAgD9F","sourcesContent":["import type { WebSearchOperations, WebSearchResult } from \"./web-search.ts\";\n\n/** Exa's hosted search endpoint. */\nexport const EXA_SEARCH_ENDPOINT = \"https://api.exa.ai/search\";\n\n/** Host of {@link EXA_SEARCH_ENDPOINT}, for the sandbox network allowlist. */\nexport const EXA_SEARCH_HOST = \"api.exa.ai\";\n\n/** Exa's own default is 10; matching it keeps an unconfigured session predictable. */\nexport const DEFAULT_EXA_RESULT_COUNT = 10;\n\n/**\n * Snippet budget per result. Exa returns whole page text when asked, which is the\n * wrong unit for a search result -- ten full pages would dwarf every other tool\n * result in the context window. 800 characters is roughly a paragraph, enough for\n * the model to judge relevance and decide whether to spend a `web_fetch` on it.\n */\nexport const DEFAULT_EXA_SNIPPET_CHARACTERS = 800;\n\nexport interface ExaWebSearchOptions {\n\t/** Resolved key, never a `$VAR` reference -- resolution happens before this call. */\n\tapiKey: string;\n\t/** Override for an Exa-compatible endpoint. Defaults to {@link EXA_SEARCH_ENDPOINT}. */\n\tendpoint?: string;\n\tnumResults?: number;\n\tsnippetMaxCharacters?: number;\n}\n\n/** Separator between highlights, which are non-contiguous passages rather than prose. */\nconst HIGHLIGHT_SEPARATOR = \" … \";\n\n/** The subset of Exa's `/search` response this adapter reads. */\ninterface ExaSearchResult {\n\ttitle?: string | null;\n\turl?: string | null;\n\ttext?: string | null;\n\thighlights?: string[] | null;\n}\n\ninterface ExaSearchResponse {\n\tresults?: ExaSearchResult[] | null;\n}\n\nfunction isAbortError(error: unknown): boolean {\n\treturn error instanceof Error && error.name === \"AbortError\";\n}\n\nfunction hostOf(endpoint: string): string {\n\ttry {\n\t\treturn new URL(endpoint).host;\n\t} catch {\n\t\treturn endpoint;\n\t}\n}\n\n/**\n * Text arrives with the source document's line structure intact. Collapsing it to a\n * single line costs nothing in meaning and keeps a ten-result list readable as a list\n * rather than ten wrapped blocks.\n */\nfunction toSnippet(text: string | null | undefined, maxCharacters: number): string {\n\tif (!text) return \"\";\n\tconst collapsed = text.replace(/\\s+/g, \" \").trim();\n\tif (collapsed.length <= maxCharacters) return collapsed;\n\tconst capped = collapsed.slice(0, maxCharacters);\n\t// End on a whole word when one fits: a snippet cut mid-word (\"specificatio\")\n\t// reads as corruption rather than truncation. A cap that lands inside the first\n\t// word has no boundary to retreat to, so the hard cut stands.\n\tconst boundary = capped.lastIndexOf(\" \");\n\treturn boundary > 0 ? capped.slice(0, boundary) : capped;\n}\n\n/**\n * Prefer highlights, fall back to page text.\n *\n * `contents.text` returns the top of the document, which on a real page is navigation\n * chrome and a repeat of the title before any content. Highlights are the passages Exa\n * judged relevant to the query, so they are both shorter and more useful. Text remains\n * the fallback because a page can yield no highlights, and an empty snippet is worse\n * than a boilerplate one.\n */\nfunction snippetFor(result: ExaSearchResult, maxCharacters: number): string {\n\tconst highlights = (result.highlights ?? []).map((highlight) => highlight?.trim()).filter(Boolean);\n\tif (highlights.length > 0) return toSnippet(highlights.join(HIGHLIGHT_SEPARATOR), maxCharacters);\n\treturn toSnippet(result.text, maxCharacters);\n}\n\nfunction toWebSearchResults(payload: ExaSearchResponse, maxCharacters: number): WebSearchResult[] {\n\tconst results: WebSearchResult[] = [];\n\tfor (const result of payload.results ?? []) {\n\t\tconst url = result?.url?.trim();\n\t\t// A result the model cannot cite or fetch is worse than one fewer result.\n\t\tif (!url) continue;\n\t\tresults.push({ title: result.title?.trim() || url, url, snippet: snippetFor(result, maxCharacters) });\n\t}\n\treturn results;\n}\n\n/**\n * Exa-backed `web_search` backend.\n *\n * Reaches the network through `globalThis.fetch` for the same reason `web_fetch`\n * does: that is the dispatcher `configureHttpDispatcher` made proxy-aware, so an\n * allowlisted host routes through the sandbox proxy and a disallowed one has no\n * route at all. Opening a raw socket here would sidestep a boundary this tool is\n * supposed to sit behind.\n */\nexport function createExaWebSearchOperations(options: ExaWebSearchOptions): WebSearchOperations {\n\tconst apiKey = options.apiKey?.trim();\n\tif (!apiKey) {\n\t\tthrow new Error(\"Exa web search requires an API key; none was resolved.\");\n\t}\n\tconst endpoint = options.endpoint ?? EXA_SEARCH_ENDPOINT;\n\tconst host = hostOf(endpoint);\n\tconst numResults = options.numResults ?? DEFAULT_EXA_RESULT_COUNT;\n\tconst snippetMaxCharacters = options.snippetMaxCharacters ?? DEFAULT_EXA_SNIPPET_CHARACTERS;\n\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tlet response: Response;\n\t\t\ttry {\n\t\t\t\tresponse = await fetch(endpoint, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"content-type\": \"application/json\", \"x-api-key\": apiKey },\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\tquery,\n\t\t\t\t\t\tnumResults,\n\t\t\t\t\t\tcontents: {\n\t\t\t\t\t\t\t// `query` steers which passages Exa picks. The deprecated\n\t\t\t\t\t\t\t// `numSentences` and `highlightsPerUrl` options are deliberately\n\t\t\t\t\t\t\t// unused; the latter is documented as ignored.\n\t\t\t\t\t\t\thighlights: { query, maxCharacters: snippetMaxCharacters },\n\t\t\t\t\t\t\ttext: { maxCharacters: snippetMaxCharacters },\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t\tsignal,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tif (isAbortError(error)) throw error;\n\t\t\t\t// `fetch failed` alone names neither the host nor the remedy. The sandbox\n\t\t\t\t// refusal wrapper's message, when it produced one, is preserved verbatim here.\n\t\t\t\tconst detail = error instanceof Error ? error.message : String(error);\n\t\t\t\tthrow new Error(`web_search request to ${host} failed: ${detail}`, { cause: error });\n\t\t\t}\n\n\t\t\tif (!response.ok) {\n\t\t\t\t// The body can echo request fields, so only the status is reported -- a\n\t\t\t\t// pasted-through body is how an API key ends up in a transcript.\n\t\t\t\tthrow new Error(`web_search request to ${host} failed with HTTP ${response.status}.`);\n\t\t\t}\n\n\t\t\tconst payload = (await response.json()) as ExaSearchResponse;\n\t\t\treturn toWebSearchResults(payload, snippetMaxCharacters);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"web-search-exa.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web-search-exa.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,iBAAiB,CAAC;AAE5E,oCAAoC;AACpC,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,sFAAsF;AACtF,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,MAAM,WAAW,mBAAmB;IACnC,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAwED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,mBAAmB,CAgD9F","sourcesContent":["import type { WebSearchOperations, WebSearchResult } from \"./web-search.ts\";\n\n/** Exa's hosted search endpoint. */\nexport const EXA_SEARCH_ENDPOINT = \"https://api.exa.ai/search\";\n\n/** Exa's own default is 10; matching it keeps an unconfigured session predictable. */\nexport const DEFAULT_EXA_RESULT_COUNT = 10;\n\n/**\n * Snippet budget per result. Exa returns whole page text when asked, which is the\n * wrong unit for a search result -- ten full pages would dwarf every other tool\n * result in the context window. 800 characters is roughly a paragraph, enough for\n * the model to judge relevance and decide whether to spend a `web_fetch` on it.\n */\nexport const DEFAULT_EXA_SNIPPET_CHARACTERS = 800;\n\nexport interface ExaWebSearchOptions {\n\t/** Resolved key, never a `$VAR` reference -- resolution happens before this call. */\n\tapiKey: string;\n\t/** Override for an Exa-compatible endpoint. Defaults to {@link EXA_SEARCH_ENDPOINT}. */\n\tendpoint?: string;\n\tnumResults?: number;\n\tsnippetMaxCharacters?: number;\n}\n\n/** Separator between highlights, which are non-contiguous passages rather than prose. */\nconst HIGHLIGHT_SEPARATOR = \" … \";\n\n/** The subset of Exa's `/search` response this adapter reads. */\ninterface ExaSearchResult {\n\ttitle?: string | null;\n\turl?: string | null;\n\ttext?: string | null;\n\thighlights?: string[] | null;\n}\n\ninterface ExaSearchResponse {\n\tresults?: ExaSearchResult[] | null;\n}\n\nfunction isAbortError(error: unknown): boolean {\n\treturn error instanceof Error && error.name === \"AbortError\";\n}\n\nfunction hostOf(endpoint: string): string {\n\ttry {\n\t\treturn new URL(endpoint).host;\n\t} catch {\n\t\treturn endpoint;\n\t}\n}\n\n/**\n * Text arrives with the source document's line structure intact. Collapsing it to a\n * single line costs nothing in meaning and keeps a ten-result list readable as a list\n * rather than ten wrapped blocks.\n */\nfunction toSnippet(text: string | null | undefined, maxCharacters: number): string {\n\tif (!text) return \"\";\n\tconst collapsed = text.replace(/\\s+/g, \" \").trim();\n\tif (collapsed.length <= maxCharacters) return collapsed;\n\tconst capped = collapsed.slice(0, maxCharacters);\n\t// End on a whole word when one fits: a snippet cut mid-word (\"specificatio\")\n\t// reads as corruption rather than truncation. A cap that lands inside the first\n\t// word has no boundary to retreat to, so the hard cut stands.\n\tconst boundary = capped.lastIndexOf(\" \");\n\treturn boundary > 0 ? capped.slice(0, boundary) : capped;\n}\n\n/**\n * Prefer highlights, fall back to page text.\n *\n * `contents.text` returns the top of the document, which on a real page is navigation\n * chrome and a repeat of the title before any content. Highlights are the passages Exa\n * judged relevant to the query, so they are both shorter and more useful. Text remains\n * the fallback because a page can yield no highlights, and an empty snippet is worse\n * than a boilerplate one.\n */\nfunction snippetFor(result: ExaSearchResult, maxCharacters: number): string {\n\tconst highlights = (result.highlights ?? []).map((highlight) => highlight?.trim()).filter(Boolean);\n\tif (highlights.length > 0) return toSnippet(highlights.join(HIGHLIGHT_SEPARATOR), maxCharacters);\n\treturn toSnippet(result.text, maxCharacters);\n}\n\nfunction toWebSearchResults(payload: ExaSearchResponse, maxCharacters: number): WebSearchResult[] {\n\tconst results: WebSearchResult[] = [];\n\tfor (const result of payload.results ?? []) {\n\t\tconst url = result?.url?.trim();\n\t\t// A result the model cannot cite or fetch is worse than one fewer result.\n\t\tif (!url) continue;\n\t\tresults.push({ title: result.title?.trim() || url, url, snippet: snippetFor(result, maxCharacters) });\n\t}\n\treturn results;\n}\n\n/**\n * Exa-backed `web_search` backend.\n *\n * Reaches the network through `globalThis.fetch` for the same reason `web_fetch`\n * does: that is the dispatcher `configureHttpDispatcher` made proxy-aware, so any\n * transport policy the environment imposes still applies. Opening a raw socket here\n * would sidestep it.\n */\nexport function createExaWebSearchOperations(options: ExaWebSearchOptions): WebSearchOperations {\n\tconst apiKey = options.apiKey?.trim();\n\tif (!apiKey) {\n\t\tthrow new Error(\"Exa web search requires an API key; none was resolved.\");\n\t}\n\tconst endpoint = options.endpoint ?? EXA_SEARCH_ENDPOINT;\n\tconst host = hostOf(endpoint);\n\tconst numResults = options.numResults ?? DEFAULT_EXA_RESULT_COUNT;\n\tconst snippetMaxCharacters = options.snippetMaxCharacters ?? DEFAULT_EXA_SNIPPET_CHARACTERS;\n\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tlet response: Response;\n\t\t\ttry {\n\t\t\t\tresponse = await fetch(endpoint, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"content-type\": \"application/json\", \"x-api-key\": apiKey },\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\tquery,\n\t\t\t\t\t\tnumResults,\n\t\t\t\t\t\tcontents: {\n\t\t\t\t\t\t\t// `query` steers which passages Exa picks. The deprecated\n\t\t\t\t\t\t\t// `numSentences` and `highlightsPerUrl` options are deliberately\n\t\t\t\t\t\t\t// unused; the latter is documented as ignored.\n\t\t\t\t\t\t\thighlights: { query, maxCharacters: snippetMaxCharacters },\n\t\t\t\t\t\t\ttext: { maxCharacters: snippetMaxCharacters },\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t\tsignal,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tif (isAbortError(error)) throw error;\n\t\t\t\t// `fetch failed` alone names neither the host nor the remedy, so the\n\t\t\t\t// underlying message is preserved verbatim alongside the host.\n\t\t\t\tconst detail = error instanceof Error ? error.message : String(error);\n\t\t\t\tthrow new Error(`web_search request to ${host} failed: ${detail}`, { cause: error });\n\t\t\t}\n\n\t\t\tif (!response.ok) {\n\t\t\t\t// The body can echo request fields, so only the status is reported -- a\n\t\t\t\t// pasted-through body is how an API key ends up in a transcript.\n\t\t\t\tthrow new Error(`web_search request to ${host} failed with HTTP ${response.status}.`);\n\t\t\t}\n\n\t\t\tconst payload = (await response.json()) as ExaSearchResponse;\n\t\t\treturn toWebSearchResults(payload, snippetMaxCharacters);\n\t\t},\n\t};\n}\n"]}
@@ -1,7 +1,5 @@
1
1
  /** Exa's hosted search endpoint. */
2
2
  export const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
3
- /** Host of {@link EXA_SEARCH_ENDPOINT}, for the sandbox network allowlist. */
4
- export const EXA_SEARCH_HOST = "api.exa.ai";
5
3
  /** Exa's own default is 10; matching it keeps an unconfigured session predictable. */
6
4
  export const DEFAULT_EXA_RESULT_COUNT = 10;
7
5
  /**
@@ -72,10 +70,9 @@ function toWebSearchResults(payload, maxCharacters) {
72
70
  * Exa-backed `web_search` backend.
73
71
  *
74
72
  * Reaches the network through `globalThis.fetch` for the same reason `web_fetch`
75
- * does: that is the dispatcher `configureHttpDispatcher` made proxy-aware, so an
76
- * allowlisted host routes through the sandbox proxy and a disallowed one has no
77
- * route at all. Opening a raw socket here would sidestep a boundary this tool is
78
- * supposed to sit behind.
73
+ * does: that is the dispatcher `configureHttpDispatcher` made proxy-aware, so any
74
+ * transport policy the environment imposes still applies. Opening a raw socket here
75
+ * would sidestep it.
79
76
  */
80
77
  export function createExaWebSearchOperations(options) {
81
78
  const apiKey = options.apiKey?.trim();
@@ -110,8 +107,8 @@ export function createExaWebSearchOperations(options) {
110
107
  catch (error) {
111
108
  if (isAbortError(error))
112
109
  throw error;
113
- // `fetch failed` alone names neither the host nor the remedy. The sandbox
114
- // refusal wrapper's message, when it produced one, is preserved verbatim here.
110
+ // `fetch failed` alone names neither the host nor the remedy, so the
111
+ // underlying message is preserved verbatim alongside the host.
115
112
  const detail = error instanceof Error ? error.message : String(error);
116
113
  throw new Error(`web_search request to ${host} failed: ${detail}`, { cause: error });
117
114
  }
@@ -1 +1 @@
1
- {"version":3,"file":"web-search-exa.js","sourceRoot":"","sources":["../../../src/core/tools/web-search-exa.ts"],"names":[],"mappings":"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","sourcesContent":["import type { WebSearchOperations, WebSearchResult } from \"./web-search.ts\";\n\n/** Exa's hosted search endpoint. */\nexport const EXA_SEARCH_ENDPOINT = \"https://api.exa.ai/search\";\n\n/** Host of {@link EXA_SEARCH_ENDPOINT}, for the sandbox network allowlist. */\nexport const EXA_SEARCH_HOST = \"api.exa.ai\";\n\n/** Exa's own default is 10; matching it keeps an unconfigured session predictable. */\nexport const DEFAULT_EXA_RESULT_COUNT = 10;\n\n/**\n * Snippet budget per result. Exa returns whole page text when asked, which is the\n * wrong unit for a search result -- ten full pages would dwarf every other tool\n * result in the context window. 800 characters is roughly a paragraph, enough for\n * the model to judge relevance and decide whether to spend a `web_fetch` on it.\n */\nexport const DEFAULT_EXA_SNIPPET_CHARACTERS = 800;\n\nexport interface ExaWebSearchOptions {\n\t/** Resolved key, never a `$VAR` reference -- resolution happens before this call. */\n\tapiKey: string;\n\t/** Override for an Exa-compatible endpoint. Defaults to {@link EXA_SEARCH_ENDPOINT}. */\n\tendpoint?: string;\n\tnumResults?: number;\n\tsnippetMaxCharacters?: number;\n}\n\n/** Separator between highlights, which are non-contiguous passages rather than prose. */\nconst HIGHLIGHT_SEPARATOR = \" … \";\n\n/** The subset of Exa's `/search` response this adapter reads. */\ninterface ExaSearchResult {\n\ttitle?: string | null;\n\turl?: string | null;\n\ttext?: string | null;\n\thighlights?: string[] | null;\n}\n\ninterface ExaSearchResponse {\n\tresults?: ExaSearchResult[] | null;\n}\n\nfunction isAbortError(error: unknown): boolean {\n\treturn error instanceof Error && error.name === \"AbortError\";\n}\n\nfunction hostOf(endpoint: string): string {\n\ttry {\n\t\treturn new URL(endpoint).host;\n\t} catch {\n\t\treturn endpoint;\n\t}\n}\n\n/**\n * Text arrives with the source document's line structure intact. Collapsing it to a\n * single line costs nothing in meaning and keeps a ten-result list readable as a list\n * rather than ten wrapped blocks.\n */\nfunction toSnippet(text: string | null | undefined, maxCharacters: number): string {\n\tif (!text) return \"\";\n\tconst collapsed = text.replace(/\\s+/g, \" \").trim();\n\tif (collapsed.length <= maxCharacters) return collapsed;\n\tconst capped = collapsed.slice(0, maxCharacters);\n\t// End on a whole word when one fits: a snippet cut mid-word (\"specificatio\")\n\t// reads as corruption rather than truncation. A cap that lands inside the first\n\t// word has no boundary to retreat to, so the hard cut stands.\n\tconst boundary = capped.lastIndexOf(\" \");\n\treturn boundary > 0 ? capped.slice(0, boundary) : capped;\n}\n\n/**\n * Prefer highlights, fall back to page text.\n *\n * `contents.text` returns the top of the document, which on a real page is navigation\n * chrome and a repeat of the title before any content. Highlights are the passages Exa\n * judged relevant to the query, so they are both shorter and more useful. Text remains\n * the fallback because a page can yield no highlights, and an empty snippet is worse\n * than a boilerplate one.\n */\nfunction snippetFor(result: ExaSearchResult, maxCharacters: number): string {\n\tconst highlights = (result.highlights ?? []).map((highlight) => highlight?.trim()).filter(Boolean);\n\tif (highlights.length > 0) return toSnippet(highlights.join(HIGHLIGHT_SEPARATOR), maxCharacters);\n\treturn toSnippet(result.text, maxCharacters);\n}\n\nfunction toWebSearchResults(payload: ExaSearchResponse, maxCharacters: number): WebSearchResult[] {\n\tconst results: WebSearchResult[] = [];\n\tfor (const result of payload.results ?? []) {\n\t\tconst url = result?.url?.trim();\n\t\t// A result the model cannot cite or fetch is worse than one fewer result.\n\t\tif (!url) continue;\n\t\tresults.push({ title: result.title?.trim() || url, url, snippet: snippetFor(result, maxCharacters) });\n\t}\n\treturn results;\n}\n\n/**\n * Exa-backed `web_search` backend.\n *\n * Reaches the network through `globalThis.fetch` for the same reason `web_fetch`\n * does: that is the dispatcher `configureHttpDispatcher` made proxy-aware, so an\n * allowlisted host routes through the sandbox proxy and a disallowed one has no\n * route at all. Opening a raw socket here would sidestep a boundary this tool is\n * supposed to sit behind.\n */\nexport function createExaWebSearchOperations(options: ExaWebSearchOptions): WebSearchOperations {\n\tconst apiKey = options.apiKey?.trim();\n\tif (!apiKey) {\n\t\tthrow new Error(\"Exa web search requires an API key; none was resolved.\");\n\t}\n\tconst endpoint = options.endpoint ?? EXA_SEARCH_ENDPOINT;\n\tconst host = hostOf(endpoint);\n\tconst numResults = options.numResults ?? DEFAULT_EXA_RESULT_COUNT;\n\tconst snippetMaxCharacters = options.snippetMaxCharacters ?? DEFAULT_EXA_SNIPPET_CHARACTERS;\n\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tlet response: Response;\n\t\t\ttry {\n\t\t\t\tresponse = await fetch(endpoint, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"content-type\": \"application/json\", \"x-api-key\": apiKey },\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\tquery,\n\t\t\t\t\t\tnumResults,\n\t\t\t\t\t\tcontents: {\n\t\t\t\t\t\t\t// `query` steers which passages Exa picks. The deprecated\n\t\t\t\t\t\t\t// `numSentences` and `highlightsPerUrl` options are deliberately\n\t\t\t\t\t\t\t// unused; the latter is documented as ignored.\n\t\t\t\t\t\t\thighlights: { query, maxCharacters: snippetMaxCharacters },\n\t\t\t\t\t\t\ttext: { maxCharacters: snippetMaxCharacters },\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t\tsignal,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tif (isAbortError(error)) throw error;\n\t\t\t\t// `fetch failed` alone names neither the host nor the remedy. The sandbox\n\t\t\t\t// refusal wrapper's message, when it produced one, is preserved verbatim here.\n\t\t\t\tconst detail = error instanceof Error ? error.message : String(error);\n\t\t\t\tthrow new Error(`web_search request to ${host} failed: ${detail}`, { cause: error });\n\t\t\t}\n\n\t\t\tif (!response.ok) {\n\t\t\t\t// The body can echo request fields, so only the status is reported -- a\n\t\t\t\t// pasted-through body is how an API key ends up in a transcript.\n\t\t\t\tthrow new Error(`web_search request to ${host} failed with HTTP ${response.status}.`);\n\t\t\t}\n\n\t\t\tconst payload = (await response.json()) as ExaSearchResponse;\n\t\t\treturn toWebSearchResults(payload, snippetMaxCharacters);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"web-search-exa.js","sourceRoot":"","sources":["../../../src/core/tools/web-search-exa.ts"],"names":[],"mappings":"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","sourcesContent":["import type { WebSearchOperations, WebSearchResult } from \"./web-search.ts\";\n\n/** Exa's hosted search endpoint. */\nexport const EXA_SEARCH_ENDPOINT = \"https://api.exa.ai/search\";\n\n/** Exa's own default is 10; matching it keeps an unconfigured session predictable. */\nexport const DEFAULT_EXA_RESULT_COUNT = 10;\n\n/**\n * Snippet budget per result. Exa returns whole page text when asked, which is the\n * wrong unit for a search result -- ten full pages would dwarf every other tool\n * result in the context window. 800 characters is roughly a paragraph, enough for\n * the model to judge relevance and decide whether to spend a `web_fetch` on it.\n */\nexport const DEFAULT_EXA_SNIPPET_CHARACTERS = 800;\n\nexport interface ExaWebSearchOptions {\n\t/** Resolved key, never a `$VAR` reference -- resolution happens before this call. */\n\tapiKey: string;\n\t/** Override for an Exa-compatible endpoint. Defaults to {@link EXA_SEARCH_ENDPOINT}. */\n\tendpoint?: string;\n\tnumResults?: number;\n\tsnippetMaxCharacters?: number;\n}\n\n/** Separator between highlights, which are non-contiguous passages rather than prose. */\nconst HIGHLIGHT_SEPARATOR = \" … \";\n\n/** The subset of Exa's `/search` response this adapter reads. */\ninterface ExaSearchResult {\n\ttitle?: string | null;\n\turl?: string | null;\n\ttext?: string | null;\n\thighlights?: string[] | null;\n}\n\ninterface ExaSearchResponse {\n\tresults?: ExaSearchResult[] | null;\n}\n\nfunction isAbortError(error: unknown): boolean {\n\treturn error instanceof Error && error.name === \"AbortError\";\n}\n\nfunction hostOf(endpoint: string): string {\n\ttry {\n\t\treturn new URL(endpoint).host;\n\t} catch {\n\t\treturn endpoint;\n\t}\n}\n\n/**\n * Text arrives with the source document's line structure intact. Collapsing it to a\n * single line costs nothing in meaning and keeps a ten-result list readable as a list\n * rather than ten wrapped blocks.\n */\nfunction toSnippet(text: string | null | undefined, maxCharacters: number): string {\n\tif (!text) return \"\";\n\tconst collapsed = text.replace(/\\s+/g, \" \").trim();\n\tif (collapsed.length <= maxCharacters) return collapsed;\n\tconst capped = collapsed.slice(0, maxCharacters);\n\t// End on a whole word when one fits: a snippet cut mid-word (\"specificatio\")\n\t// reads as corruption rather than truncation. A cap that lands inside the first\n\t// word has no boundary to retreat to, so the hard cut stands.\n\tconst boundary = capped.lastIndexOf(\" \");\n\treturn boundary > 0 ? capped.slice(0, boundary) : capped;\n}\n\n/**\n * Prefer highlights, fall back to page text.\n *\n * `contents.text` returns the top of the document, which on a real page is navigation\n * chrome and a repeat of the title before any content. Highlights are the passages Exa\n * judged relevant to the query, so they are both shorter and more useful. Text remains\n * the fallback because a page can yield no highlights, and an empty snippet is worse\n * than a boilerplate one.\n */\nfunction snippetFor(result: ExaSearchResult, maxCharacters: number): string {\n\tconst highlights = (result.highlights ?? []).map((highlight) => highlight?.trim()).filter(Boolean);\n\tif (highlights.length > 0) return toSnippet(highlights.join(HIGHLIGHT_SEPARATOR), maxCharacters);\n\treturn toSnippet(result.text, maxCharacters);\n}\n\nfunction toWebSearchResults(payload: ExaSearchResponse, maxCharacters: number): WebSearchResult[] {\n\tconst results: WebSearchResult[] = [];\n\tfor (const result of payload.results ?? []) {\n\t\tconst url = result?.url?.trim();\n\t\t// A result the model cannot cite or fetch is worse than one fewer result.\n\t\tif (!url) continue;\n\t\tresults.push({ title: result.title?.trim() || url, url, snippet: snippetFor(result, maxCharacters) });\n\t}\n\treturn results;\n}\n\n/**\n * Exa-backed `web_search` backend.\n *\n * Reaches the network through `globalThis.fetch` for the same reason `web_fetch`\n * does: that is the dispatcher `configureHttpDispatcher` made proxy-aware, so any\n * transport policy the environment imposes still applies. Opening a raw socket here\n * would sidestep it.\n */\nexport function createExaWebSearchOperations(options: ExaWebSearchOptions): WebSearchOperations {\n\tconst apiKey = options.apiKey?.trim();\n\tif (!apiKey) {\n\t\tthrow new Error(\"Exa web search requires an API key; none was resolved.\");\n\t}\n\tconst endpoint = options.endpoint ?? EXA_SEARCH_ENDPOINT;\n\tconst host = hostOf(endpoint);\n\tconst numResults = options.numResults ?? DEFAULT_EXA_RESULT_COUNT;\n\tconst snippetMaxCharacters = options.snippetMaxCharacters ?? DEFAULT_EXA_SNIPPET_CHARACTERS;\n\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tlet response: Response;\n\t\t\ttry {\n\t\t\t\tresponse = await fetch(endpoint, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"content-type\": \"application/json\", \"x-api-key\": apiKey },\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\tquery,\n\t\t\t\t\t\tnumResults,\n\t\t\t\t\t\tcontents: {\n\t\t\t\t\t\t\t// `query` steers which passages Exa picks. The deprecated\n\t\t\t\t\t\t\t// `numSentences` and `highlightsPerUrl` options are deliberately\n\t\t\t\t\t\t\t// unused; the latter is documented as ignored.\n\t\t\t\t\t\t\thighlights: { query, maxCharacters: snippetMaxCharacters },\n\t\t\t\t\t\t\ttext: { maxCharacters: snippetMaxCharacters },\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t\tsignal,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tif (isAbortError(error)) throw error;\n\t\t\t\t// `fetch failed` alone names neither the host nor the remedy, so the\n\t\t\t\t// underlying message is preserved verbatim alongside the host.\n\t\t\t\tconst detail = error instanceof Error ? error.message : String(error);\n\t\t\t\tthrow new Error(`web_search request to ${host} failed: ${detail}`, { cause: error });\n\t\t\t}\n\n\t\t\tif (!response.ok) {\n\t\t\t\t// The body can echo request fields, so only the status is reported -- a\n\t\t\t\t// pasted-through body is how an API key ends up in a transcript.\n\t\t\t\tthrow new Error(`web_search request to ${host} failed with HTTP ${response.status}.`);\n\t\t\t}\n\n\t\t\tconst payload = (await response.json()) as ExaSearchResponse;\n\t\t\treturn toWebSearchResults(payload, snippetMaxCharacters);\n\t\t},\n\t};\n}\n"]}
@@ -18,11 +18,12 @@ export declare function getProjectTrustOptions(cwd: string, options?: {
18
18
  includeSessionOnly?: boolean;
19
19
  }): ProjectTrustOption[];
20
20
  /**
21
- * Returns true when cwd has project-local resources that must be gated by
22
- * project trust: trust-requiring entries under cwd/.pi, or .agents/skills in
23
- * cwd or one of its ancestors. Returns false when no such project resources
24
- * exist. The user/global ~/.agents/skills directory is always treated as a
25
- * trusted user resource and is ignored here, even when cwd is $HOME.
21
+ * Returns true when cwd supplies a project-controlled resource that must be gated by
22
+ * project trust. The gated set is `PROJECT_RESOURCES`, which the loaders read too, plus
23
+ * a `.agents/skills` directory in cwd or one of its ancestors. Returns false when no
24
+ * such resource exists, or when the only ones present grant nothing (an empty
25
+ * permissions file). The user's own ~/.agents/skills is a trusted user resource and is
26
+ * ignored here, even when cwd is $HOME.
26
27
  */
27
28
  export declare function hasTrustRequiringProjectResources(cwd: string): boolean;
28
29
  export declare class ProjectTrustStore {
@@ -1 +1 @@
1
- {"version":3,"file":"trust-manager.d.ts","sourceRoot":"","sources":["../../src/core/trust-manager.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,IAAI,CAAC;AAElD,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,oBAAoB,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAkCD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIzE;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,kBAAkB,EAAE,CA8BpH;AAkFD;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAsBtE;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,SAAS,CAAS;IAE1B,YAAY,QAAQ,EAAE,MAAM,EAE3B;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAErC;IAED,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAKnD;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAErD;IAED,OAAO,CAAC,SAAS,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAa7C;CACD","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { CONFIG_DIR_NAME } from \"../config.ts\";\nimport { canonicalizePath, resolvePath } from \"../utils/paths.ts\";\nimport { stripBom } from \"../utils/text.ts\";\n\nexport type ProjectTrustDecision = boolean | null;\n\nexport interface ProjectTrustStoreEntry {\n\tpath: string;\n\tdecision: boolean;\n}\n\nexport interface ProjectTrustUpdate {\n\tpath: string;\n\tdecision: ProjectTrustDecision;\n}\n\nexport interface ProjectTrustOption {\n\tlabel: string;\n\ttrusted: boolean;\n\tupdates: ProjectTrustUpdate[];\n\tsavedPath?: string;\n}\n\ntype TrustFile = Record<string, boolean | null | undefined>;\n\nconst TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [\n\t\"settings.json\",\n\t\"extensions\",\n\t\"skills\",\n\t\"prompts\",\n\t\"themes\",\n\t\"SYSTEM.md\",\n\t\"APPEND_SYSTEM.md\",\n] as const;\n\nfunction normalizeCwd(cwd: string): string {\n\treturn canonicalizePath(resolvePath(cwd));\n}\n\nfunction findNearestTrustEntry(data: TrustFile, cwd: string): ProjectTrustStoreEntry | null {\n\tlet currentDir = normalizeCwd(cwd);\n\twhile (true) {\n\t\tconst value = data[currentDir];\n\t\tif (value === true || value === false) {\n\t\t\treturn { path: currentDir, decision: value };\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\tif (parentDir === currentDir) {\n\t\t\treturn null;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport function getProjectTrustParentPath(cwd: string): string | undefined {\n\tconst trustPath = normalizeCwd(cwd);\n\tconst parentDir = dirname(trustPath);\n\treturn parentDir === trustPath ? undefined : parentDir;\n}\n\nexport function getProjectTrustOptions(cwd: string, options?: { includeSessionOnly?: boolean }): ProjectTrustOption[] {\n\tconst trustPath = normalizeCwd(cwd);\n\tconst trustOptions: ProjectTrustOption[] = [\n\t\t{ label: \"Trust\", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath },\n\t];\n\tconst parentPath = getProjectTrustParentPath(cwd);\n\tif (parentPath !== undefined) {\n\t\ttrustOptions.push({\n\t\t\tlabel: `Trust parent folder (${parentPath})`,\n\t\t\ttrusted: true,\n\t\t\tupdates: [\n\t\t\t\t{ path: parentPath, decision: true },\n\t\t\t\t{ path: trustPath, decision: null },\n\t\t\t],\n\t\t\tsavedPath: parentPath,\n\t\t});\n\t}\n\tif (options?.includeSessionOnly) {\n\t\ttrustOptions.push({ label: \"Trust (this session only)\", trusted: true, updates: [] });\n\t}\n\ttrustOptions.push({\n\t\tlabel: \"Do not trust\",\n\t\ttrusted: false,\n\t\tupdates: [{ path: trustPath, decision: false }],\n\t\tsavedPath: trustPath,\n\t});\n\tif (options?.includeSessionOnly) {\n\t\ttrustOptions.push({ label: \"Do not trust (this session only)\", trusted: false, updates: [] });\n\t}\n\treturn trustOptions;\n}\n\nfunction readTrustFile(path: string): TrustFile {\n\tif (!existsSync(path)) {\n\t\treturn {};\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(stripBom(readFileSync(path, \"utf-8\")));\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthrow new Error(`Failed to read trust store ${path}: ${message}`);\n\t}\n\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new Error(`Invalid trust store ${path}: expected an object`);\n\t}\n\n\tconst data: TrustFile = {};\n\tfor (const [key, value] of Object.entries(parsed)) {\n\t\tif (value !== true && value !== false && value !== null) {\n\t\t\tthrow new Error(`Invalid trust store ${path}: value for ${JSON.stringify(key)} must be true, false, or null`);\n\t\t}\n\t\tdata[key] = value;\n\t}\n\treturn data;\n}\n\nfunction writeTrustFile(path: string, data: TrustFile): void {\n\tconst sorted: TrustFile = {};\n\tfor (const key of Object.keys(data).sort()) {\n\t\tconst value = data[key];\n\t\tif (value === true || value === false || value === null) {\n\t\t\tsorted[key] = value;\n\t\t}\n\t}\n\tmkdirSync(dirname(path), { recursive: true });\n\twriteFileSync(path, `${JSON.stringify(sorted, null, 2)}\\n`, \"utf-8\");\n}\n\nfunction acquireTrustLockSync(path: string): () => void {\n\tconst trustDir = dirname(path);\n\tmkdirSync(trustDir, { recursive: true });\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(trustDir, { realpath: false, lockfilePath: `${path}.lock` });\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t: undefined;\n\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Sleep synchronously to avoid changing trust store callers to async.\n\t\t\t}\n\t\t}\n\t}\n\n\tif (lastError instanceof Error) {\n\t\tthrow lastError;\n\t}\n\tthrow new Error(\"Failed to acquire trust store lock\");\n}\n\nfunction withTrustFileLock<T>(path: string, fn: () => T): T {\n\tconst release = acquireTrustLockSync(path);\n\ttry {\n\t\treturn fn();\n\t} finally {\n\t\trelease();\n\t}\n}\n\n/**\n * Returns true when cwd has project-local resources that must be gated by\n * project trust: trust-requiring entries under cwd/.pi, or .agents/skills in\n * cwd or one of its ancestors. Returns false when no such project resources\n * exist. The user/global ~/.agents/skills directory is always treated as a\n * trusted user resource and is ignored here, even when cwd is $HOME.\n */\nexport function hasTrustRequiringProjectResources(cwd: string): boolean {\n\tconst homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));\n\tconst userAgentsSkillsDir = join(homeDir, \".agents\", \"skills\");\n\tlet currentDir = canonicalizePath(resolvePath(cwd));\n\n\tconst configDir = join(currentDir, CONFIG_DIR_NAME);\n\tif (TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry)))) {\n\t\treturn true;\n\t}\n\n\twhile (true) {\n\t\tconst agentsSkillsDir = join(currentDir, \".agents\", \"skills\");\n\t\tif (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\tif (parentDir === currentDir) {\n\t\t\treturn false;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport class ProjectTrustStore {\n\tprivate trustPath: string;\n\n\tconstructor(agentDir: string) {\n\t\tthis.trustPath = join(resolvePath(agentDir), \"trust.json\");\n\t}\n\n\tget(cwd: string): ProjectTrustDecision {\n\t\treturn this.getEntry(cwd)?.decision ?? null;\n\t}\n\n\tgetEntry(cwd: string): ProjectTrustStoreEntry | null {\n\t\treturn withTrustFileLock(this.trustPath, () => {\n\t\t\tconst data = readTrustFile(this.trustPath);\n\t\t\treturn findNearestTrustEntry(data, cwd);\n\t\t});\n\t}\n\n\tset(cwd: string, decision: ProjectTrustDecision): void {\n\t\tthis.setMany([{ path: cwd, decision }]);\n\t}\n\n\tsetMany(decisions: ProjectTrustUpdate[]): void {\n\t\twithTrustFileLock(this.trustPath, () => {\n\t\t\tconst data = readTrustFile(this.trustPath);\n\t\t\tfor (const { path, decision } of decisions) {\n\t\t\t\tconst key = normalizeCwd(path);\n\t\t\t\tif (decision === null) {\n\t\t\t\t\tdelete data[key];\n\t\t\t\t} else {\n\t\t\t\t\tdata[key] = decision;\n\t\t\t\t}\n\t\t\t}\n\t\t\twriteTrustFile(this.trustPath, data);\n\t\t});\n\t}\n}\n"]}
1
+ {"version":3,"file":"trust-manager.d.ts","sourceRoot":"","sources":["../../src/core/trust-manager.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,IAAI,CAAC;AAElD,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,oBAAoB,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAkDD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIzE;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,kBAAkB,EAAE,CA8BpH;AAkFD;;;;;;;GAOG;AACH,wBAAgB,iCAAiC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAuBtE;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,SAAS,CAAS;IAE1B,YAAY,QAAQ,EAAE,MAAM,EAE3B;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAErC;IAED,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAKnD;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAErD;IAED,OAAO,CAAC,SAAS,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAa7C;CACD","sourcesContent":["import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { canonicalizePath, resolvePath } from \"../utils/paths.ts\";\nimport { stripBom } from \"../utils/text.ts\";\nimport { permissionContentConfersAuthority } from \"./permissions/store.ts\";\nimport { PROJECT_RESOURCES, type ProjectResource, projectResourcePath } from \"./project-resources.ts\";\n\nexport type ProjectTrustDecision = boolean | null;\n\nexport interface ProjectTrustStoreEntry {\n\tpath: string;\n\tdecision: boolean;\n}\n\nexport interface ProjectTrustUpdate {\n\tpath: string;\n\tdecision: ProjectTrustDecision;\n}\n\nexport interface ProjectTrustOption {\n\tlabel: string;\n\ttrusted: boolean;\n\tupdates: ProjectTrustUpdate[];\n\tsavedPath?: string;\n}\n\ntype TrustFile = Record<string, boolean | null | undefined>;\n\nfunction resourceConfersAuthority(resource: ProjectResource, absolutePath: string): boolean {\n\tif (!existsSync(absolutePath)) {\n\t\t// `existsSync` follows links, so a broken link is indistinguishable from absence.\n\t\t// A checkout controls the link, and the loader's own lock path creates the target\n\t\t// on first write, so a dangling link is treated as conferring rather than skipped.\n\t\ttry {\n\t\t\tlstatSync(absolutePath);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\tif (resource.authority === \"presence\") return true;\n\t// An unreadable file is treated the same way unparseable content is, because both\n\t// leave us unable to say the resource grants nothing.\n\ttry {\n\t\t// The checkout controls this path, so it may point at a FIFO or a character device.\n\t\t// A synchronous read of one blocks the process before any prompt is rendered, which\n\t\t// turns a trust check into a denial of service. Anything but a regular file confers.\n\t\tif (!lstatSync(absolutePath).isFile()) return true;\n\t\treturn permissionContentConfersAuthority(stripBom(readFileSync(absolutePath, \"utf8\")));\n\t} catch {\n\t\treturn true;\n\t}\n}\n\nfunction normalizeCwd(cwd: string): string {\n\treturn canonicalizePath(resolvePath(cwd));\n}\n\nfunction findNearestTrustEntry(data: TrustFile, cwd: string): ProjectTrustStoreEntry | null {\n\tlet currentDir = normalizeCwd(cwd);\n\twhile (true) {\n\t\tconst value = data[currentDir];\n\t\tif (value === true || value === false) {\n\t\t\treturn { path: currentDir, decision: value };\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\tif (parentDir === currentDir) {\n\t\t\treturn null;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport function getProjectTrustParentPath(cwd: string): string | undefined {\n\tconst trustPath = normalizeCwd(cwd);\n\tconst parentDir = dirname(trustPath);\n\treturn parentDir === trustPath ? undefined : parentDir;\n}\n\nexport function getProjectTrustOptions(cwd: string, options?: { includeSessionOnly?: boolean }): ProjectTrustOption[] {\n\tconst trustPath = normalizeCwd(cwd);\n\tconst trustOptions: ProjectTrustOption[] = [\n\t\t{ label: \"Trust\", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath },\n\t];\n\tconst parentPath = getProjectTrustParentPath(cwd);\n\tif (parentPath !== undefined) {\n\t\ttrustOptions.push({\n\t\t\tlabel: `Trust parent folder (${parentPath})`,\n\t\t\ttrusted: true,\n\t\t\tupdates: [\n\t\t\t\t{ path: parentPath, decision: true },\n\t\t\t\t{ path: trustPath, decision: null },\n\t\t\t],\n\t\t\tsavedPath: parentPath,\n\t\t});\n\t}\n\tif (options?.includeSessionOnly) {\n\t\ttrustOptions.push({ label: \"Trust (this session only)\", trusted: true, updates: [] });\n\t}\n\ttrustOptions.push({\n\t\tlabel: \"Do not trust\",\n\t\ttrusted: false,\n\t\tupdates: [{ path: trustPath, decision: false }],\n\t\tsavedPath: trustPath,\n\t});\n\tif (options?.includeSessionOnly) {\n\t\ttrustOptions.push({ label: \"Do not trust (this session only)\", trusted: false, updates: [] });\n\t}\n\treturn trustOptions;\n}\n\nfunction readTrustFile(path: string): TrustFile {\n\tif (!existsSync(path)) {\n\t\treturn {};\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(stripBom(readFileSync(path, \"utf-8\")));\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthrow new Error(`Failed to read trust store ${path}: ${message}`);\n\t}\n\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new Error(`Invalid trust store ${path}: expected an object`);\n\t}\n\n\tconst data: TrustFile = {};\n\tfor (const [key, value] of Object.entries(parsed)) {\n\t\tif (value !== true && value !== false && value !== null) {\n\t\t\tthrow new Error(`Invalid trust store ${path}: value for ${JSON.stringify(key)} must be true, false, or null`);\n\t\t}\n\t\tdata[key] = value;\n\t}\n\treturn data;\n}\n\nfunction writeTrustFile(path: string, data: TrustFile): void {\n\tconst sorted: TrustFile = {};\n\tfor (const key of Object.keys(data).sort()) {\n\t\tconst value = data[key];\n\t\tif (value === true || value === false || value === null) {\n\t\t\tsorted[key] = value;\n\t\t}\n\t}\n\tmkdirSync(dirname(path), { recursive: true });\n\twriteFileSync(path, `${JSON.stringify(sorted, null, 2)}\\n`, \"utf-8\");\n}\n\nfunction acquireTrustLockSync(path: string): () => void {\n\tconst trustDir = dirname(path);\n\tmkdirSync(trustDir, { recursive: true });\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(trustDir, { realpath: false, lockfilePath: `${path}.lock` });\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t: undefined;\n\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Sleep synchronously to avoid changing trust store callers to async.\n\t\t\t}\n\t\t}\n\t}\n\n\tif (lastError instanceof Error) {\n\t\tthrow lastError;\n\t}\n\tthrow new Error(\"Failed to acquire trust store lock\");\n}\n\nfunction withTrustFileLock<T>(path: string, fn: () => T): T {\n\tconst release = acquireTrustLockSync(path);\n\ttry {\n\t\treturn fn();\n\t} finally {\n\t\trelease();\n\t}\n}\n\n/**\n * Returns true when cwd supplies a project-controlled resource that must be gated by\n * project trust. The gated set is `PROJECT_RESOURCES`, which the loaders read too, plus\n * a `.agents/skills` directory in cwd or one of its ancestors. Returns false when no\n * such resource exists, or when the only ones present grant nothing (an empty\n * permissions file). The user's own ~/.agents/skills is a trusted user resource and is\n * ignored here, even when cwd is $HOME.\n */\nexport function hasTrustRequiringProjectResources(cwd: string): boolean {\n\tconst homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));\n\tconst userAgentsSkillsDir = join(homeDir, \".agents\", \"skills\");\n\tlet currentDir = canonicalizePath(resolvePath(cwd));\n\n\tfor (const resource of PROJECT_RESOURCES) {\n\t\tif (resourceConfersAuthority(resource, projectResourcePath(currentDir, resource))) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\twhile (true) {\n\t\tconst agentsSkillsDir = join(currentDir, \".agents\", \"skills\");\n\t\tif (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\tif (parentDir === currentDir) {\n\t\t\treturn false;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport class ProjectTrustStore {\n\tprivate trustPath: string;\n\n\tconstructor(agentDir: string) {\n\t\tthis.trustPath = join(resolvePath(agentDir), \"trust.json\");\n\t}\n\n\tget(cwd: string): ProjectTrustDecision {\n\t\treturn this.getEntry(cwd)?.decision ?? null;\n\t}\n\n\tgetEntry(cwd: string): ProjectTrustStoreEntry | null {\n\t\treturn withTrustFileLock(this.trustPath, () => {\n\t\t\tconst data = readTrustFile(this.trustPath);\n\t\t\treturn findNearestTrustEntry(data, cwd);\n\t\t});\n\t}\n\n\tset(cwd: string, decision: ProjectTrustDecision): void {\n\t\tthis.setMany([{ path: cwd, decision }]);\n\t}\n\n\tsetMany(decisions: ProjectTrustUpdate[]): void {\n\t\twithTrustFileLock(this.trustPath, () => {\n\t\t\tconst data = readTrustFile(this.trustPath);\n\t\t\tfor (const { path, decision } of decisions) {\n\t\t\t\tconst key = normalizeCwd(path);\n\t\t\t\tif (decision === null) {\n\t\t\t\t\tdelete data[key];\n\t\t\t\t} else {\n\t\t\t\t\tdata[key] = decision;\n\t\t\t\t}\n\t\t\t}\n\t\t\twriteTrustFile(this.trustPath, data);\n\t\t});\n\t}\n}\n"]}
@@ -1,19 +1,40 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import lockfile from "proper-lockfile";
5
- import { CONFIG_DIR_NAME } from "../config.js";
6
5
  import { canonicalizePath, resolvePath } from "../utils/paths.js";
7
6
  import { stripBom } from "../utils/text.js";
8
- const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [
9
- "settings.json",
10
- "extensions",
11
- "skills",
12
- "prompts",
13
- "themes",
14
- "SYSTEM.md",
15
- "APPEND_SYSTEM.md",
16
- ];
7
+ import { permissionContentConfersAuthority } from "./permissions/store.js";
8
+ import { PROJECT_RESOURCES, projectResourcePath } from "./project-resources.js";
9
+ function resourceConfersAuthority(resource, absolutePath) {
10
+ if (!existsSync(absolutePath)) {
11
+ // `existsSync` follows links, so a broken link is indistinguishable from absence.
12
+ // A checkout controls the link, and the loader's own lock path creates the target
13
+ // on first write, so a dangling link is treated as conferring rather than skipped.
14
+ try {
15
+ lstatSync(absolutePath);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ return true;
21
+ }
22
+ if (resource.authority === "presence")
23
+ return true;
24
+ // An unreadable file is treated the same way unparseable content is, because both
25
+ // leave us unable to say the resource grants nothing.
26
+ try {
27
+ // The checkout controls this path, so it may point at a FIFO or a character device.
28
+ // A synchronous read of one blocks the process before any prompt is rendered, which
29
+ // turns a trust check into a denial of service. Anything but a regular file confers.
30
+ if (!lstatSync(absolutePath).isFile())
31
+ return true;
32
+ return permissionContentConfersAuthority(stripBom(readFileSync(absolutePath, "utf8")));
33
+ }
34
+ catch {
35
+ return true;
36
+ }
37
+ }
17
38
  function normalizeCwd(cwd) {
18
39
  return canonicalizePath(resolvePath(cwd));
19
40
  }
@@ -141,19 +162,21 @@ function withTrustFileLock(path, fn) {
141
162
  }
142
163
  }
143
164
  /**
144
- * Returns true when cwd has project-local resources that must be gated by
145
- * project trust: trust-requiring entries under cwd/.pi, or .agents/skills in
146
- * cwd or one of its ancestors. Returns false when no such project resources
147
- * exist. The user/global ~/.agents/skills directory is always treated as a
148
- * trusted user resource and is ignored here, even when cwd is $HOME.
165
+ * Returns true when cwd supplies a project-controlled resource that must be gated by
166
+ * project trust. The gated set is `PROJECT_RESOURCES`, which the loaders read too, plus
167
+ * a `.agents/skills` directory in cwd or one of its ancestors. Returns false when no
168
+ * such resource exists, or when the only ones present grant nothing (an empty
169
+ * permissions file). The user's own ~/.agents/skills is a trusted user resource and is
170
+ * ignored here, even when cwd is $HOME.
149
171
  */
150
172
  export function hasTrustRequiringProjectResources(cwd) {
151
173
  const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
152
174
  const userAgentsSkillsDir = join(homeDir, ".agents", "skills");
153
175
  let currentDir = canonicalizePath(resolvePath(cwd));
154
- const configDir = join(currentDir, CONFIG_DIR_NAME);
155
- if (TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry)))) {
156
- return true;
176
+ for (const resource of PROJECT_RESOURCES) {
177
+ if (resourceConfersAuthority(resource, projectResourcePath(currentDir, resource))) {
178
+ return true;
179
+ }
157
180
  }
158
181
  while (true) {
159
182
  const agentsSkillsDir = join(currentDir, ".agents", "skills");