apex-code 0.0.6 → 0.1.1

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 (234) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +8 -9
  3. package/dist/cli/args.d.ts +0 -12
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +2 -32
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +4 -130
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +1 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +1 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session-services.d.ts.map +1 -1
  15. package/dist/core/agent-session-services.js +0 -8
  16. package/dist/core/agent-session-services.js.map +1 -1
  17. package/dist/core/delegation/agents.d.ts.map +1 -1
  18. package/dist/core/delegation/agents.js +2 -1
  19. package/dist/core/delegation/agents.js.map +1 -1
  20. package/dist/core/delegation/runtime.d.ts +6 -14
  21. package/dist/core/delegation/runtime.d.ts.map +1 -1
  22. package/dist/core/delegation/runtime.js +1 -2
  23. package/dist/core/delegation/runtime.js.map +1 -1
  24. package/dist/core/extensions/loader.d.ts.map +1 -1
  25. package/dist/core/extensions/loader.js +3 -4
  26. package/dist/core/extensions/loader.js.map +1 -1
  27. package/dist/core/extensions/source-runtime.d.ts +4 -5
  28. package/dist/core/extensions/source-runtime.d.ts.map +1 -1
  29. package/dist/core/extensions/source-runtime.js +4 -5
  30. package/dist/core/extensions/source-runtime.js.map +1 -1
  31. package/dist/core/formatter-lifecycle.d.ts +2 -1
  32. package/dist/core/formatter-lifecycle.d.ts.map +1 -1
  33. package/dist/core/formatter-lifecycle.js +2 -1
  34. package/dist/core/formatter-lifecycle.js.map +1 -1
  35. package/dist/core/hooks/types.d.ts +2 -3
  36. package/dist/core/hooks/types.d.ts.map +1 -1
  37. package/dist/core/hooks/types.js +2 -3
  38. package/dist/core/hooks/types.js.map +1 -1
  39. package/dist/core/http-idle-timeout.d.ts +3 -5
  40. package/dist/core/http-idle-timeout.d.ts.map +1 -1
  41. package/dist/core/http-idle-timeout.js +3 -5
  42. package/dist/core/http-idle-timeout.js.map +1 -1
  43. package/dist/core/mcp/config.d.ts +18 -1
  44. package/dist/core/mcp/config.d.ts.map +1 -1
  45. package/dist/core/mcp/config.js +24 -7
  46. package/dist/core/mcp/config.js.map +1 -1
  47. package/dist/core/mcp/connector.d.ts +2 -3
  48. package/dist/core/mcp/connector.d.ts.map +1 -1
  49. package/dist/core/mcp/connector.js.map +1 -1
  50. package/dist/core/mcp/oauth/authorize.d.ts +2 -0
  51. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
  52. package/dist/core/mcp/oauth/authorize.js +5 -2
  53. package/dist/core/mcp/oauth/authorize.js.map +1 -1
  54. package/dist/core/mcp/oauth/mcp-token.d.ts +4 -6
  55. package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -1
  56. package/dist/core/mcp/oauth/mcp-token.js +5 -7
  57. package/dist/core/mcp/oauth/mcp-token.js.map +1 -1
  58. package/dist/core/mcp/runtime.d.ts +1 -0
  59. package/dist/core/mcp/runtime.d.ts.map +1 -1
  60. package/dist/core/mcp/runtime.js +2 -1
  61. package/dist/core/mcp/runtime.js.map +1 -1
  62. package/dist/core/package-manager.d.ts.map +1 -1
  63. package/dist/core/package-manager.js +0 -15
  64. package/dist/core/package-manager.js.map +1 -1
  65. package/dist/core/permissions/store.d.ts +6 -11
  66. package/dist/core/permissions/store.d.ts.map +1 -1
  67. package/dist/core/permissions/store.js +23 -51
  68. package/dist/core/permissions/store.js.map +1 -1
  69. package/dist/core/project-resources.d.ts +62 -0
  70. package/dist/core/project-resources.d.ts.map +1 -0
  71. package/dist/core/project-resources.js +42 -0
  72. package/dist/core/project-resources.js.map +1 -0
  73. package/dist/core/sdk.d.ts +1 -9
  74. package/dist/core/sdk.d.ts.map +1 -1
  75. package/dist/core/sdk.js +10 -35
  76. package/dist/core/sdk.js.map +1 -1
  77. package/dist/core/session-share.d.ts +6 -8
  78. package/dist/core/session-share.d.ts.map +1 -1
  79. package/dist/core/session-share.js +9 -12
  80. package/dist/core/session-share.js.map +1 -1
  81. package/dist/core/settings-manager.d.ts +0 -21
  82. package/dist/core/settings-manager.d.ts.map +1 -1
  83. package/dist/core/settings-manager.js +4 -12
  84. package/dist/core/settings-manager.js.map +1 -1
  85. package/dist/core/tools/bash.d.ts.map +1 -1
  86. package/dist/core/tools/bash.js +0 -47
  87. package/dist/core/tools/bash.js.map +1 -1
  88. package/dist/core/tools/web-fetch.d.ts.map +1 -1
  89. package/dist/core/tools/web-fetch.js +4 -8
  90. package/dist/core/tools/web-fetch.js.map +1 -1
  91. package/dist/core/tools/web-search-exa.d.ts +3 -6
  92. package/dist/core/tools/web-search-exa.d.ts.map +1 -1
  93. package/dist/core/tools/web-search-exa.js +5 -8
  94. package/dist/core/tools/web-search-exa.js.map +1 -1
  95. package/dist/core/trust-manager.d.ts +6 -5
  96. package/dist/core/trust-manager.d.ts.map +1 -1
  97. package/dist/core/trust-manager.js +42 -19
  98. package/dist/core/trust-manager.js.map +1 -1
  99. package/dist/core/web-search-provider.d.ts +0 -10
  100. package/dist/core/web-search-provider.d.ts.map +1 -1
  101. package/dist/core/web-search-provider.js +2 -16
  102. package/dist/core/web-search-provider.js.map +1 -1
  103. package/dist/main.d.ts +0 -1
  104. package/dist/main.d.ts.map +1 -1
  105. package/dist/main.js +2 -3
  106. package/dist/main.js.map +1 -1
  107. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  108. package/dist/modes/interactive/components/settings-selector.js +1 -1
  109. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  110. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  111. package/dist/modes/interactive/interactive-mode.js +0 -29
  112. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  113. package/dist/modes/rpc/rpc-types.d.ts +1 -1
  114. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  115. package/dist/modes/rpc/rpc-types.js.map +1 -1
  116. package/dist/utils/tools-manager.d.ts +0 -25
  117. package/dist/utils/tools-manager.d.ts.map +1 -1
  118. package/dist/utils/tools-manager.js +3 -52
  119. package/dist/utils/tools-manager.js.map +1 -1
  120. package/dist/utils/version-check.d.ts +0 -2
  121. package/dist/utils/version-check.d.ts.map +1 -1
  122. package/dist/utils/version-check.js +0 -2
  123. package/dist/utils/version-check.js.map +1 -1
  124. package/docs/containerization.md +8 -4
  125. package/docs/sdk.md +17 -24
  126. package/docs/security.md +7 -5
  127. package/docs/settings.md +2 -7
  128. package/docs/skills.md +4 -7
  129. package/npm-shrinkwrap.json +5 -5
  130. package/package.json +2 -2
  131. package/dist/core/sandbox/bwrap-arguments.d.ts +0 -41
  132. package/dist/core/sandbox/bwrap-arguments.d.ts.map +0 -1
  133. package/dist/core/sandbox/bwrap-arguments.js +0 -136
  134. package/dist/core/sandbox/bwrap-arguments.js.map +0 -1
  135. package/dist/core/sandbox/child-entry.d.ts +0 -2
  136. package/dist/core/sandbox/child-entry.d.ts.map +0 -1
  137. package/dist/core/sandbox/child-entry.js +0 -24
  138. package/dist/core/sandbox/child-entry.js.map +0 -1
  139. package/dist/core/sandbox/cli-launch.d.ts +0 -162
  140. package/dist/core/sandbox/cli-launch.d.ts.map +0 -1
  141. package/dist/core/sandbox/cli-launch.js +0 -366
  142. package/dist/core/sandbox/cli-launch.js.map +0 -1
  143. package/dist/core/sandbox/cli-supervisor.d.ts +0 -31
  144. package/dist/core/sandbox/cli-supervisor.d.ts.map +0 -1
  145. package/dist/core/sandbox/cli-supervisor.js +0 -147
  146. package/dist/core/sandbox/cli-supervisor.js.map +0 -1
  147. package/dist/core/sandbox/default-hosts.d.ts +0 -28
  148. package/dist/core/sandbox/default-hosts.d.ts.map +0 -1
  149. package/dist/core/sandbox/default-hosts.js +0 -69
  150. package/dist/core/sandbox/default-hosts.js.map +0 -1
  151. package/dist/core/sandbox/full-access.d.ts +0 -28
  152. package/dist/core/sandbox/full-access.d.ts.map +0 -1
  153. package/dist/core/sandbox/full-access.js +0 -63
  154. package/dist/core/sandbox/full-access.js.map +0 -1
  155. package/dist/core/sandbox/git-identity.d.ts +0 -57
  156. package/dist/core/sandbox/git-identity.d.ts.map +0 -1
  157. package/dist/core/sandbox/git-identity.js +0 -78
  158. package/dist/core/sandbox/git-identity.js.map +0 -1
  159. package/dist/core/sandbox/host-approval.d.ts +0 -42
  160. package/dist/core/sandbox/host-approval.d.ts.map +0 -1
  161. package/dist/core/sandbox/host-approval.js +0 -99
  162. package/dist/core/sandbox/host-approval.js.map +0 -1
  163. package/dist/core/sandbox/linux-backend.d.ts +0 -48
  164. package/dist/core/sandbox/linux-backend.d.ts.map +0 -1
  165. package/dist/core/sandbox/linux-backend.js +0 -328
  166. package/dist/core/sandbox/linux-backend.js.map +0 -1
  167. package/dist/core/sandbox/macos-backend.d.ts +0 -30
  168. package/dist/core/sandbox/macos-backend.d.ts.map +0 -1
  169. package/dist/core/sandbox/macos-backend.js +0 -385
  170. package/dist/core/sandbox/macos-backend.js.map +0 -1
  171. package/dist/core/sandbox/network-proxy.d.ts +0 -38
  172. package/dist/core/sandbox/network-proxy.d.ts.map +0 -1
  173. package/dist/core/sandbox/network-proxy.js +0 -153
  174. package/dist/core/sandbox/network-proxy.js.map +0 -1
  175. package/dist/core/sandbox/network-refusal.d.ts +0 -24
  176. package/dist/core/sandbox/network-refusal.d.ts.map +0 -1
  177. package/dist/core/sandbox/network-refusal.js +0 -72
  178. package/dist/core/sandbox/network-refusal.js.map +0 -1
  179. package/dist/core/sandbox/policy.d.ts +0 -40
  180. package/dist/core/sandbox/policy.d.ts.map +0 -1
  181. package/dist/core/sandbox/policy.js +0 -55
  182. package/dist/core/sandbox/policy.js.map +0 -1
  183. package/dist/core/sandbox/profiles.d.ts +0 -25
  184. package/dist/core/sandbox/profiles.d.ts.map +0 -1
  185. package/dist/core/sandbox/profiles.js +0 -23
  186. package/dist/core/sandbox/profiles.js.map +0 -1
  187. package/dist/core/sandbox/rpc/command-client.d.ts +0 -28
  188. package/dist/core/sandbox/rpc/command-client.d.ts.map +0 -1
  189. package/dist/core/sandbox/rpc/command-client.js +0 -86
  190. package/dist/core/sandbox/rpc/command-client.js.map +0 -1
  191. package/dist/core/sandbox/rpc/command-proxy.d.ts +0 -48
  192. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +0 -1
  193. package/dist/core/sandbox/rpc/command-proxy.js +0 -120
  194. package/dist/core/sandbox/rpc/command-proxy.js.map +0 -1
  195. package/dist/core/sandbox/rpc/credential-client.d.ts +0 -20
  196. package/dist/core/sandbox/rpc/credential-client.d.ts.map +0 -1
  197. package/dist/core/sandbox/rpc/credential-client.js +0 -170
  198. package/dist/core/sandbox/rpc/credential-client.js.map +0 -1
  199. package/dist/core/sandbox/rpc/credential-proxy.d.ts +0 -44
  200. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +0 -1
  201. package/dist/core/sandbox/rpc/credential-proxy.js +0 -287
  202. package/dist/core/sandbox/rpc/credential-proxy.js.map +0 -1
  203. package/dist/core/sandbox/rpc/framing.d.ts +0 -35
  204. package/dist/core/sandbox/rpc/framing.d.ts.map +0 -1
  205. package/dist/core/sandbox/rpc/framing.js +0 -114
  206. package/dist/core/sandbox/rpc/framing.js.map +0 -1
  207. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +0 -47
  208. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +0 -1
  209. package/dist/core/sandbox/rpc/git-credential-helper.js +0 -222
  210. package/dist/core/sandbox/rpc/git-credential-helper.js.map +0 -1
  211. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +0 -66
  212. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +0 -1
  213. package/dist/core/sandbox/rpc/git-credential-proxy.js +0 -209
  214. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +0 -1
  215. package/dist/core/sandbox/supervisor-temp.d.ts +0 -18
  216. package/dist/core/sandbox/supervisor-temp.d.ts.map +0 -1
  217. package/dist/core/sandbox/supervisor-temp.js +0 -21
  218. package/dist/core/sandbox/supervisor-temp.js.map +0 -1
  219. package/dist/core/sandbox/supervisor.d.ts +0 -62
  220. package/dist/core/sandbox/supervisor.d.ts.map +0 -1
  221. package/dist/core/sandbox/supervisor.js +0 -32
  222. package/dist/core/sandbox/supervisor.js.map +0 -1
  223. package/dist/core/sandbox/terminal-handoff.d.ts +0 -68
  224. package/dist/core/sandbox/terminal-handoff.d.ts.map +0 -1
  225. package/dist/core/sandbox/terminal-handoff.js +0 -221
  226. package/dist/core/sandbox/terminal-handoff.js.map +0 -1
  227. package/dist/core/sandbox/terminal-size.d.ts +0 -53
  228. package/dist/core/sandbox/terminal-size.d.ts.map +0 -1
  229. package/dist/core/sandbox/terminal-size.js +0 -126
  230. package/dist/core/sandbox/terminal-size.js.map +0 -1
  231. package/dist/core/sandbox/violations.d.ts +0 -23
  232. package/dist/core/sandbox/violations.d.ts.map +0 -1
  233. package/dist/core/sandbox/violations.js +0 -30
  234. package/dist/core/sandbox/violations.js.map +0 -1
@@ -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");
@@ -1 +1 @@
1
- {"version":3,"file":"trust-manager.js","sourceRoot":"","sources":["../../src/core/trust-manager.ts"],"names":[],"mappings":"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","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.js","sourceRoot":"","sources":["../../src/core/trust-manager.ts"],"names":[],"mappings":"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","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"]}
@@ -22,16 +22,6 @@ export declare const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId;
22
22
  * the repository is what the credential store is for.
23
23
  */
24
24
  export declare function webSearchSettingsError(settings?: WebSearchSettings): string | undefined;
25
- /**
26
- * The host `web_search` needs on the sandbox allowlist, or undefined when no
27
- * credential is configured for the selected backend.
28
- *
29
- * Deliberately checks whether the key *reference* resolves rather than resolving it:
30
- * the supervisor calls this on every launch, and a `!command` reference would
31
- * otherwise spawn a shell before the child even starts. That check is an environment
32
- * lookup for a `$VAR` reference and free for a command reference.
33
- */
34
- export declare function resolveWebSearchHost(settings: WebSearchSettings | undefined, env?: Record<string, string>, authPath?: string): string | undefined;
35
25
  /**
36
26
  * Build the configured `web_search` backend, or undefined when no credential
37
27
  * resolves.
@@ -1 +1 @@
1
- {"version":3,"file":"web-search-provider.d.ts","sourceRoot":"","sources":["../../src/core/web-search-provider.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAGjE,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAsCxC;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,mBAA2B,CAAC;AAwBtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAQvF;AAiBD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,QAAQ,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAGpB;AAED;;;;;;;;GAQG;AACH;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAIjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAChD,YAAY,EAAE,MAAM,iBAAiB,GAAG,SAAS,EACjD,QAAQ,CAAC,EAAE,MAAM,GACf,mBAAmB,CAarB;AAED,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,QAAQ,CAAC,EAAE,MAAM,GACf,mBAAmB,GAAG,SAAS,CASjC","sourcesContent":["import { readStoredCredential } from \"./auth-storage.ts\";\nimport {\n\tgetConfigValueEnvVarNames,\n\tisCommandConfigValue,\n\tisConfigValueConfigured,\n\tresolveConfigValue,\n} from \"./resolve-config-value.ts\";\nimport type { WebSearchSettings } from \"./settings-manager.ts\";\nimport type { WebSearchOperations } from \"./tools/web-search.ts\";\nimport { createExaWebSearchOperations, EXA_SEARCH_HOST } from \"./tools/web-search-exa.ts\";\n\nexport type WebSearchProviderId = \"exa\";\n\n/**\n * One entry per backend `web_search` can speak to.\n *\n * A table rather than a branch because the swap is the point: the `web_search` tool\n * owns the contract and the permission grammar, the backend owns nothing but an HTTP\n * call, and adding the second backend should be a row here rather than a new code\n * path through the resolver.\n */\ninterface WebSearchProviderDefinition {\n\t/**\n\t * Config-value reference used when settings name no key. Held as a reference,\n\t * never a literal: a resolved key must never reach a file the settings loader\n\t * writes back.\n\t */\n\treadonly defaultApiKeyReference: string;\n\t/** Host the sandbox must allow before this backend can reach anything. */\n\treadonly host: string;\n\t/** `auth.json` key holding a credential saved from the settings panel. */\n\treadonly credentialId: string;\n\tcreate(options: {\n\t\tapiKey: string;\n\t\tendpoint?: string;\n\t\tnumResults?: number;\n\t\tsnippetMaxCharacters?: number;\n\t}): WebSearchOperations;\n}\n\nconst WEB_SEARCH_PROVIDERS: Record<WebSearchProviderId, WebSearchProviderDefinition> = {\n\texa: {\n\t\tdefaultApiKeyReference: \"$EXA_API_KEY\",\n\t\thost: EXA_SEARCH_HOST,\n\t\tcredentialId: \"exa\",\n\t\tcreate: createExaWebSearchOperations,\n\t},\n};\n\n/**\n * Backend used when settings name none. `web_search` still reports itself\n * unconfigured unless this backend's key resolves, so the default selects which\n * credential is looked for, not whether the tool reaches the network.\n */\nexport const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = \"exa\";\n\nfunction definitionFor(settings: WebSearchSettings | undefined): WebSearchProviderDefinition {\n\treturn WEB_SEARCH_PROVIDERS[settings?.provider ?? DEFAULT_WEB_SEARCH_PROVIDER];\n}\n\n/** `auth.json` key holding a stored credential for the selected backend. */\nfunction webSearchCredentialId(settings?: WebSearchSettings): string {\n\treturn definitionFor(settings).credentialId;\n}\n\n/**\n * The reference stored by the settings panel, or undefined when nothing is saved.\n *\n * Whatever was saved stays a reference rather than being resolved here, so the one\n * resolution below covers a literal key, a `$VAR`, and a `!command` alike. An OAuth\n * credential under the same key is not an API key and is ignored rather than coerced.\n */\nfunction storedApiKeyReference(settings: WebSearchSettings | undefined, authPath?: string): string | undefined {\n\tconst credential = readStoredCredential(webSearchCredentialId(settings), authPath);\n\tif (credential?.type !== \"api_key\") return undefined;\n\treturn credential.key?.trim() || undefined;\n}\n\n/**\n * Reject a literal secret configured in settings.\n *\n * `webSearch.apiKey` is read from project settings as well as global ones, and this\n * repository deliberately does not gitignore `<project>/.apex-code/` -- that directory\n * is meant to be versioned and shared with a team. A literal key written there would be\n * committed and handed to everyone who clones the repository.\n *\n * A reference (`$VAR`, `${VAR}`, `!command`) names where the secret lives without\n * containing it, so it is safe to commit. AGENTS.md already states that no credential\n * belongs in a file the settings loader writes; this makes that rule enforced rather\n * than documented. `auth.json` still accepts literals, because holding secrets outside\n * the repository is what the credential store is for.\n */\nexport function webSearchSettingsError(settings?: WebSearchSettings): string | undefined {\n\tconst configured = settings?.apiKey?.trim();\n\tif (!configured) return undefined;\n\tif (isCommandConfigValue(configured)) return undefined;\n\tif (getConfigValueEnvVarNames(configured).length > 0) return undefined;\n\t// The value itself is never echoed: this message reaches the model, and from there a\n\t// transcript. Naming the setting is enough to act on.\n\treturn `webSearch.apiKey must be a reference, not a literal key, because project settings are versioned and shared. Use \"$EXA_API_KEY\", \"\\${YOUR_VAR}\", or \"!your-command\", and keep the secret in your environment or credential store.`;\n}\n\n/**\n * Pick one reference, then resolve it once.\n *\n * Order is explicit configuration, then saved credential, then ambient environment.\n * A user who just typed a key into the settings dialog expects it to beat a stale\n * export they have forgotten about, and an explicit `webSearch.apiKey` in settings is\n * a direct instruction that should beat both.\n */\nfunction apiKeyReferenceFor(settings: WebSearchSettings | undefined, authPath?: string): string {\n\t// A refused literal falls through rather than being honoured, so a committed key is\n\t// inert instead of quietly working for whoever cloned the repository.\n\tconst configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();\n\treturn configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;\n}\n\n/**\n * The host `web_search` needs on the sandbox allowlist, or undefined when no\n * credential is configured for the selected backend.\n *\n * Deliberately checks whether the key *reference* resolves rather than resolving it:\n * the supervisor calls this on every launch, and a `!command` reference would\n * otherwise spawn a shell before the child even starts. That check is an environment\n * lookup for a `$VAR` reference and free for a command reference.\n */\nexport function resolveWebSearchHost(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): string | undefined {\n\tconst definition = definitionFor(settings);\n\treturn isConfigValueConfigured(apiKeyReferenceFor(settings, authPath), env) ? definition.host : undefined;\n}\n\n/**\n * Build the configured `web_search` backend, or undefined when no credential\n * resolves.\n *\n * Undefined leaves the tool on its unconfigured operations, which throw a\n * model-readable \"not configured\" error. That is deliberately not the same thing as\n * hiding the tool: a session that silently lacks `web_search` cannot tell the model\n * why, and the throw names the missing piece.\n */\n/**\n * What to tell the model, and through it the user, when nothing is configured.\n *\n * Named here rather than in `web-search.ts` because only this module knows which\n * backend was selected and therefore which credential to name. The audience is a\n * person who asked the agent to search something, so it names the two things they\n * can actually do, not the SDK option an embedder would pass.\n */\nexport function unconfiguredWebSearchMessage(settings?: WebSearchSettings): string {\n\tconst definition = definitionFor(settings);\n\tconst variable = definition.defaultApiKeyReference.replace(/^\\$/, \"\");\n\treturn `web_search has no API key configured. Set ${variable} in the environment, or run /settings and add a ${definition.credentialId} key.`;\n}\n\n/**\n * A `web_search` backend that resolves its credential on each call.\n *\n * The session builds its tool registry once, at construction, so operations captured\n * there would pin whatever was configured at startup. A key saved from the settings\n * panel would then appear to save and do nothing until restart, which is worse than\n * having no settings row at all. Resolving per call costs one small file read and\n * keeps `resolveConfigValue`'s cache for command references.\n */\nexport function createDeferredWebSearchOperations(\n\treadSettings: () => WebSearchSettings | undefined,\n\tauthPath?: string,\n): WebSearchOperations {\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tconst settings = readSettings();\n\t\t\t// Reported ahead of the unconfigured message: \"no key\" and \"your key is in the\n\t\t\t// wrong place\" need different fixes.\n\t\t\tconst settingsError = webSearchSettingsError(settings);\n\t\t\tif (settingsError) throw new Error(settingsError);\n\t\t\tconst operations = resolveWebSearchOperations(settings, undefined, authPath);\n\t\t\tif (!operations) throw new Error(unconfiguredWebSearchMessage(settings));\n\t\t\treturn operations.search(query, signal);\n\t\t},\n\t};\n}\n\nexport function resolveWebSearchOperations(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): WebSearchOperations | undefined {\n\tconst apiKey = resolveConfigValue(apiKeyReferenceFor(settings, authPath), env)?.trim();\n\tif (!apiKey) return undefined;\n\treturn definitionFor(settings).create({\n\t\tapiKey,\n\t\t...(settings?.endpoint === undefined ? {} : { endpoint: settings.endpoint }),\n\t\t...(settings?.numResults === undefined ? {} : { numResults: settings.numResults }),\n\t\t...(settings?.snippetMaxCharacters === undefined ? {} : { snippetMaxCharacters: settings.snippetMaxCharacters }),\n\t});\n}\n"]}
1
+ {"version":3,"file":"web-search-provider.d.ts","sourceRoot":"","sources":["../../src/core/web-search-provider.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAGjE,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAmCxC;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,mBAA2B,CAAC;AAwBtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAQvF;AAiBD;;;;;;;;GAQG;AACH;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAIjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAChD,YAAY,EAAE,MAAM,iBAAiB,GAAG,SAAS,EACjD,QAAQ,CAAC,EAAE,MAAM,GACf,mBAAmB,CAarB;AAED,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,QAAQ,CAAC,EAAE,MAAM,GACf,mBAAmB,GAAG,SAAS,CASjC","sourcesContent":["import { readStoredCredential } from \"./auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue, resolveConfigValue } from \"./resolve-config-value.ts\";\nimport type { WebSearchSettings } from \"./settings-manager.ts\";\nimport type { WebSearchOperations } from \"./tools/web-search.ts\";\nimport { createExaWebSearchOperations } from \"./tools/web-search-exa.ts\";\n\nexport type WebSearchProviderId = \"exa\";\n\n/**\n * One entry per backend `web_search` can speak to.\n *\n * A table rather than a branch because the swap is the point: the `web_search` tool\n * owns the contract and the permission grammar, the backend owns nothing but an HTTP\n * call, and adding the second backend should be a row here rather than a new code\n * path through the resolver.\n */\ninterface WebSearchProviderDefinition {\n\t/**\n\t * Config-value reference used when settings name no key. Held as a reference,\n\t * never a literal: a resolved key must never reach a file the settings loader\n\t * writes back.\n\t */\n\treadonly defaultApiKeyReference: string;\n\t/** `auth.json` key holding a credential saved from the settings panel. */\n\treadonly credentialId: string;\n\tcreate(options: {\n\t\tapiKey: string;\n\t\tendpoint?: string;\n\t\tnumResults?: number;\n\t\tsnippetMaxCharacters?: number;\n\t}): WebSearchOperations;\n}\n\nconst WEB_SEARCH_PROVIDERS: Record<WebSearchProviderId, WebSearchProviderDefinition> = {\n\texa: {\n\t\tdefaultApiKeyReference: \"$EXA_API_KEY\",\n\t\tcredentialId: \"exa\",\n\t\tcreate: createExaWebSearchOperations,\n\t},\n};\n\n/**\n * Backend used when settings name none. `web_search` still reports itself\n * unconfigured unless this backend's key resolves, so the default selects which\n * credential is looked for, not whether the tool reaches the network.\n */\nexport const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = \"exa\";\n\nfunction definitionFor(settings: WebSearchSettings | undefined): WebSearchProviderDefinition {\n\treturn WEB_SEARCH_PROVIDERS[settings?.provider ?? DEFAULT_WEB_SEARCH_PROVIDER];\n}\n\n/** `auth.json` key holding a stored credential for the selected backend. */\nfunction webSearchCredentialId(settings?: WebSearchSettings): string {\n\treturn definitionFor(settings).credentialId;\n}\n\n/**\n * The reference stored by the settings panel, or undefined when nothing is saved.\n *\n * Whatever was saved stays a reference rather than being resolved here, so the one\n * resolution below covers a literal key, a `$VAR`, and a `!command` alike. An OAuth\n * credential under the same key is not an API key and is ignored rather than coerced.\n */\nfunction storedApiKeyReference(settings: WebSearchSettings | undefined, authPath?: string): string | undefined {\n\tconst credential = readStoredCredential(webSearchCredentialId(settings), authPath);\n\tif (credential?.type !== \"api_key\") return undefined;\n\treturn credential.key?.trim() || undefined;\n}\n\n/**\n * Reject a literal secret configured in settings.\n *\n * `webSearch.apiKey` is read from project settings as well as global ones, and this\n * repository deliberately does not gitignore `<project>/.apex-code/` -- that directory\n * is meant to be versioned and shared with a team. A literal key written there would be\n * committed and handed to everyone who clones the repository.\n *\n * A reference (`$VAR`, `${VAR}`, `!command`) names where the secret lives without\n * containing it, so it is safe to commit. AGENTS.md already states that no credential\n * belongs in a file the settings loader writes; this makes that rule enforced rather\n * than documented. `auth.json` still accepts literals, because holding secrets outside\n * the repository is what the credential store is for.\n */\nexport function webSearchSettingsError(settings?: WebSearchSettings): string | undefined {\n\tconst configured = settings?.apiKey?.trim();\n\tif (!configured) return undefined;\n\tif (isCommandConfigValue(configured)) return undefined;\n\tif (getConfigValueEnvVarNames(configured).length > 0) return undefined;\n\t// The value itself is never echoed: this message reaches the model, and from there a\n\t// transcript. Naming the setting is enough to act on.\n\treturn `webSearch.apiKey must be a reference, not a literal key, because project settings are versioned and shared. Use \"$EXA_API_KEY\", \"\\${YOUR_VAR}\", or \"!your-command\", and keep the secret in your environment or credential store.`;\n}\n\n/**\n * Pick one reference, then resolve it once.\n *\n * Order is explicit configuration, then saved credential, then ambient environment.\n * A user who just typed a key into the settings dialog expects it to beat a stale\n * export they have forgotten about, and an explicit `webSearch.apiKey` in settings is\n * a direct instruction that should beat both.\n */\nfunction apiKeyReferenceFor(settings: WebSearchSettings | undefined, authPath?: string): string {\n\t// A refused literal falls through rather than being honoured, so a committed key is\n\t// inert instead of quietly working for whoever cloned the repository.\n\tconst configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();\n\treturn configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;\n}\n\n/**\n * Build the configured `web_search` backend, or undefined when no credential\n * resolves.\n *\n * Undefined leaves the tool on its unconfigured operations, which throw a\n * model-readable \"not configured\" error. That is deliberately not the same thing as\n * hiding the tool: a session that silently lacks `web_search` cannot tell the model\n * why, and the throw names the missing piece.\n */\n/**\n * What to tell the model, and through it the user, when nothing is configured.\n *\n * Named here rather than in `web-search.ts` because only this module knows which\n * backend was selected and therefore which credential to name. The audience is a\n * person who asked the agent to search something, so it names the two things they\n * can actually do, not the SDK option an embedder would pass.\n */\nexport function unconfiguredWebSearchMessage(settings?: WebSearchSettings): string {\n\tconst definition = definitionFor(settings);\n\tconst variable = definition.defaultApiKeyReference.replace(/^\\$/, \"\");\n\treturn `web_search has no API key configured. Set ${variable} in the environment, or run /settings and add a ${definition.credentialId} key.`;\n}\n\n/**\n * A `web_search` backend that resolves its credential on each call.\n *\n * The session builds its tool registry once, at construction, so operations captured\n * there would pin whatever was configured at startup. A key saved from the settings\n * panel would then appear to save and do nothing until restart, which is worse than\n * having no settings row at all. Resolving per call costs one small file read and\n * keeps `resolveConfigValue`'s cache for command references.\n */\nexport function createDeferredWebSearchOperations(\n\treadSettings: () => WebSearchSettings | undefined,\n\tauthPath?: string,\n): WebSearchOperations {\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tconst settings = readSettings();\n\t\t\t// Reported ahead of the unconfigured message: \"no key\" and \"your key is in the\n\t\t\t// wrong place\" need different fixes.\n\t\t\tconst settingsError = webSearchSettingsError(settings);\n\t\t\tif (settingsError) throw new Error(settingsError);\n\t\t\tconst operations = resolveWebSearchOperations(settings, undefined, authPath);\n\t\t\tif (!operations) throw new Error(unconfiguredWebSearchMessage(settings));\n\t\t\treturn operations.search(query, signal);\n\t\t},\n\t};\n}\n\nexport function resolveWebSearchOperations(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): WebSearchOperations | undefined {\n\tconst apiKey = resolveConfigValue(apiKeyReferenceFor(settings, authPath), env)?.trim();\n\tif (!apiKey) return undefined;\n\treturn definitionFor(settings).create({\n\t\tapiKey,\n\t\t...(settings?.endpoint === undefined ? {} : { endpoint: settings.endpoint }),\n\t\t...(settings?.numResults === undefined ? {} : { numResults: settings.numResults }),\n\t\t...(settings?.snippetMaxCharacters === undefined ? {} : { snippetMaxCharacters: settings.snippetMaxCharacters }),\n\t});\n}\n"]}
@@ -1,10 +1,9 @@
1
1
  import { readStoredCredential } from "./auth-storage.js";
2
- import { getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, resolveConfigValue, } from "./resolve-config-value.js";
3
- import { createExaWebSearchOperations, EXA_SEARCH_HOST } from "./tools/web-search-exa.js";
2
+ import { getConfigValueEnvVarNames, isCommandConfigValue, resolveConfigValue } from "./resolve-config-value.js";
3
+ import { createExaWebSearchOperations } from "./tools/web-search-exa.js";
4
4
  const WEB_SEARCH_PROVIDERS = {
5
5
  exa: {
6
6
  defaultApiKeyReference: "$EXA_API_KEY",
7
- host: EXA_SEARCH_HOST,
8
7
  credentialId: "exa",
9
8
  create: createExaWebSearchOperations,
10
9
  },
@@ -75,19 +74,6 @@ function apiKeyReferenceFor(settings, authPath) {
75
74
  const configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();
76
75
  return configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;
77
76
  }
78
- /**
79
- * The host `web_search` needs on the sandbox allowlist, or undefined when no
80
- * credential is configured for the selected backend.
81
- *
82
- * Deliberately checks whether the key *reference* resolves rather than resolving it:
83
- * the supervisor calls this on every launch, and a `!command` reference would
84
- * otherwise spawn a shell before the child even starts. That check is an environment
85
- * lookup for a `$VAR` reference and free for a command reference.
86
- */
87
- export function resolveWebSearchHost(settings, env, authPath) {
88
- const definition = definitionFor(settings);
89
- return isConfigValueConfigured(apiKeyReferenceFor(settings, authPath), env) ? definition.host : undefined;
90
- }
91
77
  /**
92
78
  * Build the configured `web_search` backend, or undefined when no credential
93
79
  * resolves.
@@ -1 +1 @@
1
- {"version":3,"file":"web-search-provider.js","sourceRoot":"","sources":["../../src/core/web-search-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { readStoredCredential } from \"./auth-storage.ts\";\nimport {\n\tgetConfigValueEnvVarNames,\n\tisCommandConfigValue,\n\tisConfigValueConfigured,\n\tresolveConfigValue,\n} from \"./resolve-config-value.ts\";\nimport type { WebSearchSettings } from \"./settings-manager.ts\";\nimport type { WebSearchOperations } from \"./tools/web-search.ts\";\nimport { createExaWebSearchOperations, EXA_SEARCH_HOST } from \"./tools/web-search-exa.ts\";\n\nexport type WebSearchProviderId = \"exa\";\n\n/**\n * One entry per backend `web_search` can speak to.\n *\n * A table rather than a branch because the swap is the point: the `web_search` tool\n * owns the contract and the permission grammar, the backend owns nothing but an HTTP\n * call, and adding the second backend should be a row here rather than a new code\n * path through the resolver.\n */\ninterface WebSearchProviderDefinition {\n\t/**\n\t * Config-value reference used when settings name no key. Held as a reference,\n\t * never a literal: a resolved key must never reach a file the settings loader\n\t * writes back.\n\t */\n\treadonly defaultApiKeyReference: string;\n\t/** Host the sandbox must allow before this backend can reach anything. */\n\treadonly host: string;\n\t/** `auth.json` key holding a credential saved from the settings panel. */\n\treadonly credentialId: string;\n\tcreate(options: {\n\t\tapiKey: string;\n\t\tendpoint?: string;\n\t\tnumResults?: number;\n\t\tsnippetMaxCharacters?: number;\n\t}): WebSearchOperations;\n}\n\nconst WEB_SEARCH_PROVIDERS: Record<WebSearchProviderId, WebSearchProviderDefinition> = {\n\texa: {\n\t\tdefaultApiKeyReference: \"$EXA_API_KEY\",\n\t\thost: EXA_SEARCH_HOST,\n\t\tcredentialId: \"exa\",\n\t\tcreate: createExaWebSearchOperations,\n\t},\n};\n\n/**\n * Backend used when settings name none. `web_search` still reports itself\n * unconfigured unless this backend's key resolves, so the default selects which\n * credential is looked for, not whether the tool reaches the network.\n */\nexport const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = \"exa\";\n\nfunction definitionFor(settings: WebSearchSettings | undefined): WebSearchProviderDefinition {\n\treturn WEB_SEARCH_PROVIDERS[settings?.provider ?? DEFAULT_WEB_SEARCH_PROVIDER];\n}\n\n/** `auth.json` key holding a stored credential for the selected backend. */\nfunction webSearchCredentialId(settings?: WebSearchSettings): string {\n\treturn definitionFor(settings).credentialId;\n}\n\n/**\n * The reference stored by the settings panel, or undefined when nothing is saved.\n *\n * Whatever was saved stays a reference rather than being resolved here, so the one\n * resolution below covers a literal key, a `$VAR`, and a `!command` alike. An OAuth\n * credential under the same key is not an API key and is ignored rather than coerced.\n */\nfunction storedApiKeyReference(settings: WebSearchSettings | undefined, authPath?: string): string | undefined {\n\tconst credential = readStoredCredential(webSearchCredentialId(settings), authPath);\n\tif (credential?.type !== \"api_key\") return undefined;\n\treturn credential.key?.trim() || undefined;\n}\n\n/**\n * Reject a literal secret configured in settings.\n *\n * `webSearch.apiKey` is read from project settings as well as global ones, and this\n * repository deliberately does not gitignore `<project>/.apex-code/` -- that directory\n * is meant to be versioned and shared with a team. A literal key written there would be\n * committed and handed to everyone who clones the repository.\n *\n * A reference (`$VAR`, `${VAR}`, `!command`) names where the secret lives without\n * containing it, so it is safe to commit. AGENTS.md already states that no credential\n * belongs in a file the settings loader writes; this makes that rule enforced rather\n * than documented. `auth.json` still accepts literals, because holding secrets outside\n * the repository is what the credential store is for.\n */\nexport function webSearchSettingsError(settings?: WebSearchSettings): string | undefined {\n\tconst configured = settings?.apiKey?.trim();\n\tif (!configured) return undefined;\n\tif (isCommandConfigValue(configured)) return undefined;\n\tif (getConfigValueEnvVarNames(configured).length > 0) return undefined;\n\t// The value itself is never echoed: this message reaches the model, and from there a\n\t// transcript. Naming the setting is enough to act on.\n\treturn `webSearch.apiKey must be a reference, not a literal key, because project settings are versioned and shared. Use \"$EXA_API_KEY\", \"\\${YOUR_VAR}\", or \"!your-command\", and keep the secret in your environment or credential store.`;\n}\n\n/**\n * Pick one reference, then resolve it once.\n *\n * Order is explicit configuration, then saved credential, then ambient environment.\n * A user who just typed a key into the settings dialog expects it to beat a stale\n * export they have forgotten about, and an explicit `webSearch.apiKey` in settings is\n * a direct instruction that should beat both.\n */\nfunction apiKeyReferenceFor(settings: WebSearchSettings | undefined, authPath?: string): string {\n\t// A refused literal falls through rather than being honoured, so a committed key is\n\t// inert instead of quietly working for whoever cloned the repository.\n\tconst configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();\n\treturn configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;\n}\n\n/**\n * The host `web_search` needs on the sandbox allowlist, or undefined when no\n * credential is configured for the selected backend.\n *\n * Deliberately checks whether the key *reference* resolves rather than resolving it:\n * the supervisor calls this on every launch, and a `!command` reference would\n * otherwise spawn a shell before the child even starts. That check is an environment\n * lookup for a `$VAR` reference and free for a command reference.\n */\nexport function resolveWebSearchHost(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): string | undefined {\n\tconst definition = definitionFor(settings);\n\treturn isConfigValueConfigured(apiKeyReferenceFor(settings, authPath), env) ? definition.host : undefined;\n}\n\n/**\n * Build the configured `web_search` backend, or undefined when no credential\n * resolves.\n *\n * Undefined leaves the tool on its unconfigured operations, which throw a\n * model-readable \"not configured\" error. That is deliberately not the same thing as\n * hiding the tool: a session that silently lacks `web_search` cannot tell the model\n * why, and the throw names the missing piece.\n */\n/**\n * What to tell the model, and through it the user, when nothing is configured.\n *\n * Named here rather than in `web-search.ts` because only this module knows which\n * backend was selected and therefore which credential to name. The audience is a\n * person who asked the agent to search something, so it names the two things they\n * can actually do, not the SDK option an embedder would pass.\n */\nexport function unconfiguredWebSearchMessage(settings?: WebSearchSettings): string {\n\tconst definition = definitionFor(settings);\n\tconst variable = definition.defaultApiKeyReference.replace(/^\\$/, \"\");\n\treturn `web_search has no API key configured. Set ${variable} in the environment, or run /settings and add a ${definition.credentialId} key.`;\n}\n\n/**\n * A `web_search` backend that resolves its credential on each call.\n *\n * The session builds its tool registry once, at construction, so operations captured\n * there would pin whatever was configured at startup. A key saved from the settings\n * panel would then appear to save and do nothing until restart, which is worse than\n * having no settings row at all. Resolving per call costs one small file read and\n * keeps `resolveConfigValue`'s cache for command references.\n */\nexport function createDeferredWebSearchOperations(\n\treadSettings: () => WebSearchSettings | undefined,\n\tauthPath?: string,\n): WebSearchOperations {\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tconst settings = readSettings();\n\t\t\t// Reported ahead of the unconfigured message: \"no key\" and \"your key is in the\n\t\t\t// wrong place\" need different fixes.\n\t\t\tconst settingsError = webSearchSettingsError(settings);\n\t\t\tif (settingsError) throw new Error(settingsError);\n\t\t\tconst operations = resolveWebSearchOperations(settings, undefined, authPath);\n\t\t\tif (!operations) throw new Error(unconfiguredWebSearchMessage(settings));\n\t\t\treturn operations.search(query, signal);\n\t\t},\n\t};\n}\n\nexport function resolveWebSearchOperations(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): WebSearchOperations | undefined {\n\tconst apiKey = resolveConfigValue(apiKeyReferenceFor(settings, authPath), env)?.trim();\n\tif (!apiKey) return undefined;\n\treturn definitionFor(settings).create({\n\t\tapiKey,\n\t\t...(settings?.endpoint === undefined ? {} : { endpoint: settings.endpoint }),\n\t\t...(settings?.numResults === undefined ? {} : { numResults: settings.numResults }),\n\t\t...(settings?.snippetMaxCharacters === undefined ? {} : { snippetMaxCharacters: settings.snippetMaxCharacters }),\n\t});\n}\n"]}
1
+ {"version":3,"file":"web-search-provider.js","sourceRoot":"","sources":["../../src/core/web-search-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { readStoredCredential } from \"./auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue, resolveConfigValue } from \"./resolve-config-value.ts\";\nimport type { WebSearchSettings } from \"./settings-manager.ts\";\nimport type { WebSearchOperations } from \"./tools/web-search.ts\";\nimport { createExaWebSearchOperations } from \"./tools/web-search-exa.ts\";\n\nexport type WebSearchProviderId = \"exa\";\n\n/**\n * One entry per backend `web_search` can speak to.\n *\n * A table rather than a branch because the swap is the point: the `web_search` tool\n * owns the contract and the permission grammar, the backend owns nothing but an HTTP\n * call, and adding the second backend should be a row here rather than a new code\n * path through the resolver.\n */\ninterface WebSearchProviderDefinition {\n\t/**\n\t * Config-value reference used when settings name no key. Held as a reference,\n\t * never a literal: a resolved key must never reach a file the settings loader\n\t * writes back.\n\t */\n\treadonly defaultApiKeyReference: string;\n\t/** `auth.json` key holding a credential saved from the settings panel. */\n\treadonly credentialId: string;\n\tcreate(options: {\n\t\tapiKey: string;\n\t\tendpoint?: string;\n\t\tnumResults?: number;\n\t\tsnippetMaxCharacters?: number;\n\t}): WebSearchOperations;\n}\n\nconst WEB_SEARCH_PROVIDERS: Record<WebSearchProviderId, WebSearchProviderDefinition> = {\n\texa: {\n\t\tdefaultApiKeyReference: \"$EXA_API_KEY\",\n\t\tcredentialId: \"exa\",\n\t\tcreate: createExaWebSearchOperations,\n\t},\n};\n\n/**\n * Backend used when settings name none. `web_search` still reports itself\n * unconfigured unless this backend's key resolves, so the default selects which\n * credential is looked for, not whether the tool reaches the network.\n */\nexport const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = \"exa\";\n\nfunction definitionFor(settings: WebSearchSettings | undefined): WebSearchProviderDefinition {\n\treturn WEB_SEARCH_PROVIDERS[settings?.provider ?? DEFAULT_WEB_SEARCH_PROVIDER];\n}\n\n/** `auth.json` key holding a stored credential for the selected backend. */\nfunction webSearchCredentialId(settings?: WebSearchSettings): string {\n\treturn definitionFor(settings).credentialId;\n}\n\n/**\n * The reference stored by the settings panel, or undefined when nothing is saved.\n *\n * Whatever was saved stays a reference rather than being resolved here, so the one\n * resolution below covers a literal key, a `$VAR`, and a `!command` alike. An OAuth\n * credential under the same key is not an API key and is ignored rather than coerced.\n */\nfunction storedApiKeyReference(settings: WebSearchSettings | undefined, authPath?: string): string | undefined {\n\tconst credential = readStoredCredential(webSearchCredentialId(settings), authPath);\n\tif (credential?.type !== \"api_key\") return undefined;\n\treturn credential.key?.trim() || undefined;\n}\n\n/**\n * Reject a literal secret configured in settings.\n *\n * `webSearch.apiKey` is read from project settings as well as global ones, and this\n * repository deliberately does not gitignore `<project>/.apex-code/` -- that directory\n * is meant to be versioned and shared with a team. A literal key written there would be\n * committed and handed to everyone who clones the repository.\n *\n * A reference (`$VAR`, `${VAR}`, `!command`) names where the secret lives without\n * containing it, so it is safe to commit. AGENTS.md already states that no credential\n * belongs in a file the settings loader writes; this makes that rule enforced rather\n * than documented. `auth.json` still accepts literals, because holding secrets outside\n * the repository is what the credential store is for.\n */\nexport function webSearchSettingsError(settings?: WebSearchSettings): string | undefined {\n\tconst configured = settings?.apiKey?.trim();\n\tif (!configured) return undefined;\n\tif (isCommandConfigValue(configured)) return undefined;\n\tif (getConfigValueEnvVarNames(configured).length > 0) return undefined;\n\t// The value itself is never echoed: this message reaches the model, and from there a\n\t// transcript. Naming the setting is enough to act on.\n\treturn `webSearch.apiKey must be a reference, not a literal key, because project settings are versioned and shared. Use \"$EXA_API_KEY\", \"\\${YOUR_VAR}\", or \"!your-command\", and keep the secret in your environment or credential store.`;\n}\n\n/**\n * Pick one reference, then resolve it once.\n *\n * Order is explicit configuration, then saved credential, then ambient environment.\n * A user who just typed a key into the settings dialog expects it to beat a stale\n * export they have forgotten about, and an explicit `webSearch.apiKey` in settings is\n * a direct instruction that should beat both.\n */\nfunction apiKeyReferenceFor(settings: WebSearchSettings | undefined, authPath?: string): string {\n\t// A refused literal falls through rather than being honoured, so a committed key is\n\t// inert instead of quietly working for whoever cloned the repository.\n\tconst configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();\n\treturn configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;\n}\n\n/**\n * Build the configured `web_search` backend, or undefined when no credential\n * resolves.\n *\n * Undefined leaves the tool on its unconfigured operations, which throw a\n * model-readable \"not configured\" error. That is deliberately not the same thing as\n * hiding the tool: a session that silently lacks `web_search` cannot tell the model\n * why, and the throw names the missing piece.\n */\n/**\n * What to tell the model, and through it the user, when nothing is configured.\n *\n * Named here rather than in `web-search.ts` because only this module knows which\n * backend was selected and therefore which credential to name. The audience is a\n * person who asked the agent to search something, so it names the two things they\n * can actually do, not the SDK option an embedder would pass.\n */\nexport function unconfiguredWebSearchMessage(settings?: WebSearchSettings): string {\n\tconst definition = definitionFor(settings);\n\tconst variable = definition.defaultApiKeyReference.replace(/^\\$/, \"\");\n\treturn `web_search has no API key configured. Set ${variable} in the environment, or run /settings and add a ${definition.credentialId} key.`;\n}\n\n/**\n * A `web_search` backend that resolves its credential on each call.\n *\n * The session builds its tool registry once, at construction, so operations captured\n * there would pin whatever was configured at startup. A key saved from the settings\n * panel would then appear to save and do nothing until restart, which is worse than\n * having no settings row at all. Resolving per call costs one small file read and\n * keeps `resolveConfigValue`'s cache for command references.\n */\nexport function createDeferredWebSearchOperations(\n\treadSettings: () => WebSearchSettings | undefined,\n\tauthPath?: string,\n): WebSearchOperations {\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tconst settings = readSettings();\n\t\t\t// Reported ahead of the unconfigured message: \"no key\" and \"your key is in the\n\t\t\t// wrong place\" need different fixes.\n\t\t\tconst settingsError = webSearchSettingsError(settings);\n\t\t\tif (settingsError) throw new Error(settingsError);\n\t\t\tconst operations = resolveWebSearchOperations(settings, undefined, authPath);\n\t\t\tif (!operations) throw new Error(unconfiguredWebSearchMessage(settings));\n\t\t\treturn operations.search(query, signal);\n\t\t},\n\t};\n}\n\nexport function resolveWebSearchOperations(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): WebSearchOperations | undefined {\n\tconst apiKey = resolveConfigValue(apiKeyReferenceFor(settings, authPath), env)?.trim();\n\tif (!apiKey) return undefined;\n\treturn definitionFor(settings).create({\n\t\tapiKey,\n\t\t...(settings?.endpoint === undefined ? {} : { endpoint: settings.endpoint }),\n\t\t...(settings?.numResults === undefined ? {} : { numResults: settings.numResults }),\n\t\t...(settings?.snippetMaxCharacters === undefined ? {} : { snippetMaxCharacters: settings.snippetMaxCharacters }),\n\t});\n}\n"]}