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
@@ -13,7 +13,8 @@
13
13
  *
14
14
  * What this does NOT confine, deliberately and recorded rather than implied:
15
15
  * a formatter writing to an absolute path outside the workspace, or reaching
16
- * the network. Those are the OS sandbox's job. Copy plus restricted promotion
16
+ * the network. Nothing in the harness confines either since ADR 0032 removed the
17
+ * OS boundary, so that is the operator's container. Copy plus restricted promotion
17
18
  * confines workspace mutation, nothing wider.
18
19
  *
19
20
  * Promotion refuses any file whose live bytes changed during the run, because
@@ -1 +1 @@
1
- {"version":3,"file":"formatter-lifecycle.js","sourceRoot":"","sources":["../../src/core/formatter-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * VF.5 plus PS.2: the formatter lifecycle. A formatter runs against an\n * isolated copy of the workspace, never the workspace itself, and only the\n * changes matching its declared paths are promoted back. An undeclared write\n * is left behind in the discarded stage directory, so the live workspace never\n * holds bytes the policy did not declare.\n *\n * This replaces post-hoc reporting. The earlier lifecycle ran the formatter in\n * the live workspace and listed undeclared writes afterwards, which is a\n * report, not a boundary: the stray bytes were already on disk. `status` is\n * now `scope-violated` whenever a write fell outside the declared set, and\n * that value can never read as `passed`.\n *\n * What this does NOT confine, deliberately and recorded rather than implied:\n * a formatter writing to an absolute path outside the workspace, or reaching\n * the network. Those are the OS sandbox's job. Copy plus restricted promotion\n * confines workspace mutation, nothing wider.\n *\n * Promotion refuses any file whose live bytes changed during the run, because\n * overwriting there would destroy a concurrent edit by the user. Such a run\n * fails rather than reverting the user's work.\n *\n * The snapshot is a bounded walk (harness directories skipped, per-file and\n * total byte caps). `truncatedSnapshot` is set when coverage was cut short, so\n * an \"unchanged\" verdict can be read with the right suspicion.\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n\tcopyFileSync,\n\ttype Dirent,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { minimatch } from \"minimatch\";\nimport type { AuthorizeConfiguredCommand } from \"./permissions/policy-command.ts\";\nimport { type PolicyRunStatus, runPolicyCommand } from \"./policy-executor.ts\";\nimport type { FormatterPolicy } from \"./policy-loader.ts\";\nimport { preparePathOperation, writePreparedPath } from \"./tools/path-utils.ts\";\nimport type { WorkspaceArtifactRef, WorkspaceArtifactStore } from \"./workspace/artifacts.ts\";\n\n/** Per-file hash cap: larger files compare by size plus prefix hash. */\nconst MAX_HASH_BYTES_PER_FILE = 5 * 1024 * 1024;\n/** Prefix length hashed when a file exceeds the per-file cap. */\nconst LARGE_FILE_PREFIX_BYTES = 64 * 1024;\n/** File-count cap for the before/after walk. */\nconst MAX_SNAPSHOT_FILES = 2_000;\n/** Total bytes hashed across one snapshot. */\nconst MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024;\n/** Directory names never part of a formatter's honest workspace view. */\nconst SKIPPED_DIRECTORIES = new Set([\".git\", \"node_modules\", \".apex-code\", \"sessions\"]);\n\nexport interface FormatterMutationReport {\n\t/** Workspace-relative paths (forward slashes) that differ before vs after. */\n\tchangedPaths: string[];\n\t/** Changed paths the policy did not declare inside its pathScope. */\n\tundeclaredPaths: string[];\n\t/** Changed paths whose real location resolves outside the workspace. */\n\tescapedPaths: string[];\n\t/** True when nothing in the snapshot differs. */\n\tunchanged: boolean;\n\t/** True when the walk hit its caps before covering the whole workspace. */\n\ttruncatedSnapshot: boolean;\n}\n\nexport interface FormatterEvidence {\n\tpolicyId: string;\n\texecutable: string;\n\targv: string[];\n\tcwd: string;\n\tstatus: PolicyRunStatus;\n\tdurationMs: number;\n\texitCode?: number;\n\tsignal?: string;\n\ttruncated: boolean;\n\tartifact?: WorkspaceArtifactRef;\n}\n\nexport interface FormatterRunOutcome {\n\tstatus: PolicyRunStatus;\n\tmutations: FormatterMutationReport;\n\tevidence: FormatterEvidence;\n\trefusalReason?: string;\n}\n\nexport interface FormatterRunOptions {\n\tworkspaceRoot: string;\n\tsignal?: AbortSignal;\n\tartifactStore?: WorkspaceArtifactStore;\n\t/** PS.1. A blocked formatter is refused before the snapshot and before the spawn. */\n\tauthorize?: AuthorizeConfiguredCommand;\n}\n\ninterface ScopeSnapshot {\n\t/** Workspace-relative path -> content fingerprint. */\n\thashes: Map<string, string>;\n\ttruncated: boolean;\n}\n\nfunction matchesAny(relPath: string, patterns: string[]): boolean {\n\treturn patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }));\n}\n\nfunction fingerprint(buffer: Buffer): string {\n\treturn createHash(\"sha256\").update(buffer).digest(\"hex\");\n}\n\nfunction hashFile(absolute: string, size: number): string {\n\tif (size <= MAX_HASH_BYTES_PER_FILE) return fingerprint(readFileSync(absolute));\n\t// Oversized file: fingerprint the prefix and let the size field the\n\t// snapshot caller already compared catch whole-file rewrites.\n\treturn fingerprint(readFileSync(absolute).subarray(0, LARGE_FILE_PREFIX_BYTES));\n}\n\nfunction snapshotScope(root: string, priority: string[], background: string[]): ScopeSnapshot {\n\tconst hashes = new Map<string, string>();\n\tlet truncated = false;\n\tlet budgetFiles = MAX_SNAPSHOT_FILES;\n\tlet budgetBytes = MAX_SNAPSHOT_BYTES;\n\n\tconst walk = (dir: string): void => {\n\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\ttruncated = true;\n\t\t\treturn;\n\t\t}\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn; // unreadable directory: skip, never throw\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (!SKIPPED_DIRECTORIES.has(entry.name)) walk(join(dir, entry.name));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile() && !entry.isSymbolicLink()) continue;\n\t\t\tconst absolute = join(dir, entry.name);\n\t\t\tconst rel = relative(root, absolute).split(sep).join(\"/\");\n\t\t\tif (!matchesAny(rel, priority) && !matchesAny(rel, background)) continue;\n\t\t\tlet size: number;\n\t\t\ttry {\n\t\t\t\tsize = statSync(absolute).size;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgetFiles -= 1;\n\t\t\tif (size > budgetBytes) truncated = true;\n\t\t\ttry {\n\t\t\t\tbudgetBytes -= Math.min(size, MAX_HASH_BYTES_PER_FILE);\n\t\t\t\thashes.set(rel, hashFile(absolute, size));\n\t\t\t} catch {\n\t\t\t\t// presence without a fingerprint would lie; mark it honestly\n\t\t\t\thashes.set(rel, `unreadable:${size}`);\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(resolve(root));\n\treturn { hashes, truncated };\n}\n\nfunction insideWorkspace(root: string, absolute: string): boolean {\n\t// The root itself may sit behind a symlink (macOS /tmp -> /private/tmp):\n\t// compare the changed path's realpath against the root's realpath, or\n\t// every workspace under a linked directory reads as an escape.\n\tlet realRoot = root;\n\ttry {\n\t\trealRoot = realpathSync(root);\n\t} catch {\n\t\t// root vanished between snapshots: compare as-given\n\t}\n\tconst rel = relative(realRoot, absolute);\n\treturn rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n}\n\nfunction diffScope(before: ScopeSnapshot, after: ScopeSnapshot): string[] {\n\tconst changed: string[] = [];\n\tfor (const [relPath, beforeHash] of before.hashes) {\n\t\tif (after.hashes.get(relPath) !== beforeHash) changed.push(relPath);\n\t}\n\tfor (const relPath of after.hashes.keys()) {\n\t\tif (!before.hashes.has(relPath)) changed.push(relPath);\n\t}\n\treturn changed.sort();\n}\n\n/**\n * Patterns from declaredPaths that remain inside pathScope. A pattern is\n * kept when it equals a scope pattern or a scope pattern is a directory\n * prefix of it; minimatch cannot express containment between globs, so\n * prefix containment is the conservative reading.\n */\nfunction intersectPatterns(declared: string[], scope: string[]): string[] {\n\treturn declared.filter((pattern) =>\n\t\tscope.some(\n\t\t\t(scopePattern) =>\n\t\t\t\tpattern === scopePattern ||\n\t\t\t\tpattern.startsWith(scopePattern.endsWith(\"/\") ? scopePattern : `${scopePattern}/`),\n\t\t),\n\t);\n}\n\nfunction emptyMutations(): FormatterMutationReport {\n\treturn { changedPaths: [], undeclaredPaths: [], escapedPaths: [], unchanged: true, truncatedSnapshot: false };\n}\n\n/** A formatter that never ran. Nothing was spawned, so the mutation report is empty by construction. */\nfunction refused(policy: FormatterPolicy, workspaceRoot: string, reason: string): FormatterRunOutcome {\n\treturn {\n\t\tstatus: \"refused\",\n\t\tmutations: emptyMutations(),\n\t\tevidence: {\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: workspaceRoot,\n\t\t\tstatus: \"refused\",\n\t\t\tdurationMs: 0,\n\t\t\ttruncated: false,\n\t\t},\n\t\trefusalReason: reason,\n\t};\n}\n\n/**\n * Copy the workspace into a private stage directory the formatter runs in.\n * Regular files only: a symlink is not reproduced, so a formatter cannot reach\n * a link's target through the stage, and the live link is never followed on\n * promotion either.\n */\nfunction materializeStage(root: string): { stageRoot: string; truncated: boolean } {\n\tconst stageRoot = mkdtempSync(join(tmpdir(), \"apex-formatter-stage-\"));\n\tlet truncated = false;\n\tlet budgetFiles = MAX_SNAPSHOT_FILES;\n\tlet budgetBytes = MAX_SNAPSHOT_BYTES;\n\n\tconst walk = (dir: string): void => {\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst absolute = join(dir, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (SKIPPED_DIRECTORIES.has(entry.name)) continue;\n\t\t\t\tmkdirSync(join(stageRoot, relative(root, absolute)), { recursive: true });\n\t\t\t\twalk(absolute);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile()) continue;\n\t\t\tlet size: number;\n\t\t\ttry {\n\t\t\t\tsize = statSync(absolute).size;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgetFiles -= 1;\n\t\t\tbudgetBytes -= size;\n\t\t\ttry {\n\t\t\t\tcopyFileSync(absolute, join(stageRoot, relative(root, absolute)));\n\t\t\t} catch {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(root);\n\treturn { stageRoot, truncated };\n}\n\ninterface PromotionResult {\n\tpromoted: string[];\n\t/** Declared changes refused because the live file changed during the run. */\n\tconflicted: string[];\n}\n\n/**\n * Write the declared stage changes back into the live workspace through the\n * canonical no-follow write path (CA.2), so a symlink planted at the live\n * target is never followed. A file whose live bytes no longer match the\n * pre-run fingerprint belongs to a concurrent editor and is refused.\n */\nfunction promoteDeclaredChanges(\n\tworkspaceRoot: string,\n\tstageRoot: string,\n\tdeclaredChanges: string[],\n\tpreRun: Map<string, string>,\n): PromotionResult {\n\tconst promoted: string[] = [];\n\tconst conflicted: string[] = [];\n\tfor (const relPath of declaredChanges) {\n\t\tconst livePath = join(workspaceRoot, ...relPath.split(\"/\"));\n\t\tconst stagePath = join(stageRoot, ...relPath.split(\"/\"));\n\t\tlet liveHash: string | undefined;\n\t\ttry {\n\t\t\tliveHash = hashFile(livePath, statSync(livePath).size);\n\t\t} catch {\n\t\t\tliveHash = undefined;\n\t\t}\n\t\tif (liveHash !== preRun.get(relPath)) {\n\t\t\tconflicted.push(relPath);\n\t\t\tcontinue;\n\t\t}\n\t\tlet content: Buffer;\n\t\ttry {\n\t\t\tcontent = readFileSync(stagePath);\n\t\t} catch {\n\t\t\tconflicted.push(relPath);\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tmkdirSync(dirname(livePath), { recursive: true });\n\t\t\tconst prepared = preparePathOperation(livePath, workspaceRoot);\n\t\t\t// A symlink at the live target canonicalizes to whatever it points at.\n\t\t\t// Promotion writes only inside the workspace, so a target that resolves\n\t\t\t// out is refused rather than followed.\n\t\t\tif (!insideWorkspace(workspaceRoot, prepared.path.value)) {\n\t\t\t\tconflicted.push(relPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twritePreparedPath(prepared, content);\n\t\t\tpromoted.push(relPath);\n\t\t} catch {\n\t\t\tconflicted.push(relPath);\n\t\t}\n\t}\n\treturn { promoted, conflicted };\n}\n\nexport async function runFormatterCommand(\n\tpolicy: FormatterPolicy,\n\toptions: FormatterRunOptions,\n): Promise<FormatterRunOutcome> {\n\tconst workspaceRoot = resolve(options.workspaceRoot);\n\n\t// The loader rejects traversal and absolute scope patterns; this is the\n\t// defensive second gate, because the worst outcome here is running a\n\t// mutator whose declared scope points outside the workspace.\n\tconst scopePatterns = [...policy.declaredPaths, ...(policy.pathScope ?? [])];\n\tconst offending = scopePatterns.find((pattern) => pattern.includes(\"..\") || isAbsolute(pattern));\n\tif (offending !== undefined) {\n\t\treturn refused(\n\t\t\tpolicy,\n\t\t\tworkspaceRoot,\n\t\t\t`declared scope ${JSON.stringify(offending)} must stay inside the workspace`,\n\t\t);\n\t}\n\n\tconst tracked =\n\t\tpolicy.pathScope === undefined ? policy.declaredPaths : intersectPatterns(policy.declaredPaths, policy.pathScope);\n\n\tif (options.authorize !== undefined) {\n\t\tconst decision = await options.authorize({\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: workspaceRoot,\n\t\t\twriteScope: tracked,\n\t\t\tcapabilities: new Set([\"exec\", \"fs.write\"]),\n\t\t\tpermission: policy.permission,\n\t\t});\n\t\tif (decision.block) return refused(policy, workspaceRoot, decision.reason ?? \"not permitted\");\n\t}\n\n\t// Background coverage is everything: an unexpected mutation lives\n\t// outside every declared pattern, so only a whole-workspace view can\n\t// report it. Caps bound the walk; truncatedSnapshot carries the doubt.\n\tconst preRun = snapshotScope(workspaceRoot, tracked, [\"**\"]);\n\tconst stage = materializeStage(workspaceRoot);\n\tconst before = snapshotScope(stage.stageRoot, tracked, [\"**\"]);\n\tconst run = await runPolicyCommand(policy, {\n\t\tworkspaceRoot: stage.stageRoot,\n\t\tsignal: options.signal,\n\t\tartifactStore: options.artifactStore,\n\t});\n\tconst after = snapshotScope(stage.stageRoot, tracked, [\"**\"]);\n\n\tconst changedPaths = diffScope(before, after);\n\tconst undeclaredPaths: string[] = [];\n\tconst escapedPaths: string[] = [];\n\tfor (const relPath of changedPaths) {\n\t\tif (!matchesAny(relPath, tracked)) undeclaredPaths.push(relPath);\n\t\tconst absolute = join(stage.stageRoot, ...relPath.split(\"/\"));\n\t\ttry {\n\t\t\tconst real = realpathSync(absolute);\n\t\t\tif (!insideWorkspace(stage.stageRoot, real)) escapedPaths.push(relPath);\n\t\t} catch {\n\t\t\t// deleted or unresolvable between snapshots: no live escape to flag\n\t\t}\n\t}\n\n\tconst declaredChanges = changedPaths.filter(\n\t\t(relPath) => matchesAny(relPath, tracked) && !escapedPaths.includes(relPath),\n\t);\n\tconst promotion = promoteDeclaredChanges(workspaceRoot, stage.stageRoot, declaredChanges, preRun.hashes);\n\trmSync(stage.stageRoot, { recursive: true, force: true });\n\n\tconst outOfScope = undeclaredPaths.length > 0 || escapedPaths.length > 0;\n\tconst status: PolicyRunStatus =\n\t\trun.status !== \"passed\"\n\t\t\t? run.status\n\t\t\t: outOfScope\n\t\t\t\t? \"scope-violated\"\n\t\t\t\t: promotion.conflicted.length > 0\n\t\t\t\t\t? \"failed\"\n\t\t\t\t\t: \"passed\";\n\n\treturn {\n\t\tstatus,\n\t\tmutations: {\n\t\t\tchangedPaths,\n\t\t\tundeclaredPaths,\n\t\t\tescapedPaths,\n\t\t\tunchanged: changedPaths.length === 0,\n\t\t\ttruncatedSnapshot: preRun.truncated || before.truncated || after.truncated || stage.truncated,\n\t\t},\n\t\tevidence: {\n\t\t\tpolicyId: run.policyId,\n\t\t\texecutable: run.executable,\n\t\t\targv: run.argv,\n\t\t\tcwd: run.cwd,\n\t\t\tstatus,\n\t\t\tdurationMs: run.durationMs,\n\t\t\texitCode: run.exitCode,\n\t\t\tsignal: run.signal,\n\t\t\ttruncated: run.truncated,\n\t\t\tartifact: run.artifact,\n\t\t},\n\t\trefusalReason: run.refusalReason,\n\t};\n}\n"]}
1
+ {"version":3,"file":"formatter-lifecycle.js","sourceRoot":"","sources":["../../src/core/formatter-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * VF.5 plus PS.2: the formatter lifecycle. A formatter runs against an\n * isolated copy of the workspace, never the workspace itself, and only the\n * changes matching its declared paths are promoted back. An undeclared write\n * is left behind in the discarded stage directory, so the live workspace never\n * holds bytes the policy did not declare.\n *\n * This replaces post-hoc reporting. The earlier lifecycle ran the formatter in\n * the live workspace and listed undeclared writes afterwards, which is a\n * report, not a boundary: the stray bytes were already on disk. `status` is\n * now `scope-violated` whenever a write fell outside the declared set, and\n * that value can never read as `passed`.\n *\n * What this does NOT confine, deliberately and recorded rather than implied:\n * a formatter writing to an absolute path outside the workspace, or reaching\n * the network. Nothing in the harness confines either since ADR 0032 removed the\n * OS boundary, so that is the operator's container. Copy plus restricted promotion\n * confines workspace mutation, nothing wider.\n *\n * Promotion refuses any file whose live bytes changed during the run, because\n * overwriting there would destroy a concurrent edit by the user. Such a run\n * fails rather than reverting the user's work.\n *\n * The snapshot is a bounded walk (harness directories skipped, per-file and\n * total byte caps). `truncatedSnapshot` is set when coverage was cut short, so\n * an \"unchanged\" verdict can be read with the right suspicion.\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n\tcopyFileSync,\n\ttype Dirent,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { minimatch } from \"minimatch\";\nimport type { AuthorizeConfiguredCommand } from \"./permissions/policy-command.ts\";\nimport { type PolicyRunStatus, runPolicyCommand } from \"./policy-executor.ts\";\nimport type { FormatterPolicy } from \"./policy-loader.ts\";\nimport { preparePathOperation, writePreparedPath } from \"./tools/path-utils.ts\";\nimport type { WorkspaceArtifactRef, WorkspaceArtifactStore } from \"./workspace/artifacts.ts\";\n\n/** Per-file hash cap: larger files compare by size plus prefix hash. */\nconst MAX_HASH_BYTES_PER_FILE = 5 * 1024 * 1024;\n/** Prefix length hashed when a file exceeds the per-file cap. */\nconst LARGE_FILE_PREFIX_BYTES = 64 * 1024;\n/** File-count cap for the before/after walk. */\nconst MAX_SNAPSHOT_FILES = 2_000;\n/** Total bytes hashed across one snapshot. */\nconst MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024;\n/** Directory names never part of a formatter's honest workspace view. */\nconst SKIPPED_DIRECTORIES = new Set([\".git\", \"node_modules\", \".apex-code\", \"sessions\"]);\n\nexport interface FormatterMutationReport {\n\t/** Workspace-relative paths (forward slashes) that differ before vs after. */\n\tchangedPaths: string[];\n\t/** Changed paths the policy did not declare inside its pathScope. */\n\tundeclaredPaths: string[];\n\t/** Changed paths whose real location resolves outside the workspace. */\n\tescapedPaths: string[];\n\t/** True when nothing in the snapshot differs. */\n\tunchanged: boolean;\n\t/** True when the walk hit its caps before covering the whole workspace. */\n\ttruncatedSnapshot: boolean;\n}\n\nexport interface FormatterEvidence {\n\tpolicyId: string;\n\texecutable: string;\n\targv: string[];\n\tcwd: string;\n\tstatus: PolicyRunStatus;\n\tdurationMs: number;\n\texitCode?: number;\n\tsignal?: string;\n\ttruncated: boolean;\n\tartifact?: WorkspaceArtifactRef;\n}\n\nexport interface FormatterRunOutcome {\n\tstatus: PolicyRunStatus;\n\tmutations: FormatterMutationReport;\n\tevidence: FormatterEvidence;\n\trefusalReason?: string;\n}\n\nexport interface FormatterRunOptions {\n\tworkspaceRoot: string;\n\tsignal?: AbortSignal;\n\tartifactStore?: WorkspaceArtifactStore;\n\t/** PS.1. A blocked formatter is refused before the snapshot and before the spawn. */\n\tauthorize?: AuthorizeConfiguredCommand;\n}\n\ninterface ScopeSnapshot {\n\t/** Workspace-relative path -> content fingerprint. */\n\thashes: Map<string, string>;\n\ttruncated: boolean;\n}\n\nfunction matchesAny(relPath: string, patterns: string[]): boolean {\n\treturn patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }));\n}\n\nfunction fingerprint(buffer: Buffer): string {\n\treturn createHash(\"sha256\").update(buffer).digest(\"hex\");\n}\n\nfunction hashFile(absolute: string, size: number): string {\n\tif (size <= MAX_HASH_BYTES_PER_FILE) return fingerprint(readFileSync(absolute));\n\t// Oversized file: fingerprint the prefix and let the size field the\n\t// snapshot caller already compared catch whole-file rewrites.\n\treturn fingerprint(readFileSync(absolute).subarray(0, LARGE_FILE_PREFIX_BYTES));\n}\n\nfunction snapshotScope(root: string, priority: string[], background: string[]): ScopeSnapshot {\n\tconst hashes = new Map<string, string>();\n\tlet truncated = false;\n\tlet budgetFiles = MAX_SNAPSHOT_FILES;\n\tlet budgetBytes = MAX_SNAPSHOT_BYTES;\n\n\tconst walk = (dir: string): void => {\n\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\ttruncated = true;\n\t\t\treturn;\n\t\t}\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn; // unreadable directory: skip, never throw\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (!SKIPPED_DIRECTORIES.has(entry.name)) walk(join(dir, entry.name));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile() && !entry.isSymbolicLink()) continue;\n\t\t\tconst absolute = join(dir, entry.name);\n\t\t\tconst rel = relative(root, absolute).split(sep).join(\"/\");\n\t\t\tif (!matchesAny(rel, priority) && !matchesAny(rel, background)) continue;\n\t\t\tlet size: number;\n\t\t\ttry {\n\t\t\t\tsize = statSync(absolute).size;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgetFiles -= 1;\n\t\t\tif (size > budgetBytes) truncated = true;\n\t\t\ttry {\n\t\t\t\tbudgetBytes -= Math.min(size, MAX_HASH_BYTES_PER_FILE);\n\t\t\t\thashes.set(rel, hashFile(absolute, size));\n\t\t\t} catch {\n\t\t\t\t// presence without a fingerprint would lie; mark it honestly\n\t\t\t\thashes.set(rel, `unreadable:${size}`);\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(resolve(root));\n\treturn { hashes, truncated };\n}\n\nfunction insideWorkspace(root: string, absolute: string): boolean {\n\t// The root itself may sit behind a symlink (macOS /tmp -> /private/tmp):\n\t// compare the changed path's realpath against the root's realpath, or\n\t// every workspace under a linked directory reads as an escape.\n\tlet realRoot = root;\n\ttry {\n\t\trealRoot = realpathSync(root);\n\t} catch {\n\t\t// root vanished between snapshots: compare as-given\n\t}\n\tconst rel = relative(realRoot, absolute);\n\treturn rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n}\n\nfunction diffScope(before: ScopeSnapshot, after: ScopeSnapshot): string[] {\n\tconst changed: string[] = [];\n\tfor (const [relPath, beforeHash] of before.hashes) {\n\t\tif (after.hashes.get(relPath) !== beforeHash) changed.push(relPath);\n\t}\n\tfor (const relPath of after.hashes.keys()) {\n\t\tif (!before.hashes.has(relPath)) changed.push(relPath);\n\t}\n\treturn changed.sort();\n}\n\n/**\n * Patterns from declaredPaths that remain inside pathScope. A pattern is\n * kept when it equals a scope pattern or a scope pattern is a directory\n * prefix of it; minimatch cannot express containment between globs, so\n * prefix containment is the conservative reading.\n */\nfunction intersectPatterns(declared: string[], scope: string[]): string[] {\n\treturn declared.filter((pattern) =>\n\t\tscope.some(\n\t\t\t(scopePattern) =>\n\t\t\t\tpattern === scopePattern ||\n\t\t\t\tpattern.startsWith(scopePattern.endsWith(\"/\") ? scopePattern : `${scopePattern}/`),\n\t\t),\n\t);\n}\n\nfunction emptyMutations(): FormatterMutationReport {\n\treturn { changedPaths: [], undeclaredPaths: [], escapedPaths: [], unchanged: true, truncatedSnapshot: false };\n}\n\n/** A formatter that never ran. Nothing was spawned, so the mutation report is empty by construction. */\nfunction refused(policy: FormatterPolicy, workspaceRoot: string, reason: string): FormatterRunOutcome {\n\treturn {\n\t\tstatus: \"refused\",\n\t\tmutations: emptyMutations(),\n\t\tevidence: {\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: workspaceRoot,\n\t\t\tstatus: \"refused\",\n\t\t\tdurationMs: 0,\n\t\t\ttruncated: false,\n\t\t},\n\t\trefusalReason: reason,\n\t};\n}\n\n/**\n * Copy the workspace into a private stage directory the formatter runs in.\n * Regular files only: a symlink is not reproduced, so a formatter cannot reach\n * a link's target through the stage, and the live link is never followed on\n * promotion either.\n */\nfunction materializeStage(root: string): { stageRoot: string; truncated: boolean } {\n\tconst stageRoot = mkdtempSync(join(tmpdir(), \"apex-formatter-stage-\"));\n\tlet truncated = false;\n\tlet budgetFiles = MAX_SNAPSHOT_FILES;\n\tlet budgetBytes = MAX_SNAPSHOT_BYTES;\n\n\tconst walk = (dir: string): void => {\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst absolute = join(dir, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (SKIPPED_DIRECTORIES.has(entry.name)) continue;\n\t\t\t\tmkdirSync(join(stageRoot, relative(root, absolute)), { recursive: true });\n\t\t\t\twalk(absolute);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile()) continue;\n\t\t\tlet size: number;\n\t\t\ttry {\n\t\t\t\tsize = statSync(absolute).size;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgetFiles -= 1;\n\t\t\tbudgetBytes -= size;\n\t\t\ttry {\n\t\t\t\tcopyFileSync(absolute, join(stageRoot, relative(root, absolute)));\n\t\t\t} catch {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(root);\n\treturn { stageRoot, truncated };\n}\n\ninterface PromotionResult {\n\tpromoted: string[];\n\t/** Declared changes refused because the live file changed during the run. */\n\tconflicted: string[];\n}\n\n/**\n * Write the declared stage changes back into the live workspace through the\n * canonical no-follow write path (CA.2), so a symlink planted at the live\n * target is never followed. A file whose live bytes no longer match the\n * pre-run fingerprint belongs to a concurrent editor and is refused.\n */\nfunction promoteDeclaredChanges(\n\tworkspaceRoot: string,\n\tstageRoot: string,\n\tdeclaredChanges: string[],\n\tpreRun: Map<string, string>,\n): PromotionResult {\n\tconst promoted: string[] = [];\n\tconst conflicted: string[] = [];\n\tfor (const relPath of declaredChanges) {\n\t\tconst livePath = join(workspaceRoot, ...relPath.split(\"/\"));\n\t\tconst stagePath = join(stageRoot, ...relPath.split(\"/\"));\n\t\tlet liveHash: string | undefined;\n\t\ttry {\n\t\t\tliveHash = hashFile(livePath, statSync(livePath).size);\n\t\t} catch {\n\t\t\tliveHash = undefined;\n\t\t}\n\t\tif (liveHash !== preRun.get(relPath)) {\n\t\t\tconflicted.push(relPath);\n\t\t\tcontinue;\n\t\t}\n\t\tlet content: Buffer;\n\t\ttry {\n\t\t\tcontent = readFileSync(stagePath);\n\t\t} catch {\n\t\t\tconflicted.push(relPath);\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tmkdirSync(dirname(livePath), { recursive: true });\n\t\t\tconst prepared = preparePathOperation(livePath, workspaceRoot);\n\t\t\t// A symlink at the live target canonicalizes to whatever it points at.\n\t\t\t// Promotion writes only inside the workspace, so a target that resolves\n\t\t\t// out is refused rather than followed.\n\t\t\tif (!insideWorkspace(workspaceRoot, prepared.path.value)) {\n\t\t\t\tconflicted.push(relPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twritePreparedPath(prepared, content);\n\t\t\tpromoted.push(relPath);\n\t\t} catch {\n\t\t\tconflicted.push(relPath);\n\t\t}\n\t}\n\treturn { promoted, conflicted };\n}\n\nexport async function runFormatterCommand(\n\tpolicy: FormatterPolicy,\n\toptions: FormatterRunOptions,\n): Promise<FormatterRunOutcome> {\n\tconst workspaceRoot = resolve(options.workspaceRoot);\n\n\t// The loader rejects traversal and absolute scope patterns; this is the\n\t// defensive second gate, because the worst outcome here is running a\n\t// mutator whose declared scope points outside the workspace.\n\tconst scopePatterns = [...policy.declaredPaths, ...(policy.pathScope ?? [])];\n\tconst offending = scopePatterns.find((pattern) => pattern.includes(\"..\") || isAbsolute(pattern));\n\tif (offending !== undefined) {\n\t\treturn refused(\n\t\t\tpolicy,\n\t\t\tworkspaceRoot,\n\t\t\t`declared scope ${JSON.stringify(offending)} must stay inside the workspace`,\n\t\t);\n\t}\n\n\tconst tracked =\n\t\tpolicy.pathScope === undefined ? policy.declaredPaths : intersectPatterns(policy.declaredPaths, policy.pathScope);\n\n\tif (options.authorize !== undefined) {\n\t\tconst decision = await options.authorize({\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: workspaceRoot,\n\t\t\twriteScope: tracked,\n\t\t\tcapabilities: new Set([\"exec\", \"fs.write\"]),\n\t\t\tpermission: policy.permission,\n\t\t});\n\t\tif (decision.block) return refused(policy, workspaceRoot, decision.reason ?? \"not permitted\");\n\t}\n\n\t// Background coverage is everything: an unexpected mutation lives\n\t// outside every declared pattern, so only a whole-workspace view can\n\t// report it. Caps bound the walk; truncatedSnapshot carries the doubt.\n\tconst preRun = snapshotScope(workspaceRoot, tracked, [\"**\"]);\n\tconst stage = materializeStage(workspaceRoot);\n\tconst before = snapshotScope(stage.stageRoot, tracked, [\"**\"]);\n\tconst run = await runPolicyCommand(policy, {\n\t\tworkspaceRoot: stage.stageRoot,\n\t\tsignal: options.signal,\n\t\tartifactStore: options.artifactStore,\n\t});\n\tconst after = snapshotScope(stage.stageRoot, tracked, [\"**\"]);\n\n\tconst changedPaths = diffScope(before, after);\n\tconst undeclaredPaths: string[] = [];\n\tconst escapedPaths: string[] = [];\n\tfor (const relPath of changedPaths) {\n\t\tif (!matchesAny(relPath, tracked)) undeclaredPaths.push(relPath);\n\t\tconst absolute = join(stage.stageRoot, ...relPath.split(\"/\"));\n\t\ttry {\n\t\t\tconst real = realpathSync(absolute);\n\t\t\tif (!insideWorkspace(stage.stageRoot, real)) escapedPaths.push(relPath);\n\t\t} catch {\n\t\t\t// deleted or unresolvable between snapshots: no live escape to flag\n\t\t}\n\t}\n\n\tconst declaredChanges = changedPaths.filter(\n\t\t(relPath) => matchesAny(relPath, tracked) && !escapedPaths.includes(relPath),\n\t);\n\tconst promotion = promoteDeclaredChanges(workspaceRoot, stage.stageRoot, declaredChanges, preRun.hashes);\n\trmSync(stage.stageRoot, { recursive: true, force: true });\n\n\tconst outOfScope = undeclaredPaths.length > 0 || escapedPaths.length > 0;\n\tconst status: PolicyRunStatus =\n\t\trun.status !== \"passed\"\n\t\t\t? run.status\n\t\t\t: outOfScope\n\t\t\t\t? \"scope-violated\"\n\t\t\t\t: promotion.conflicted.length > 0\n\t\t\t\t\t? \"failed\"\n\t\t\t\t\t: \"passed\";\n\n\treturn {\n\t\tstatus,\n\t\tmutations: {\n\t\t\tchangedPaths,\n\t\t\tundeclaredPaths,\n\t\t\tescapedPaths,\n\t\t\tunchanged: changedPaths.length === 0,\n\t\t\ttruncatedSnapshot: preRun.truncated || before.truncated || after.truncated || stage.truncated,\n\t\t},\n\t\tevidence: {\n\t\t\tpolicyId: run.policyId,\n\t\t\texecutable: run.executable,\n\t\t\targv: run.argv,\n\t\t\tcwd: run.cwd,\n\t\t\tstatus,\n\t\t\tdurationMs: run.durationMs,\n\t\t\texitCode: run.exitCode,\n\t\t\tsignal: run.signal,\n\t\t\ttruncated: run.truncated,\n\t\t\tartifact: run.artifact,\n\t\t},\n\t\trefusalReason: run.refusalReason,\n\t};\n}\n"]}
@@ -6,9 +6,8 @@
6
6
  *
7
7
  * The decision vocabulary is restriction-only: `block` short-circuits, `ask`
8
8
  * defers to the permission gate, and `allow` is recorded but never bypasses
9
- * the gate. A hook can narrow what runs, never widen it -- which is what makes
10
- * trust-gated project-scope hooks safe where project-sandbox profiles are not
11
- * (ADR 0016).
9
+ * the gate. A hook can narrow what runs, never widen it, which is what makes
10
+ * trust-gated project-scope hooks safe to load from a project at all.
12
11
  */
13
12
  export declare const HOOK_EVENT_NAMES: readonly ["tool_call", "tool_result", "session_start", "turn_end", "session_before_compact"];
14
13
  export type HookEventName = (typeof HOOK_EVENT_NAMES)[number];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,gBAAgB,8FAMnB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,SAAS,CAAC;IAChB,8HAA8H;IAC9H,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,OAAO,EAAE,MAAM,CAAC;IAChB,8GAA8G;IAC9G,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0JAA0J;IAC1J,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GAAG,wBAAwB,GAAG,qBAAqB,CAAC;AAEjF,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;AAEhF,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnH,MAAM,WAAW,WAAW;IAC3B,uDAAuD;IACvD,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACzD;AAED,kHAAkH;AAClH,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC3C,qFAAqF;IACrF,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,WAAW,EAAE,CAAC;IAC1D;;;;;OAKG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACpG;;;OAGG;IACH,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED,0FAA0F;AAC1F,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAC9C,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,QAAS,CAAC","sourcesContent":["/**\n * Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path\n * onto the extension event catalog, for operators who will never author a\n * TypeScript extension. Version one maps five events onto command and HTTP\n * handlers, with decisions read only from `tool_call`.\n *\n * The decision vocabulary is restriction-only: `block` short-circuits, `ask`\n * defers to the permission gate, and `allow` is recorded but never bypasses\n * the gate. A hook can narrow what runs, never widen it -- which is what makes\n * trust-gated project-scope hooks safe where project-sandbox profiles are not\n * (ADR 0016).\n */\n\nexport const HOOK_EVENT_NAMES = [\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"session_start\",\n\t\"turn_end\",\n\t\"session_before_compact\",\n] as const;\nexport type HookEventName = (typeof HOOK_EVENT_NAMES)[number];\n\nexport interface HookCommandHandlerConfig {\n\ttype: \"command\";\n\t/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */\n\tmatcher?: string;\n\t/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */\n\tcommand: string;\n\t/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */\n\ttimeoutMs?: number;\n}\n\nexport interface HookHttpHandlerConfig {\n\ttype: \"http\";\n\tmatcher?: string;\n\t/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */\n\turl: string;\n\ttimeoutMs?: number;\n}\n\nexport type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;\n\nexport type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;\n\nexport interface HookDecision {\n\tdecision: \"allow\" | \"block\" | \"ask\";\n\treason?: string;\n}\n\nexport interface HookEventPayload {\n\ttype: HookEventName;\n\ttoolName?: string;\n\ttoolCallId?: string;\n\tinput?: unknown;\n\treason?: string;\n}\n\nexport type HookOutcome = { ok: true; decision?: HookDecision; warning?: string } | { ok: false; warning: string };\n\nexport interface HookHandler {\n\t/** Absent on a handler means it matches every tool. */\n\tmatchesTool?(toolName: string): boolean;\n\texecute(payload: HookEventPayload): Promise<HookOutcome>;\n}\n\n/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */\nexport interface HookToolCallBlock {\n\tblock: true;\n\treason: string;\n}\n\nexport interface HookRuntime {\n\thasHandlers(event: HookEventName): boolean;\n\t/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */\n\thandlersFor(event: HookEventName): readonly HookHandler[];\n\t/**\n\t * `tool_call` decisions, restriction-only: `block` returns a block for the\n\t * seam to consume, every other outcome (allow, ask, no decision) returns\n\t * undefined so the permission gate still evaluates the call. A handler that\n\t * fails or times out fails closed.\n\t */\n\tdecideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;\n\t/**\n\t * Observe-only emission for `tool_result` and lifecycle events: handlers are\n\t * awaited in order, failures are swallowed, and no decision is ever read.\n\t */\n\temitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;\n}\n\n/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Hard cap on configured timeouts. */\nexport const MAX_HOOK_TIMEOUT_MS = 60_000;\n"]}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,gBAAgB,8FAMnB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,SAAS,CAAC;IAChB,8HAA8H;IAC9H,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,OAAO,EAAE,MAAM,CAAC;IAChB,8GAA8G;IAC9G,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0JAA0J;IAC1J,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GAAG,wBAAwB,GAAG,qBAAqB,CAAC;AAEjF,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;AAEhF,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnH,MAAM,WAAW,WAAW;IAC3B,uDAAuD;IACvD,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACzD;AAED,kHAAkH;AAClH,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC3C,qFAAqF;IACrF,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,WAAW,EAAE,CAAC;IAC1D;;;;;OAKG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACpG;;;OAGG;IACH,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED,0FAA0F;AAC1F,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAC9C,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,QAAS,CAAC","sourcesContent":["/**\n * Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path\n * onto the extension event catalog, for operators who will never author a\n * TypeScript extension. Version one maps five events onto command and HTTP\n * handlers, with decisions read only from `tool_call`.\n *\n * The decision vocabulary is restriction-only: `block` short-circuits, `ask`\n * defers to the permission gate, and `allow` is recorded but never bypasses\n * the gate. A hook can narrow what runs, never widen it, which is what makes\n * trust-gated project-scope hooks safe to load from a project at all.\n */\n\nexport const HOOK_EVENT_NAMES = [\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"session_start\",\n\t\"turn_end\",\n\t\"session_before_compact\",\n] as const;\nexport type HookEventName = (typeof HOOK_EVENT_NAMES)[number];\n\nexport interface HookCommandHandlerConfig {\n\ttype: \"command\";\n\t/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */\n\tmatcher?: string;\n\t/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */\n\tcommand: string;\n\t/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */\n\ttimeoutMs?: number;\n}\n\nexport interface HookHttpHandlerConfig {\n\ttype: \"http\";\n\tmatcher?: string;\n\t/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */\n\turl: string;\n\ttimeoutMs?: number;\n}\n\nexport type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;\n\nexport type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;\n\nexport interface HookDecision {\n\tdecision: \"allow\" | \"block\" | \"ask\";\n\treason?: string;\n}\n\nexport interface HookEventPayload {\n\ttype: HookEventName;\n\ttoolName?: string;\n\ttoolCallId?: string;\n\tinput?: unknown;\n\treason?: string;\n}\n\nexport type HookOutcome = { ok: true; decision?: HookDecision; warning?: string } | { ok: false; warning: string };\n\nexport interface HookHandler {\n\t/** Absent on a handler means it matches every tool. */\n\tmatchesTool?(toolName: string): boolean;\n\texecute(payload: HookEventPayload): Promise<HookOutcome>;\n}\n\n/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */\nexport interface HookToolCallBlock {\n\tblock: true;\n\treason: string;\n}\n\nexport interface HookRuntime {\n\thasHandlers(event: HookEventName): boolean;\n\t/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */\n\thandlersFor(event: HookEventName): readonly HookHandler[];\n\t/**\n\t * `tool_call` decisions, restriction-only: `block` returns a block for the\n\t * seam to consume, every other outcome (allow, ask, no decision) returns\n\t * undefined so the permission gate still evaluates the call. A handler that\n\t * fails or times out fails closed.\n\t */\n\tdecideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;\n\t/**\n\t * Observe-only emission for `tool_result` and lifecycle events: handlers are\n\t * awaited in order, failures are swallowed, and no decision is ever read.\n\t */\n\temitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;\n}\n\n/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Hard cap on configured timeouts. */\nexport const MAX_HOOK_TIMEOUT_MS = 60_000;\n"]}
@@ -6,9 +6,8 @@
6
6
  *
7
7
  * The decision vocabulary is restriction-only: `block` short-circuits, `ask`
8
8
  * defers to the permission gate, and `allow` is recorded but never bypasses
9
- * the gate. A hook can narrow what runs, never widen it -- which is what makes
10
- * trust-gated project-scope hooks safe where project-sandbox profiles are not
11
- * (ADR 0016).
9
+ * the gate. A hook can narrow what runs, never widen it, which is what makes
10
+ * trust-gated project-scope hooks safe to load from a project at all.
12
11
  */
13
12
  export const HOOK_EVENT_NAMES = [
14
13
  "tool_call",
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,WAAW;IACX,aAAa;IACb,eAAe;IACf,UAAU;IACV,wBAAwB;CACf,CAAC;AAsEX,0FAA0F;AAC1F,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAC9C,uCAAuC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC","sourcesContent":["/**\n * Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path\n * onto the extension event catalog, for operators who will never author a\n * TypeScript extension. Version one maps five events onto command and HTTP\n * handlers, with decisions read only from `tool_call`.\n *\n * The decision vocabulary is restriction-only: `block` short-circuits, `ask`\n * defers to the permission gate, and `allow` is recorded but never bypasses\n * the gate. A hook can narrow what runs, never widen it -- which is what makes\n * trust-gated project-scope hooks safe where project-sandbox profiles are not\n * (ADR 0016).\n */\n\nexport const HOOK_EVENT_NAMES = [\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"session_start\",\n\t\"turn_end\",\n\t\"session_before_compact\",\n] as const;\nexport type HookEventName = (typeof HOOK_EVENT_NAMES)[number];\n\nexport interface HookCommandHandlerConfig {\n\ttype: \"command\";\n\t/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */\n\tmatcher?: string;\n\t/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */\n\tcommand: string;\n\t/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */\n\ttimeoutMs?: number;\n}\n\nexport interface HookHttpHandlerConfig {\n\ttype: \"http\";\n\tmatcher?: string;\n\t/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */\n\turl: string;\n\ttimeoutMs?: number;\n}\n\nexport type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;\n\nexport type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;\n\nexport interface HookDecision {\n\tdecision: \"allow\" | \"block\" | \"ask\";\n\treason?: string;\n}\n\nexport interface HookEventPayload {\n\ttype: HookEventName;\n\ttoolName?: string;\n\ttoolCallId?: string;\n\tinput?: unknown;\n\treason?: string;\n}\n\nexport type HookOutcome = { ok: true; decision?: HookDecision; warning?: string } | { ok: false; warning: string };\n\nexport interface HookHandler {\n\t/** Absent on a handler means it matches every tool. */\n\tmatchesTool?(toolName: string): boolean;\n\texecute(payload: HookEventPayload): Promise<HookOutcome>;\n}\n\n/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */\nexport interface HookToolCallBlock {\n\tblock: true;\n\treason: string;\n}\n\nexport interface HookRuntime {\n\thasHandlers(event: HookEventName): boolean;\n\t/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */\n\thandlersFor(event: HookEventName): readonly HookHandler[];\n\t/**\n\t * `tool_call` decisions, restriction-only: `block` returns a block for the\n\t * seam to consume, every other outcome (allow, ask, no decision) returns\n\t * undefined so the permission gate still evaluates the call. A handler that\n\t * fails or times out fails closed.\n\t */\n\tdecideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;\n\t/**\n\t * Observe-only emission for `tool_result` and lifecycle events: handlers are\n\t * awaited in order, failures are swallowed, and no decision is ever read.\n\t */\n\temitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;\n}\n\n/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Hard cap on configured timeouts. */\nexport const MAX_HOOK_TIMEOUT_MS = 60_000;\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,WAAW;IACX,aAAa;IACb,eAAe;IACf,UAAU;IACV,wBAAwB;CACf,CAAC;AAsEX,0FAA0F;AAC1F,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAC9C,uCAAuC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC","sourcesContent":["/**\n * Declarative hooks (spec 2026-08-31-declarative-hooks): a settings-file path\n * onto the extension event catalog, for operators who will never author a\n * TypeScript extension. Version one maps five events onto command and HTTP\n * handlers, with decisions read only from `tool_call`.\n *\n * The decision vocabulary is restriction-only: `block` short-circuits, `ask`\n * defers to the permission gate, and `allow` is recorded but never bypasses\n * the gate. A hook can narrow what runs, never widen it, which is what makes\n * trust-gated project-scope hooks safe to load from a project at all.\n */\n\nexport const HOOK_EVENT_NAMES = [\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"session_start\",\n\t\"turn_end\",\n\t\"session_before_compact\",\n] as const;\nexport type HookEventName = (typeof HOOK_EVENT_NAMES)[number];\n\nexport interface HookCommandHandlerConfig {\n\ttype: \"command\";\n\t/** Tool-name exact or `|`/`,`-separated list; absent matches every tool. Only consulted for events that carry a tool name. */\n\tmatcher?: string;\n\t/** Shell command; the event payload arrives as JSON on stdin, the decision as JSON on stdout. */\n\tcommand: string;\n\t/** Default 10s (DEFAULT_HOOK_TIMEOUT_MS). A handler that outlives its timeout fails closed on `tool_call`. */\n\ttimeoutMs?: number;\n}\n\nexport interface HookHttpHandlerConfig {\n\ttype: \"http\";\n\tmatcher?: string;\n\t/** The event payload is POSTed here; the decision is read from the 200 response body. URLs come from settings only -- never environment interpolation. */\n\turl: string;\n\ttimeoutMs?: number;\n}\n\nexport type HookHandlerConfig = HookCommandHandlerConfig | HookHttpHandlerConfig;\n\nexport type HooksSettings = Partial<Record<HookEventName, HookHandlerConfig[]>>;\n\nexport interface HookDecision {\n\tdecision: \"allow\" | \"block\" | \"ask\";\n\treason?: string;\n}\n\nexport interface HookEventPayload {\n\ttype: HookEventName;\n\ttoolName?: string;\n\ttoolCallId?: string;\n\tinput?: unknown;\n\treason?: string;\n}\n\nexport type HookOutcome = { ok: true; decision?: HookDecision; warning?: string } | { ok: false; warning: string };\n\nexport interface HookHandler {\n\t/** Absent on a handler means it matches every tool. */\n\tmatchesTool?(toolName: string): boolean;\n\texecute(payload: HookEventPayload): Promise<HookOutcome>;\n}\n\n/** What a `tool_call` hook hands back to the session: the same shape the beforeToolCall seam already consumes. */\nexport interface HookToolCallBlock {\n\tblock: true;\n\treason: string;\n}\n\nexport interface HookRuntime {\n\thasHandlers(event: HookEventName): boolean;\n\t/** Test and diagnostic accessor; the runtime never mutates the handlers it holds. */\n\thandlersFor(event: HookEventName): readonly HookHandler[];\n\t/**\n\t * `tool_call` decisions, restriction-only: `block` returns a block for the\n\t * seam to consume, every other outcome (allow, ask, no decision) returns\n\t * undefined so the permission gate still evaluates the call. A handler that\n\t * fails or times out fails closed.\n\t */\n\tdecideToolCall(toolName: string, payload: HookEventPayload): Promise<HookToolCallBlock | undefined>;\n\t/**\n\t * Observe-only emission for `tool_result` and lifecycle events: handlers are\n\t * awaited in order, failures are swallowed, and no decision is ever read.\n\t */\n\temitObserve(event: HookEventName, payload: HookEventPayload): Promise<void>;\n}\n\n/** Default per-handler timeout. A hung formatter must not stall the loop indefinitely. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Hard cap on configured timeouts. */\nexport const MAX_HOOK_TIMEOUT_MS = 60_000;\n"]}
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on
3
- * purpose: the dispatcher module loads `undici`, and `settings-manager` -- which every
4
- * supervisor launch imports to resolve the network allowlist before the child exists
5
- * (see `scripts/measure-supervisor-imports.mjs`) -- only needs these two definitions.
6
- * Keeping them here means parsing a settings file never pays for an HTTP stack, while
7
- * the dispatcher remains the single place that consumes the timeout.
3
+ * purpose: the dispatcher module loads `undici`, and `settings-manager` only needs these
4
+ * two definitions. Keeping them here means parsing a settings file never pays for an HTTP
5
+ * stack, while the dispatcher remains the single place that consumes the timeout.
8
6
  */
9
7
  /** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */
10
8
  export declare const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300000;
@@ -1 +1 @@
1
- {"version":3,"file":"http-idle-timeout.d.ts","sourceRoot":"","sources":["../../src/core/http-idle-timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uFAAuF;AACvF,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAEpD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAgBzE","sourcesContent":["/**\n * The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on\n * purpose: the dispatcher module loads `undici`, and `settings-manager` -- which every\n * supervisor launch imports to resolve the network allowlist before the child exists\n * (see `scripts/measure-supervisor-imports.mjs`) -- only needs these two definitions.\n * Keeping them here means parsing a settings file never pays for an HTTP stack, while\n * the dispatcher remains the single place that consumes the timeout.\n */\n\n/** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n"]}
1
+ {"version":3,"file":"http-idle-timeout.d.ts","sourceRoot":"","sources":["../../src/core/http-idle-timeout.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,uFAAuF;AACvF,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAEpD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAgBzE","sourcesContent":["/**\n * The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on\n * purpose: the dispatcher module loads `undici`, and `settings-manager` only needs these\n * two definitions. Keeping them here means parsing a settings file never pays for an HTTP\n * stack, while the dispatcher remains the single place that consumes the timeout.\n */\n\n/** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n"]}
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on
3
- * purpose: the dispatcher module loads `undici`, and `settings-manager` -- which every
4
- * supervisor launch imports to resolve the network allowlist before the child exists
5
- * (see `scripts/measure-supervisor-imports.mjs`) -- only needs these two definitions.
6
- * Keeping them here means parsing a settings file never pays for an HTTP stack, while
7
- * the dispatcher remains the single place that consumes the timeout.
3
+ * purpose: the dispatcher module loads `undici`, and `settings-manager` only needs these
4
+ * two definitions. Keeping them here means parsing a settings file never pays for an HTTP
5
+ * stack, while the dispatcher remains the single place that consumes the timeout.
8
6
  */
9
7
  /** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */
10
8
  export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
@@ -1 +1 @@
1
- {"version":3,"file":"http-idle-timeout.js","sourceRoot":"","sources":["../../src/core/http-idle-timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uFAAuF;AACvF,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAsB;IAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC;YAC1C,OAAO,CAAC,CAAC;QACV,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,OAAO,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAAA,CACzB","sourcesContent":["/**\n * The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on\n * purpose: the dispatcher module loads `undici`, and `settings-manager` -- which every\n * supervisor launch imports to resolve the network allowlist before the child exists\n * (see `scripts/measure-supervisor-imports.mjs`) -- only needs these two definitions.\n * Keeping them here means parsing a settings file never pays for an HTTP stack, while\n * the dispatcher remains the single place that consumes the timeout.\n */\n\n/** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n"]}
1
+ {"version":3,"file":"http-idle-timeout.js","sourceRoot":"","sources":["../../src/core/http-idle-timeout.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,uFAAuF;AACvF,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAsB;IAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC;YAC1C,OAAO,CAAC,CAAC;QACV,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,OAAO,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAAA,CACzB","sourcesContent":["/**\n * The idle-timeout setting's default and parser, isolated from `http-dispatcher.ts` on\n * purpose: the dispatcher module loads `undici`, and `settings-manager` only needs these\n * two definitions. Keeping them here means parsing a settings file never pays for an HTTP\n * stack, while the dispatcher remains the single place that consumes the timeout.\n */\n\n/** Five minutes; Node's own default (no timeout) leaves stale pools open for hours. */\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value === \"string\") {\n\t\tconst trimmed = value.trim();\n\t\tif (trimmed.toLowerCase() === \"disabled\") {\n\t\t\treturn 0;\n\t\t}\n\t\tif (trimmed.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn parseHttpIdleTimeoutMs(Number(trimmed));\n\t}\n\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n"]}
@@ -8,7 +8,24 @@
8
8
  */
9
9
  import { type McpConfig } from "./types.ts";
10
10
  export declare const PROJECT_CONFIG_FILENAME = ".mcp.json";
11
- export declare function globalMcpConfigPath(): string;
11
+ /**
12
+ * The user-scope `mcp.json`, which sits beside the agent directory rather than inside it.
13
+ *
14
+ * Derived from `agentDir` and not from `homedir()`, so that injecting an agent directory
15
+ * isolates MCP config the way it already isolates settings, auth, and sessions. Reading
16
+ * the home directory directly made this the one user-scope resolver that ignored the
17
+ * injection seam, which is why suites asserting "no server is configured" picked up the
18
+ * developer's real servers and failed. For the default agent directory the resolved path
19
+ * is unchanged.
20
+ */
21
+ export declare function globalMcpConfigPath(agentDir?: string): string;
22
+ /**
23
+ * Both sources are explicit, and an omitted one means "no config from that scope" rather
24
+ * than "go and find the real one". Defaulting `globalPath` to the user's home made every
25
+ * caller that forgot it read the developer's own servers, which is how suites asserting
26
+ * an empty registry started failing on a configured machine. A boundary resolves the
27
+ * path; this does not reach for one.
28
+ */
12
29
  export declare function loadMcpConfig(options?: {
13
30
  projectPath?: string;
14
31
  globalPath?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,EAGN,KAAK,SAAS,EAKd,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAEnD,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAgJD,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAYpG","sourcesContent":["/**\n * Reads the ecosystem-standard `{ \"mcpServers\": { … } }` shape from a project\n * `.mcp.json` and a global `mcp.json`, project winning per server name.\n *\n * Every failure degrades. A malformed file yields no servers and one diagnostic; a\n * malformed entry drops itself and leaves its siblings intact. A config file is the\n * first thing a user hand-edits, so a parse error must never take the session down.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../../config.ts\";\nimport { ALL_CAPABILITIES, type Capability } from \"../tools/contract.ts\";\nimport {\n\tDEFAULT_IDLE_TIMEOUT_MINUTES,\n\tDEFAULT_LIFECYCLE,\n\ttype McpConfig,\n\ttype McpConfigDiagnostic,\n\ttype McpLifecycle,\n\ttype McpServerConfig,\n\ttype McpTransport,\n} from \"./types.ts\";\n\nconst LIFECYCLES: readonly McpLifecycle[] = [\"lazy\", \"eager\", \"keep-alive\", \"lazy-keep-alive\"];\n\nexport const PROJECT_CONFIG_FILENAME = \".mcp.json\";\n\nexport function globalMcpConfigPath(): string {\n\treturn join(homedir(), CONFIG_DIR_NAME, \"mcp.json\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n\tif (!isRecord(value)) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn out;\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseTransport(entry: Record<string, unknown>): McpTransport | string {\n\tconst command = typeof entry.command === \"string\" ? entry.command : undefined;\n\tconst url = typeof entry.url === \"string\" ? entry.url : undefined;\n\n\tif (command && url) return \"declares both command and url; exactly one is required\";\n\tif (command) {\n\t\treturn {\n\t\t\tkind: \"stdio\",\n\t\t\tcommand,\n\t\t\targs: stringArray(entry.args),\n\t\t\tenv: stringRecord(entry.env),\n\t\t\tcwd: typeof entry.cwd === \"string\" ? entry.cwd : undefined,\n\t\t};\n\t}\n\tif (url) {\n\t\treturn {\n\t\t\tkind: \"http\",\n\t\t\turl,\n\t\t\theaders: stringRecord(entry.headers),\n\t\t\tbearerTokenEnv: typeof entry.bearerTokenEnv === \"string\" ? entry.bearerTokenEnv : undefined,\n\t\t};\n\t}\n\treturn \"declares neither command nor url\";\n}\n\n/**\n * An absent `capabilities` is the common case, not an edge case: a config pasted from\n * another host carries no Apex-specific field. Those servers get the full set, which\n * keeps them classified (never `UNCLASSIFIED`) without narrowing anything the user\n * did not ask to narrow.\n */\nfunction parseCapabilities(entry: Record<string, unknown>, report: (message: string) => void): ReadonlySet<Capability> {\n\tif (!(\"capabilities\" in entry)) return ALL_CAPABILITIES;\n\tif (!Array.isArray(entry.capabilities)) {\n\t\treport(\"capabilities is not an array; falling back to the full capability set\");\n\t\treturn ALL_CAPABILITIES;\n\t}\n\n\tconst resolved = new Set<Capability>();\n\tconst unknown: string[] = [];\n\tfor (const candidate of entry.capabilities) {\n\t\tif (typeof candidate === \"string\" && ALL_CAPABILITIES.has(candidate as Capability)) {\n\t\t\tresolved.add(candidate as Capability);\n\t\t} else {\n\t\t\tunknown.push(String(candidate));\n\t\t}\n\t}\n\tif (unknown.length > 0) report(`unknown capabilities dropped: ${unknown.join(\", \")}`);\n\treturn resolved;\n}\n\nfunction parseLifecycle(entry: Record<string, unknown>, report: (message: string) => void): McpLifecycle {\n\tconst value = entry.lifecycle;\n\tif (value === undefined) return DEFAULT_LIFECYCLE;\n\tif (typeof value === \"string\" && LIFECYCLES.includes(value as McpLifecycle)) return value as McpLifecycle;\n\treport(`unknown lifecycle ${JSON.stringify(value)}; using ${DEFAULT_LIFECYCLE}`);\n\treturn DEFAULT_LIFECYCLE;\n}\n\nfunction parseIdleTimeout(entry: Record<string, unknown>): number {\n\tconst value = entry.idleTimeout;\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIMEOUT_MINUTES;\n}\n\nfunction parseAuth(\n\tentry: Record<string, unknown>,\n\ttransport: McpTransport,\n\treport: (message: string) => void,\n): \"oauth\" | undefined {\n\tconst value = entry.auth;\n\tif (value === undefined) return undefined;\n\tif (transport.kind !== \"http\") {\n\t\treport(\"auth is only supported on HTTP servers; ignoring\");\n\t\treturn undefined;\n\t}\n\tif (value === \"oauth\") return \"oauth\";\n\treport(`unknown auth ${JSON.stringify(value)}; only \"oauth\" is supported; ignoring`);\n\treturn undefined;\n}\n\nfunction readServers(path: string, diagnostics: McpConfigDiagnostic[]): Map<string, McpServerConfig> {\n\tconst servers = new Map<string, McpServerConfig>();\n\tif (!existsSync(path)) return servers;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t} catch (error) {\n\t\tdiagnostics.push({\n\t\t\tpath,\n\t\t\tserver: undefined,\n\t\t\tmessage: `could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t});\n\t\treturn servers;\n\t}\n\n\tif (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {\n\t\tdiagnostics.push({ path, server: undefined, message: \"has no mcpServers object\" });\n\t\treturn servers;\n\t}\n\n\tfor (const [name, raw] of Object.entries(parsed.mcpServers)) {\n\t\tif (!isRecord(raw)) {\n\t\t\tdiagnostics.push({ path, server: name, message: \"entry is not an object\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst transport = parseTransport(raw);\n\t\tif (typeof transport === \"string\") {\n\t\t\tdiagnostics.push({ path, server: name, message: transport });\n\t\t\tcontinue;\n\t\t}\n\t\tconst report = (message: string) => diagnostics.push({ path, server: name, message });\n\t\tservers.set(name, {\n\t\t\tname,\n\t\t\ttransport,\n\t\t\tauth: parseAuth(raw, transport, report),\n\t\t\tcapabilities: parseCapabilities(raw, report),\n\t\t\tlifecycle: parseLifecycle(raw, report),\n\t\t\tidleTimeoutMinutes: parseIdleTimeout(raw),\n\t\t});\n\t}\n\n\treturn servers;\n}\n\nexport function loadMcpConfig(options: { projectPath?: string; globalPath?: string } = {}): McpConfig {\n\tconst diagnostics: McpConfigDiagnostic[] = [];\n\tconst globalPath = options.globalPath ?? globalMcpConfigPath();\n\n\tconst servers = readServers(globalPath, diagnostics);\n\tif (options.projectPath) {\n\t\tfor (const [name, config] of readServers(options.projectPath, diagnostics)) {\n\t\t\tservers.set(name, config);\n\t\t}\n\t}\n\n\treturn { servers, diagnostics };\n}\n"]}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAGN,KAAK,SAAS,EAKd,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAEnD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE5E;AAgJD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAapG","sourcesContent":["/**\n * Reads the ecosystem-standard `{ \"mcpServers\": { … } }` shape from a project\n * `.mcp.json` and a global `mcp.json`, project winning per server name.\n *\n * Every failure degrades. A malformed file yields no servers and one diagnostic; a\n * malformed entry drops itself and leaves its siblings intact. A config file is the\n * first thing a user hand-edits, so a parse error must never take the session down.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { ALL_CAPABILITIES, type Capability } from \"../tools/contract.ts\";\nimport {\n\tDEFAULT_IDLE_TIMEOUT_MINUTES,\n\tDEFAULT_LIFECYCLE,\n\ttype McpConfig,\n\ttype McpConfigDiagnostic,\n\ttype McpLifecycle,\n\ttype McpServerConfig,\n\ttype McpTransport,\n} from \"./types.ts\";\n\nconst LIFECYCLES: readonly McpLifecycle[] = [\"lazy\", \"eager\", \"keep-alive\", \"lazy-keep-alive\"];\n\nexport const PROJECT_CONFIG_FILENAME = \".mcp.json\";\n\n/**\n * The user-scope `mcp.json`, which sits beside the agent directory rather than inside it.\n *\n * Derived from `agentDir` and not from `homedir()`, so that injecting an agent directory\n * isolates MCP config the way it already isolates settings, auth, and sessions. Reading\n * the home directory directly made this the one user-scope resolver that ignored the\n * injection seam, which is why suites asserting \"no server is configured\" picked up the\n * developer's real servers and failed. For the default agent directory the resolved path\n * is unchanged.\n */\nexport function globalMcpConfigPath(agentDir: string = getAgentDir()): string {\n\treturn join(dirname(agentDir), \"mcp.json\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n\tif (!isRecord(value)) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn out;\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseTransport(entry: Record<string, unknown>): McpTransport | string {\n\tconst command = typeof entry.command === \"string\" ? entry.command : undefined;\n\tconst url = typeof entry.url === \"string\" ? entry.url : undefined;\n\n\tif (command && url) return \"declares both command and url; exactly one is required\";\n\tif (command) {\n\t\treturn {\n\t\t\tkind: \"stdio\",\n\t\t\tcommand,\n\t\t\targs: stringArray(entry.args),\n\t\t\tenv: stringRecord(entry.env),\n\t\t\tcwd: typeof entry.cwd === \"string\" ? entry.cwd : undefined,\n\t\t};\n\t}\n\tif (url) {\n\t\treturn {\n\t\t\tkind: \"http\",\n\t\t\turl,\n\t\t\theaders: stringRecord(entry.headers),\n\t\t\tbearerTokenEnv: typeof entry.bearerTokenEnv === \"string\" ? entry.bearerTokenEnv : undefined,\n\t\t};\n\t}\n\treturn \"declares neither command nor url\";\n}\n\n/**\n * An absent `capabilities` is the common case, not an edge case: a config pasted from\n * another host carries no Apex-specific field. Those servers get the full set, which\n * keeps them classified (never `UNCLASSIFIED`) without narrowing anything the user\n * did not ask to narrow.\n */\nfunction parseCapabilities(entry: Record<string, unknown>, report: (message: string) => void): ReadonlySet<Capability> {\n\tif (!(\"capabilities\" in entry)) return ALL_CAPABILITIES;\n\tif (!Array.isArray(entry.capabilities)) {\n\t\treport(\"capabilities is not an array; falling back to the full capability set\");\n\t\treturn ALL_CAPABILITIES;\n\t}\n\n\tconst resolved = new Set<Capability>();\n\tconst unknown: string[] = [];\n\tfor (const candidate of entry.capabilities) {\n\t\tif (typeof candidate === \"string\" && ALL_CAPABILITIES.has(candidate as Capability)) {\n\t\t\tresolved.add(candidate as Capability);\n\t\t} else {\n\t\t\tunknown.push(String(candidate));\n\t\t}\n\t}\n\tif (unknown.length > 0) report(`unknown capabilities dropped: ${unknown.join(\", \")}`);\n\treturn resolved;\n}\n\nfunction parseLifecycle(entry: Record<string, unknown>, report: (message: string) => void): McpLifecycle {\n\tconst value = entry.lifecycle;\n\tif (value === undefined) return DEFAULT_LIFECYCLE;\n\tif (typeof value === \"string\" && LIFECYCLES.includes(value as McpLifecycle)) return value as McpLifecycle;\n\treport(`unknown lifecycle ${JSON.stringify(value)}; using ${DEFAULT_LIFECYCLE}`);\n\treturn DEFAULT_LIFECYCLE;\n}\n\nfunction parseIdleTimeout(entry: Record<string, unknown>): number {\n\tconst value = entry.idleTimeout;\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIMEOUT_MINUTES;\n}\n\nfunction parseAuth(\n\tentry: Record<string, unknown>,\n\ttransport: McpTransport,\n\treport: (message: string) => void,\n): \"oauth\" | undefined {\n\tconst value = entry.auth;\n\tif (value === undefined) return undefined;\n\tif (transport.kind !== \"http\") {\n\t\treport(\"auth is only supported on HTTP servers; ignoring\");\n\t\treturn undefined;\n\t}\n\tif (value === \"oauth\") return \"oauth\";\n\treport(`unknown auth ${JSON.stringify(value)}; only \"oauth\" is supported; ignoring`);\n\treturn undefined;\n}\n\nfunction readServers(path: string, diagnostics: McpConfigDiagnostic[]): Map<string, McpServerConfig> {\n\tconst servers = new Map<string, McpServerConfig>();\n\tif (!existsSync(path)) return servers;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t} catch (error) {\n\t\tdiagnostics.push({\n\t\t\tpath,\n\t\t\tserver: undefined,\n\t\t\tmessage: `could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t});\n\t\treturn servers;\n\t}\n\n\tif (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {\n\t\tdiagnostics.push({ path, server: undefined, message: \"has no mcpServers object\" });\n\t\treturn servers;\n\t}\n\n\tfor (const [name, raw] of Object.entries(parsed.mcpServers)) {\n\t\tif (!isRecord(raw)) {\n\t\t\tdiagnostics.push({ path, server: name, message: \"entry is not an object\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst transport = parseTransport(raw);\n\t\tif (typeof transport === \"string\") {\n\t\t\tdiagnostics.push({ path, server: name, message: transport });\n\t\t\tcontinue;\n\t\t}\n\t\tconst report = (message: string) => diagnostics.push({ path, server: name, message });\n\t\tservers.set(name, {\n\t\t\tname,\n\t\t\ttransport,\n\t\t\tauth: parseAuth(raw, transport, report),\n\t\t\tcapabilities: parseCapabilities(raw, report),\n\t\t\tlifecycle: parseLifecycle(raw, report),\n\t\t\tidleTimeoutMinutes: parseIdleTimeout(raw),\n\t\t});\n\t}\n\n\treturn servers;\n}\n\n/**\n * Both sources are explicit, and an omitted one means \"no config from that scope\" rather\n * than \"go and find the real one\". Defaulting `globalPath` to the user's home made every\n * caller that forgot it read the developer's own servers, which is how suites asserting\n * an empty registry started failing on a configured machine. A boundary resolves the\n * path; this does not reach for one.\n */\nexport function loadMcpConfig(options: { projectPath?: string; globalPath?: string } = {}): McpConfig {\n\tconst diagnostics: McpConfigDiagnostic[] = [];\n\n\tconst servers = options.globalPath\n\t\t? readServers(options.globalPath, diagnostics)\n\t\t: new Map<string, McpServerConfig>();\n\tif (options.projectPath) {\n\t\tfor (const [name, config] of readServers(options.projectPath, diagnostics)) {\n\t\t\tservers.set(name, config);\n\t\t}\n\t}\n\n\treturn { servers, diagnostics };\n}\n"]}
@@ -7,15 +7,24 @@
7
7
  * first thing a user hand-edits, so a parse error must never take the session down.
8
8
  */
9
9
  import { existsSync, readFileSync } from "node:fs";
10
- import { homedir } from "node:os";
11
- import { join } from "node:path";
12
- import { CONFIG_DIR_NAME } from "../../config.js";
10
+ import { dirname, join } from "node:path";
11
+ import { getAgentDir } from "../../config.js";
13
12
  import { ALL_CAPABILITIES } from "../tools/contract.js";
14
13
  import { DEFAULT_IDLE_TIMEOUT_MINUTES, DEFAULT_LIFECYCLE, } from "./types.js";
15
14
  const LIFECYCLES = ["lazy", "eager", "keep-alive", "lazy-keep-alive"];
16
15
  export const PROJECT_CONFIG_FILENAME = ".mcp.json";
17
- export function globalMcpConfigPath() {
18
- return join(homedir(), CONFIG_DIR_NAME, "mcp.json");
16
+ /**
17
+ * The user-scope `mcp.json`, which sits beside the agent directory rather than inside it.
18
+ *
19
+ * Derived from `agentDir` and not from `homedir()`, so that injecting an agent directory
20
+ * isolates MCP config the way it already isolates settings, auth, and sessions. Reading
21
+ * the home directory directly made this the one user-scope resolver that ignored the
22
+ * injection seam, which is why suites asserting "no server is configured" picked up the
23
+ * developer's real servers and failed. For the default agent directory the resolved path
24
+ * is unchanged.
25
+ */
26
+ export function globalMcpConfigPath(agentDir = getAgentDir()) {
27
+ return join(dirname(agentDir), "mcp.json");
19
28
  }
20
29
  function isRecord(value) {
21
30
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -152,10 +161,18 @@ function readServers(path, diagnostics) {
152
161
  }
153
162
  return servers;
154
163
  }
164
+ /**
165
+ * Both sources are explicit, and an omitted one means "no config from that scope" rather
166
+ * than "go and find the real one". Defaulting `globalPath` to the user's home made every
167
+ * caller that forgot it read the developer's own servers, which is how suites asserting
168
+ * an empty registry started failing on a configured machine. A boundary resolves the
169
+ * path; this does not reach for one.
170
+ */
155
171
  export function loadMcpConfig(options = {}) {
156
172
  const diagnostics = [];
157
- const globalPath = options.globalPath ?? globalMcpConfigPath();
158
- const servers = readServers(globalPath, diagnostics);
173
+ const servers = options.globalPath
174
+ ? readServers(options.globalPath, diagnostics)
175
+ : new Map();
159
176
  if (options.projectPath) {
160
177
  for (const [name, config] of readServers(options.projectPath, diagnostics)) {
161
178
  servers.set(name, config);
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads the ecosystem-standard `{ \"mcpServers\": { … } }` shape from a project\n * `.mcp.json` and a global `mcp.json`, project winning per server name.\n *\n * Every failure degrades. A malformed file yields no servers and one diagnostic; a\n * malformed entry drops itself and leaves its siblings intact. A config file is the\n * first thing a user hand-edits, so a parse error must never take the session down.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../../config.ts\";\nimport { ALL_CAPABILITIES, type Capability } from \"../tools/contract.ts\";\nimport {\n\tDEFAULT_IDLE_TIMEOUT_MINUTES,\n\tDEFAULT_LIFECYCLE,\n\ttype McpConfig,\n\ttype McpConfigDiagnostic,\n\ttype McpLifecycle,\n\ttype McpServerConfig,\n\ttype McpTransport,\n} from \"./types.ts\";\n\nconst LIFECYCLES: readonly McpLifecycle[] = [\"lazy\", \"eager\", \"keep-alive\", \"lazy-keep-alive\"];\n\nexport const PROJECT_CONFIG_FILENAME = \".mcp.json\";\n\nexport function globalMcpConfigPath(): string {\n\treturn join(homedir(), CONFIG_DIR_NAME, \"mcp.json\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n\tif (!isRecord(value)) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn out;\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseTransport(entry: Record<string, unknown>): McpTransport | string {\n\tconst command = typeof entry.command === \"string\" ? entry.command : undefined;\n\tconst url = typeof entry.url === \"string\" ? entry.url : undefined;\n\n\tif (command && url) return \"declares both command and url; exactly one is required\";\n\tif (command) {\n\t\treturn {\n\t\t\tkind: \"stdio\",\n\t\t\tcommand,\n\t\t\targs: stringArray(entry.args),\n\t\t\tenv: stringRecord(entry.env),\n\t\t\tcwd: typeof entry.cwd === \"string\" ? entry.cwd : undefined,\n\t\t};\n\t}\n\tif (url) {\n\t\treturn {\n\t\t\tkind: \"http\",\n\t\t\turl,\n\t\t\theaders: stringRecord(entry.headers),\n\t\t\tbearerTokenEnv: typeof entry.bearerTokenEnv === \"string\" ? entry.bearerTokenEnv : undefined,\n\t\t};\n\t}\n\treturn \"declares neither command nor url\";\n}\n\n/**\n * An absent `capabilities` is the common case, not an edge case: a config pasted from\n * another host carries no Apex-specific field. Those servers get the full set, which\n * keeps them classified (never `UNCLASSIFIED`) without narrowing anything the user\n * did not ask to narrow.\n */\nfunction parseCapabilities(entry: Record<string, unknown>, report: (message: string) => void): ReadonlySet<Capability> {\n\tif (!(\"capabilities\" in entry)) return ALL_CAPABILITIES;\n\tif (!Array.isArray(entry.capabilities)) {\n\t\treport(\"capabilities is not an array; falling back to the full capability set\");\n\t\treturn ALL_CAPABILITIES;\n\t}\n\n\tconst resolved = new Set<Capability>();\n\tconst unknown: string[] = [];\n\tfor (const candidate of entry.capabilities) {\n\t\tif (typeof candidate === \"string\" && ALL_CAPABILITIES.has(candidate as Capability)) {\n\t\t\tresolved.add(candidate as Capability);\n\t\t} else {\n\t\t\tunknown.push(String(candidate));\n\t\t}\n\t}\n\tif (unknown.length > 0) report(`unknown capabilities dropped: ${unknown.join(\", \")}`);\n\treturn resolved;\n}\n\nfunction parseLifecycle(entry: Record<string, unknown>, report: (message: string) => void): McpLifecycle {\n\tconst value = entry.lifecycle;\n\tif (value === undefined) return DEFAULT_LIFECYCLE;\n\tif (typeof value === \"string\" && LIFECYCLES.includes(value as McpLifecycle)) return value as McpLifecycle;\n\treport(`unknown lifecycle ${JSON.stringify(value)}; using ${DEFAULT_LIFECYCLE}`);\n\treturn DEFAULT_LIFECYCLE;\n}\n\nfunction parseIdleTimeout(entry: Record<string, unknown>): number {\n\tconst value = entry.idleTimeout;\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIMEOUT_MINUTES;\n}\n\nfunction parseAuth(\n\tentry: Record<string, unknown>,\n\ttransport: McpTransport,\n\treport: (message: string) => void,\n): \"oauth\" | undefined {\n\tconst value = entry.auth;\n\tif (value === undefined) return undefined;\n\tif (transport.kind !== \"http\") {\n\t\treport(\"auth is only supported on HTTP servers; ignoring\");\n\t\treturn undefined;\n\t}\n\tif (value === \"oauth\") return \"oauth\";\n\treport(`unknown auth ${JSON.stringify(value)}; only \"oauth\" is supported; ignoring`);\n\treturn undefined;\n}\n\nfunction readServers(path: string, diagnostics: McpConfigDiagnostic[]): Map<string, McpServerConfig> {\n\tconst servers = new Map<string, McpServerConfig>();\n\tif (!existsSync(path)) return servers;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t} catch (error) {\n\t\tdiagnostics.push({\n\t\t\tpath,\n\t\t\tserver: undefined,\n\t\t\tmessage: `could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t});\n\t\treturn servers;\n\t}\n\n\tif (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {\n\t\tdiagnostics.push({ path, server: undefined, message: \"has no mcpServers object\" });\n\t\treturn servers;\n\t}\n\n\tfor (const [name, raw] of Object.entries(parsed.mcpServers)) {\n\t\tif (!isRecord(raw)) {\n\t\t\tdiagnostics.push({ path, server: name, message: \"entry is not an object\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst transport = parseTransport(raw);\n\t\tif (typeof transport === \"string\") {\n\t\t\tdiagnostics.push({ path, server: name, message: transport });\n\t\t\tcontinue;\n\t\t}\n\t\tconst report = (message: string) => diagnostics.push({ path, server: name, message });\n\t\tservers.set(name, {\n\t\t\tname,\n\t\t\ttransport,\n\t\t\tauth: parseAuth(raw, transport, report),\n\t\t\tcapabilities: parseCapabilities(raw, report),\n\t\t\tlifecycle: parseLifecycle(raw, report),\n\t\t\tidleTimeoutMinutes: parseIdleTimeout(raw),\n\t\t});\n\t}\n\n\treturn servers;\n}\n\nexport function loadMcpConfig(options: { projectPath?: string; globalPath?: string } = {}): McpConfig {\n\tconst diagnostics: McpConfigDiagnostic[] = [];\n\tconst globalPath = options.globalPath ?? globalMcpConfigPath();\n\n\tconst servers = readServers(globalPath, diagnostics);\n\tif (options.projectPath) {\n\t\tfor (const [name, config] of readServers(options.projectPath, diagnostics)) {\n\t\t\tservers.set(name, config);\n\t\t}\n\t}\n\n\treturn { servers, diagnostics };\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads the ecosystem-standard `{ \"mcpServers\": { … } }` shape from a project\n * `.mcp.json` and a global `mcp.json`, project winning per server name.\n *\n * Every failure degrades. A malformed file yields no servers and one diagnostic; a\n * malformed entry drops itself and leaves its siblings intact. A config file is the\n * first thing a user hand-edits, so a parse error must never take the session down.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { ALL_CAPABILITIES, type Capability } from \"../tools/contract.ts\";\nimport {\n\tDEFAULT_IDLE_TIMEOUT_MINUTES,\n\tDEFAULT_LIFECYCLE,\n\ttype McpConfig,\n\ttype McpConfigDiagnostic,\n\ttype McpLifecycle,\n\ttype McpServerConfig,\n\ttype McpTransport,\n} from \"./types.ts\";\n\nconst LIFECYCLES: readonly McpLifecycle[] = [\"lazy\", \"eager\", \"keep-alive\", \"lazy-keep-alive\"];\n\nexport const PROJECT_CONFIG_FILENAME = \".mcp.json\";\n\n/**\n * The user-scope `mcp.json`, which sits beside the agent directory rather than inside it.\n *\n * Derived from `agentDir` and not from `homedir()`, so that injecting an agent directory\n * isolates MCP config the way it already isolates settings, auth, and sessions. Reading\n * the home directory directly made this the one user-scope resolver that ignored the\n * injection seam, which is why suites asserting \"no server is configured\" picked up the\n * developer's real servers and failed. For the default agent directory the resolved path\n * is unchanged.\n */\nexport function globalMcpConfigPath(agentDir: string = getAgentDir()): string {\n\treturn join(dirname(agentDir), \"mcp.json\");\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n\tif (!isRecord(value)) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn out;\n}\n\nfunction stringArray(value: unknown): string[] {\n\treturn Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nfunction parseTransport(entry: Record<string, unknown>): McpTransport | string {\n\tconst command = typeof entry.command === \"string\" ? entry.command : undefined;\n\tconst url = typeof entry.url === \"string\" ? entry.url : undefined;\n\n\tif (command && url) return \"declares both command and url; exactly one is required\";\n\tif (command) {\n\t\treturn {\n\t\t\tkind: \"stdio\",\n\t\t\tcommand,\n\t\t\targs: stringArray(entry.args),\n\t\t\tenv: stringRecord(entry.env),\n\t\t\tcwd: typeof entry.cwd === \"string\" ? entry.cwd : undefined,\n\t\t};\n\t}\n\tif (url) {\n\t\treturn {\n\t\t\tkind: \"http\",\n\t\t\turl,\n\t\t\theaders: stringRecord(entry.headers),\n\t\t\tbearerTokenEnv: typeof entry.bearerTokenEnv === \"string\" ? entry.bearerTokenEnv : undefined,\n\t\t};\n\t}\n\treturn \"declares neither command nor url\";\n}\n\n/**\n * An absent `capabilities` is the common case, not an edge case: a config pasted from\n * another host carries no Apex-specific field. Those servers get the full set, which\n * keeps them classified (never `UNCLASSIFIED`) without narrowing anything the user\n * did not ask to narrow.\n */\nfunction parseCapabilities(entry: Record<string, unknown>, report: (message: string) => void): ReadonlySet<Capability> {\n\tif (!(\"capabilities\" in entry)) return ALL_CAPABILITIES;\n\tif (!Array.isArray(entry.capabilities)) {\n\t\treport(\"capabilities is not an array; falling back to the full capability set\");\n\t\treturn ALL_CAPABILITIES;\n\t}\n\n\tconst resolved = new Set<Capability>();\n\tconst unknown: string[] = [];\n\tfor (const candidate of entry.capabilities) {\n\t\tif (typeof candidate === \"string\" && ALL_CAPABILITIES.has(candidate as Capability)) {\n\t\t\tresolved.add(candidate as Capability);\n\t\t} else {\n\t\t\tunknown.push(String(candidate));\n\t\t}\n\t}\n\tif (unknown.length > 0) report(`unknown capabilities dropped: ${unknown.join(\", \")}`);\n\treturn resolved;\n}\n\nfunction parseLifecycle(entry: Record<string, unknown>, report: (message: string) => void): McpLifecycle {\n\tconst value = entry.lifecycle;\n\tif (value === undefined) return DEFAULT_LIFECYCLE;\n\tif (typeof value === \"string\" && LIFECYCLES.includes(value as McpLifecycle)) return value as McpLifecycle;\n\treport(`unknown lifecycle ${JSON.stringify(value)}; using ${DEFAULT_LIFECYCLE}`);\n\treturn DEFAULT_LIFECYCLE;\n}\n\nfunction parseIdleTimeout(entry: Record<string, unknown>): number {\n\tconst value = entry.idleTimeout;\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIMEOUT_MINUTES;\n}\n\nfunction parseAuth(\n\tentry: Record<string, unknown>,\n\ttransport: McpTransport,\n\treport: (message: string) => void,\n): \"oauth\" | undefined {\n\tconst value = entry.auth;\n\tif (value === undefined) return undefined;\n\tif (transport.kind !== \"http\") {\n\t\treport(\"auth is only supported on HTTP servers; ignoring\");\n\t\treturn undefined;\n\t}\n\tif (value === \"oauth\") return \"oauth\";\n\treport(`unknown auth ${JSON.stringify(value)}; only \"oauth\" is supported; ignoring`);\n\treturn undefined;\n}\n\nfunction readServers(path: string, diagnostics: McpConfigDiagnostic[]): Map<string, McpServerConfig> {\n\tconst servers = new Map<string, McpServerConfig>();\n\tif (!existsSync(path)) return servers;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(path, \"utf-8\"));\n\t} catch (error) {\n\t\tdiagnostics.push({\n\t\t\tpath,\n\t\t\tserver: undefined,\n\t\t\tmessage: `could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`,\n\t\t});\n\t\treturn servers;\n\t}\n\n\tif (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {\n\t\tdiagnostics.push({ path, server: undefined, message: \"has no mcpServers object\" });\n\t\treturn servers;\n\t}\n\n\tfor (const [name, raw] of Object.entries(parsed.mcpServers)) {\n\t\tif (!isRecord(raw)) {\n\t\t\tdiagnostics.push({ path, server: name, message: \"entry is not an object\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst transport = parseTransport(raw);\n\t\tif (typeof transport === \"string\") {\n\t\t\tdiagnostics.push({ path, server: name, message: transport });\n\t\t\tcontinue;\n\t\t}\n\t\tconst report = (message: string) => diagnostics.push({ path, server: name, message });\n\t\tservers.set(name, {\n\t\t\tname,\n\t\t\ttransport,\n\t\t\tauth: parseAuth(raw, transport, report),\n\t\t\tcapabilities: parseCapabilities(raw, report),\n\t\t\tlifecycle: parseLifecycle(raw, report),\n\t\t\tidleTimeoutMinutes: parseIdleTimeout(raw),\n\t\t});\n\t}\n\n\treturn servers;\n}\n\n/**\n * Both sources are explicit, and an omitted one means \"no config from that scope\" rather\n * than \"go and find the real one\". Defaulting `globalPath` to the user's home made every\n * caller that forgot it read the developer's own servers, which is how suites asserting\n * an empty registry started failing on a configured machine. A boundary resolves the\n * path; this does not reach for one.\n */\nexport function loadMcpConfig(options: { projectPath?: string; globalPath?: string } = {}): McpConfig {\n\tconst diagnostics: McpConfigDiagnostic[] = [];\n\n\tconst servers = options.globalPath\n\t\t? readServers(options.globalPath, diagnostics)\n\t\t: new Map<string, McpServerConfig>();\n\tif (options.projectPath) {\n\t\tfor (const [name, config] of readServers(options.projectPath, diagnostics)) {\n\t\t\tservers.set(name, config);\n\t\t}\n\t}\n\n\treturn { servers, diagnostics };\n}\n"]}
@@ -17,9 +17,8 @@ export declare function resolveAuthorizationHeaders(server: McpServerConfig, oau
17
17
  export declare function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection>;
18
18
  export interface SessionMcpConnectorOptions {
19
19
  /**
20
- * The session's credential store: `AuthStorage` on the host, `SandboxAuthStorage`
21
- * in a sandboxed child (reads from the projection, writes through the supervisor
22
- * channel). OAuth-configured servers without one fail closed.
20
+ * The session's credential store, `AuthStorage` by default. OAuth-configured
21
+ * servers without one fail closed.
23
22
  */
24
23
  credentials?: CredentialStore;
25
24
  now?: () => number;
@@ -1 +1 @@
1
- {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAA6B,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1G;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,MAAM,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,MAAM,GACzB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAwCD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqBnH;AAED,MAAM,WAAW,0BAA0B;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,0BAA+B,GAAG,YAAY,CAchG","sourcesContent":["/**\n * The MCP SDK lives here and nowhere else.\n *\n * Everything above this file talks to `McpConnection`, so the lifecycle logic, the\n * proxy tool, and their tests never import the SDK or start a process. This module is\n * the boundary where an external protocol becomes an internal type.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\nimport type { FetchLike } from \"./oauth/discover.ts\";\nimport { authRequired, ensureFreshServerToken } from \"./oauth/mcp-token.ts\";\nimport type { CachedTool, McpCallResult, McpConnection, McpConnector, McpServerConfig } from \"./types.ts\";\n\n/**\n * Authorization for one HTTP connection, in precedence order: a static configured\n * header, then a named bearer env var, then the credential store's OAuth token.\n * Later wins, because each later source is the more deliberately configured one.\n */\nexport function resolveAuthorizationHeaders(\n\tserver: McpServerConfig,\n\toauthAuthorization?: string,\n): Record<string, string> {\n\tif (server.transport.kind !== \"http\") return {};\n\tconst headers = { ...server.transport.headers };\n\tconst token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;\n\tif (token) headers.Authorization = `Bearer ${token}`;\n\tif (oauthAuthorization) headers.Authorization = oauthAuthorization;\n\treturn headers;\n}\n\nfunction createTransport(server: McpServerConfig, oauthAuthorization?: string) {\n\tif (server.transport.kind === \"stdio\") {\n\t\treturn new StdioClientTransport({\n\t\t\tcommand: server.transport.command,\n\t\t\targs: server.transport.args,\n\t\t\tenv: { ...process.env, ...server.transport.env } as Record<string, string>,\n\t\t\tcwd: server.transport.cwd,\n\t\t});\n\t}\n\n\treturn new StreamableHTTPClientTransport(new URL(server.transport.url), {\n\t\trequestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },\n\t});\n}\n\n/**\n * Server content arrives as a heterogeneous array. Only text is carried forward; an\n * image or an embedded resource is named rather than dropped silently, so a caller\n * can tell the difference between \"returned nothing\" and \"returned something we do\n * not render yet\".\n */\nfunction toCallResult(raw: unknown): McpCallResult {\n\tconst result = raw as { content?: unknown; isError?: unknown };\n\tconst parts = Array.isArray(result?.content) ? result.content : [];\n\tconst content = parts.map((part) => {\n\t\tconst item = part as { type?: unknown; text?: unknown };\n\t\tif (item?.type === \"text\" && typeof item.text === \"string\") {\n\t\t\treturn { type: \"text\" as const, text: item.text };\n\t\t}\n\t\treturn { type: \"text\" as const, text: `[${String(item?.type ?? \"unknown\")} content omitted]` };\n\t});\n\n\treturn {\n\t\tcontent: content.length > 0 ? content : [{ type: \"text\" as const, text: \"(no content)\" }],\n\t\tisError: result?.isError === true,\n\t};\n}\n\nexport async function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection> {\n\tconst client = new Client({ name: APP_NAME, version: VERSION });\n\tawait client.connect(createTransport(server, oauthAuthorization));\n\n\treturn {\n\t\tasync listTools(): Promise<CachedTool[]> {\n\t\t\tconst { tools } = await client.listTools();\n\t\t\treturn tools.map((tool) => ({\n\t\t\t\tserver: server.name,\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tinputSchema: tool.inputSchema,\n\t\t\t}));\n\t\t},\n\t\tasync callTool(name: string, args: unknown): Promise<McpCallResult> {\n\t\t\treturn toCallResult(await client.callTool({ name, arguments: (args ?? {}) as Record<string, unknown> }));\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait client.close();\n\t\t},\n\t};\n}\n\nexport interface SessionMcpConnectorOptions {\n\t/**\n\t * The session's credential store: `AuthStorage` on the host, `SandboxAuthStorage`\n\t * in a sandboxed child (reads from the projection, writes through the supervisor\n\t * channel). OAuth-configured servers without one fail closed.\n\t */\n\tcredentials?: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}\n\n/**\n * The session connector: resolves OAuth tokens for servers configured with\n * `auth: \"oauth\"`, and behaves exactly like `connectMcpServer` for everything else.\n * A missing authorization throws before the transport is constructed, so no OAuth\n * endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).\n */\nexport function createSessionMcpConnector(options: SessionMcpConnectorOptions = {}): McpConnector {\n\treturn async (server) => {\n\t\tif (server.transport.kind === \"http\" && server.auth === \"oauth\") {\n\t\t\tif (!options.credentials) throw authRequired(server.name);\n\t\t\tconst accessToken = await ensureFreshServerToken({\n\t\t\t\tserver,\n\t\t\t\tcredentials: options.credentials,\n\t\t\t\tnow: options.now,\n\t\t\t\tfetch: options.fetch,\n\t\t\t});\n\t\t\treturn connectMcpServer(server, `Bearer ${accessToken}`);\n\t\t}\n\t\treturn connectMcpServer(server);\n\t};\n}\n"]}
1
+ {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAA6B,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1G;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,MAAM,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,MAAM,GACzB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAwCD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqBnH;AAED,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,0BAA+B,GAAG,YAAY,CAchG","sourcesContent":["/**\n * The MCP SDK lives here and nowhere else.\n *\n * Everything above this file talks to `McpConnection`, so the lifecycle logic, the\n * proxy tool, and their tests never import the SDK or start a process. This module is\n * the boundary where an external protocol becomes an internal type.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\nimport type { FetchLike } from \"./oauth/discover.ts\";\nimport { authRequired, ensureFreshServerToken } from \"./oauth/mcp-token.ts\";\nimport type { CachedTool, McpCallResult, McpConnection, McpConnector, McpServerConfig } from \"./types.ts\";\n\n/**\n * Authorization for one HTTP connection, in precedence order: a static configured\n * header, then a named bearer env var, then the credential store's OAuth token.\n * Later wins, because each later source is the more deliberately configured one.\n */\nexport function resolveAuthorizationHeaders(\n\tserver: McpServerConfig,\n\toauthAuthorization?: string,\n): Record<string, string> {\n\tif (server.transport.kind !== \"http\") return {};\n\tconst headers = { ...server.transport.headers };\n\tconst token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;\n\tif (token) headers.Authorization = `Bearer ${token}`;\n\tif (oauthAuthorization) headers.Authorization = oauthAuthorization;\n\treturn headers;\n}\n\nfunction createTransport(server: McpServerConfig, oauthAuthorization?: string) {\n\tif (server.transport.kind === \"stdio\") {\n\t\treturn new StdioClientTransport({\n\t\t\tcommand: server.transport.command,\n\t\t\targs: server.transport.args,\n\t\t\tenv: { ...process.env, ...server.transport.env } as Record<string, string>,\n\t\t\tcwd: server.transport.cwd,\n\t\t});\n\t}\n\n\treturn new StreamableHTTPClientTransport(new URL(server.transport.url), {\n\t\trequestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },\n\t});\n}\n\n/**\n * Server content arrives as a heterogeneous array. Only text is carried forward; an\n * image or an embedded resource is named rather than dropped silently, so a caller\n * can tell the difference between \"returned nothing\" and \"returned something we do\n * not render yet\".\n */\nfunction toCallResult(raw: unknown): McpCallResult {\n\tconst result = raw as { content?: unknown; isError?: unknown };\n\tconst parts = Array.isArray(result?.content) ? result.content : [];\n\tconst content = parts.map((part) => {\n\t\tconst item = part as { type?: unknown; text?: unknown };\n\t\tif (item?.type === \"text\" && typeof item.text === \"string\") {\n\t\t\treturn { type: \"text\" as const, text: item.text };\n\t\t}\n\t\treturn { type: \"text\" as const, text: `[${String(item?.type ?? \"unknown\")} content omitted]` };\n\t});\n\n\treturn {\n\t\tcontent: content.length > 0 ? content : [{ type: \"text\" as const, text: \"(no content)\" }],\n\t\tisError: result?.isError === true,\n\t};\n}\n\nexport async function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection> {\n\tconst client = new Client({ name: APP_NAME, version: VERSION });\n\tawait client.connect(createTransport(server, oauthAuthorization));\n\n\treturn {\n\t\tasync listTools(): Promise<CachedTool[]> {\n\t\t\tconst { tools } = await client.listTools();\n\t\t\treturn tools.map((tool) => ({\n\t\t\t\tserver: server.name,\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tinputSchema: tool.inputSchema,\n\t\t\t}));\n\t\t},\n\t\tasync callTool(name: string, args: unknown): Promise<McpCallResult> {\n\t\t\treturn toCallResult(await client.callTool({ name, arguments: (args ?? {}) as Record<string, unknown> }));\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait client.close();\n\t\t},\n\t};\n}\n\nexport interface SessionMcpConnectorOptions {\n\t/**\n\t * The session's credential store, `AuthStorage` by default. OAuth-configured\n\t * servers without one fail closed.\n\t */\n\tcredentials?: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}\n\n/**\n * The session connector: resolves OAuth tokens for servers configured with\n * `auth: \"oauth\"`, and behaves exactly like `connectMcpServer` for everything else.\n * A missing authorization throws before the transport is constructed, so no OAuth\n * endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).\n */\nexport function createSessionMcpConnector(options: SessionMcpConnectorOptions = {}): McpConnector {\n\treturn async (server) => {\n\t\tif (server.transport.kind === \"http\" && server.auth === \"oauth\") {\n\t\t\tif (!options.credentials) throw authRequired(server.name);\n\t\t\tconst accessToken = await ensureFreshServerToken({\n\t\t\t\tserver,\n\t\t\t\tcredentials: options.credentials,\n\t\t\t\tnow: options.now,\n\t\t\t\tfetch: options.fetch,\n\t\t\t});\n\t\t\treturn connectMcpServer(server, `Bearer ${accessToken}`);\n\t\t}\n\t\treturn connectMcpServer(server);\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"connector.js","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The MCP SDK lives here and nowhere else.\n *\n * Everything above this file talks to `McpConnection`, so the lifecycle logic, the\n * proxy tool, and their tests never import the SDK or start a process. This module is\n * the boundary where an external protocol becomes an internal type.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\nimport type { FetchLike } from \"./oauth/discover.ts\";\nimport { authRequired, ensureFreshServerToken } from \"./oauth/mcp-token.ts\";\nimport type { CachedTool, McpCallResult, McpConnection, McpConnector, McpServerConfig } from \"./types.ts\";\n\n/**\n * Authorization for one HTTP connection, in precedence order: a static configured\n * header, then a named bearer env var, then the credential store's OAuth token.\n * Later wins, because each later source is the more deliberately configured one.\n */\nexport function resolveAuthorizationHeaders(\n\tserver: McpServerConfig,\n\toauthAuthorization?: string,\n): Record<string, string> {\n\tif (server.transport.kind !== \"http\") return {};\n\tconst headers = { ...server.transport.headers };\n\tconst token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;\n\tif (token) headers.Authorization = `Bearer ${token}`;\n\tif (oauthAuthorization) headers.Authorization = oauthAuthorization;\n\treturn headers;\n}\n\nfunction createTransport(server: McpServerConfig, oauthAuthorization?: string) {\n\tif (server.transport.kind === \"stdio\") {\n\t\treturn new StdioClientTransport({\n\t\t\tcommand: server.transport.command,\n\t\t\targs: server.transport.args,\n\t\t\tenv: { ...process.env, ...server.transport.env } as Record<string, string>,\n\t\t\tcwd: server.transport.cwd,\n\t\t});\n\t}\n\n\treturn new StreamableHTTPClientTransport(new URL(server.transport.url), {\n\t\trequestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },\n\t});\n}\n\n/**\n * Server content arrives as a heterogeneous array. Only text is carried forward; an\n * image or an embedded resource is named rather than dropped silently, so a caller\n * can tell the difference between \"returned nothing\" and \"returned something we do\n * not render yet\".\n */\nfunction toCallResult(raw: unknown): McpCallResult {\n\tconst result = raw as { content?: unknown; isError?: unknown };\n\tconst parts = Array.isArray(result?.content) ? result.content : [];\n\tconst content = parts.map((part) => {\n\t\tconst item = part as { type?: unknown; text?: unknown };\n\t\tif (item?.type === \"text\" && typeof item.text === \"string\") {\n\t\t\treturn { type: \"text\" as const, text: item.text };\n\t\t}\n\t\treturn { type: \"text\" as const, text: `[${String(item?.type ?? \"unknown\")} content omitted]` };\n\t});\n\n\treturn {\n\t\tcontent: content.length > 0 ? content : [{ type: \"text\" as const, text: \"(no content)\" }],\n\t\tisError: result?.isError === true,\n\t};\n}\n\nexport async function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection> {\n\tconst client = new Client({ name: APP_NAME, version: VERSION });\n\tawait client.connect(createTransport(server, oauthAuthorization));\n\n\treturn {\n\t\tasync listTools(): Promise<CachedTool[]> {\n\t\t\tconst { tools } = await client.listTools();\n\t\t\treturn tools.map((tool) => ({\n\t\t\t\tserver: server.name,\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tinputSchema: tool.inputSchema,\n\t\t\t}));\n\t\t},\n\t\tasync callTool(name: string, args: unknown): Promise<McpCallResult> {\n\t\t\treturn toCallResult(await client.callTool({ name, arguments: (args ?? {}) as Record<string, unknown> }));\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait client.close();\n\t\t},\n\t};\n}\n\nexport interface SessionMcpConnectorOptions {\n\t/**\n\t * The session's credential store: `AuthStorage` on the host, `SandboxAuthStorage`\n\t * in a sandboxed child (reads from the projection, writes through the supervisor\n\t * channel). OAuth-configured servers without one fail closed.\n\t */\n\tcredentials?: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}\n\n/**\n * The session connector: resolves OAuth tokens for servers configured with\n * `auth: \"oauth\"`, and behaves exactly like `connectMcpServer` for everything else.\n * A missing authorization throws before the transport is constructed, so no OAuth\n * endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).\n */\nexport function createSessionMcpConnector(options: SessionMcpConnectorOptions = {}): McpConnector {\n\treturn async (server) => {\n\t\tif (server.transport.kind === \"http\" && server.auth === \"oauth\") {\n\t\t\tif (!options.credentials) throw authRequired(server.name);\n\t\t\tconst accessToken = await ensureFreshServerToken({\n\t\t\t\tserver,\n\t\t\t\tcredentials: options.credentials,\n\t\t\t\tnow: options.now,\n\t\t\t\tfetch: options.fetch,\n\t\t\t});\n\t\t\treturn connectMcpServer(server, `Bearer ${accessToken}`);\n\t\t}\n\t\treturn connectMcpServer(server);\n\t};\n}\n"]}
1
+ {"version":3,"file":"connector.js","sourceRoot":"","sources":["../../../src/core/mcp/connector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The MCP SDK lives here and nowhere else.\n *\n * Everything above this file talks to `McpConnection`, so the lifecycle logic, the\n * proxy tool, and their tests never import the SDK or start a process. This module is\n * the boundary where an external protocol becomes an internal type.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\nimport type { FetchLike } from \"./oauth/discover.ts\";\nimport { authRequired, ensureFreshServerToken } from \"./oauth/mcp-token.ts\";\nimport type { CachedTool, McpCallResult, McpConnection, McpConnector, McpServerConfig } from \"./types.ts\";\n\n/**\n * Authorization for one HTTP connection, in precedence order: a static configured\n * header, then a named bearer env var, then the credential store's OAuth token.\n * Later wins, because each later source is the more deliberately configured one.\n */\nexport function resolveAuthorizationHeaders(\n\tserver: McpServerConfig,\n\toauthAuthorization?: string,\n): Record<string, string> {\n\tif (server.transport.kind !== \"http\") return {};\n\tconst headers = { ...server.transport.headers };\n\tconst token = server.transport.bearerTokenEnv ? process.env[server.transport.bearerTokenEnv] : undefined;\n\tif (token) headers.Authorization = `Bearer ${token}`;\n\tif (oauthAuthorization) headers.Authorization = oauthAuthorization;\n\treturn headers;\n}\n\nfunction createTransport(server: McpServerConfig, oauthAuthorization?: string) {\n\tif (server.transport.kind === \"stdio\") {\n\t\treturn new StdioClientTransport({\n\t\t\tcommand: server.transport.command,\n\t\t\targs: server.transport.args,\n\t\t\tenv: { ...process.env, ...server.transport.env } as Record<string, string>,\n\t\t\tcwd: server.transport.cwd,\n\t\t});\n\t}\n\n\treturn new StreamableHTTPClientTransport(new URL(server.transport.url), {\n\t\trequestInit: { headers: resolveAuthorizationHeaders(server, oauthAuthorization) },\n\t});\n}\n\n/**\n * Server content arrives as a heterogeneous array. Only text is carried forward; an\n * image or an embedded resource is named rather than dropped silently, so a caller\n * can tell the difference between \"returned nothing\" and \"returned something we do\n * not render yet\".\n */\nfunction toCallResult(raw: unknown): McpCallResult {\n\tconst result = raw as { content?: unknown; isError?: unknown };\n\tconst parts = Array.isArray(result?.content) ? result.content : [];\n\tconst content = parts.map((part) => {\n\t\tconst item = part as { type?: unknown; text?: unknown };\n\t\tif (item?.type === \"text\" && typeof item.text === \"string\") {\n\t\t\treturn { type: \"text\" as const, text: item.text };\n\t\t}\n\t\treturn { type: \"text\" as const, text: `[${String(item?.type ?? \"unknown\")} content omitted]` };\n\t});\n\n\treturn {\n\t\tcontent: content.length > 0 ? content : [{ type: \"text\" as const, text: \"(no content)\" }],\n\t\tisError: result?.isError === true,\n\t};\n}\n\nexport async function connectMcpServer(server: McpServerConfig, oauthAuthorization?: string): Promise<McpConnection> {\n\tconst client = new Client({ name: APP_NAME, version: VERSION });\n\tawait client.connect(createTransport(server, oauthAuthorization));\n\n\treturn {\n\t\tasync listTools(): Promise<CachedTool[]> {\n\t\t\tconst { tools } = await client.listTools();\n\t\t\treturn tools.map((tool) => ({\n\t\t\t\tserver: server.name,\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tinputSchema: tool.inputSchema,\n\t\t\t}));\n\t\t},\n\t\tasync callTool(name: string, args: unknown): Promise<McpCallResult> {\n\t\t\treturn toCallResult(await client.callTool({ name, arguments: (args ?? {}) as Record<string, unknown> }));\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait client.close();\n\t\t},\n\t};\n}\n\nexport interface SessionMcpConnectorOptions {\n\t/**\n\t * The session's credential store, `AuthStorage` by default. OAuth-configured\n\t * servers without one fail closed.\n\t */\n\tcredentials?: CredentialStore;\n\tnow?: () => number;\n\tfetch?: FetchLike;\n}\n\n/**\n * The session connector: resolves OAuth tokens for servers configured with\n * `auth: \"oauth\"`, and behaves exactly like `connectMcpServer` for everything else.\n * A missing authorization throws before the transport is constructed, so no OAuth\n * endpoint is ever contacted from a tool call path (spec 2026-09-01-mcp-oauth).\n */\nexport function createSessionMcpConnector(options: SessionMcpConnectorOptions = {}): McpConnector {\n\treturn async (server) => {\n\t\tif (server.transport.kind === \"http\" && server.auth === \"oauth\") {\n\t\t\tif (!options.credentials) throw authRequired(server.name);\n\t\t\tconst accessToken = await ensureFreshServerToken({\n\t\t\t\tserver,\n\t\t\t\tcredentials: options.credentials,\n\t\t\t\tnow: options.now,\n\t\t\t\tfetch: options.fetch,\n\t\t\t});\n\t\t\treturn connectMcpServer(server, `Bearer ${accessToken}`);\n\t\t}\n\t\treturn connectMcpServer(server);\n\t};\n}\n"]}
@@ -10,6 +10,8 @@ export interface AuthorizeServerOptions {
10
10
  serverName: string;
11
11
  /** Project root for `.mcp.json` discovery. Default: process.cwd(). */
12
12
  cwd?: string;
13
+ /** Agent directory the user-scope `mcp.json` is resolved against. Default: the host's. */
14
+ agentDir?: string;
13
15
  /** Credential sink. Default: the host store (`AuthStorage.create()`). */
14
16
  credentials?: CredentialStore;
15
17
  openBrowser?: (url: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,sBAAsB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,eAAe,CAAC;IAC9B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B9F","sourcesContent":["/**\n * Shared entry point for `apex-code mcp auth <server>` and the TUI `/mcp auth`\n * command. Resolves the named server from the merged MCP config, checks it is an\n * OAuth-configured HTTP server, and runs the flow. UI-agnostic: callers supply the\n * line printer and the browser opener.\n */\n\nimport { join } from \"node:path\";\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { loadMcpConfig, PROJECT_CONFIG_FILENAME } from \"../config.ts\";\nimport { runMcpOAuthFlow } from \"./flow.ts\";\n\nexport interface AuthorizeServerOptions {\n\tserverName: string;\n\t/** Project root for `.mcp.json` discovery. Default: process.cwd(). */\n\tcwd?: string;\n\t/** Credential sink. Default: the host store (`AuthStorage.create()`). */\n\tcredentials?: CredentialStore;\n\topenBrowser?: (url: string) => void;\n\t/** Injectable flow, for tests. */\n\tflow?: typeof runMcpOAuthFlow;\n\tlog: (line: string) => void;\n}\n\nexport async function authorizeConfiguredServer(options: AuthorizeServerOptions): Promise<void> {\n\tconst { servers } = loadMcpConfig({ projectPath: join(options.cwd ?? process.cwd(), PROJECT_CONFIG_FILENAME) });\n\tconst server = servers.get(options.serverName);\n\tif (!server) {\n\t\tconst names = [...servers.keys()];\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured. Configured servers: ${names.join(\", \") || \"none\"}. Add it to .mcp.json first.`,\n\t\t);\n\t}\n\tif (server.transport.kind !== \"http\") {\n\t\tthrow new Error(`MCP server \"${options.serverName}\" is a stdio server; OAuth applies to HTTP servers only.`);\n\t}\n\tif (server.auth !== \"oauth\") {\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured for OAuth. Add \"auth\": \"oauth\" to its entry.`,\n\t\t);\n\t}\n\tconst result = await (options.flow ?? runMcpOAuthFlow)({\n\t\tserverName: options.serverName,\n\t\tserverUrl: server.transport.url,\n\t\tcredentials: options.credentials ?? AuthStorage.create(),\n\t\tdeps: { print: options.log, openBrowser: options.openBrowser ?? (() => {}) },\n\t});\n\toptions.log(\n\t\t`Authorized \"${options.serverName}\". Token stored; expires ${new Date(result.expiresAt).toISOString()}.`,\n\t);\n}\n"]}
1
+ {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,sBAAsB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,eAAe,CAAC;IAC9B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B9F","sourcesContent":["/**\n * Shared entry point for `apex-code mcp auth <server>` and the TUI `/mcp auth`\n * command. Resolves the named server from the merged MCP config, checks it is an\n * OAuth-configured HTTP server, and runs the flow. UI-agnostic: callers supply the\n * line printer and the browser opener.\n */\n\nimport { join } from \"node:path\";\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from \"../config.ts\";\nimport { runMcpOAuthFlow } from \"./flow.ts\";\n\nexport interface AuthorizeServerOptions {\n\tserverName: string;\n\t/** Project root for `.mcp.json` discovery. Default: process.cwd(). */\n\tcwd?: string;\n\t/** Agent directory the user-scope `mcp.json` is resolved against. Default: the host's. */\n\tagentDir?: string;\n\t/** Credential sink. Default: the host store (`AuthStorage.create()`). */\n\tcredentials?: CredentialStore;\n\topenBrowser?: (url: string) => void;\n\t/** Injectable flow, for tests. */\n\tflow?: typeof runMcpOAuthFlow;\n\tlog: (line: string) => void;\n}\n\nexport async function authorizeConfiguredServer(options: AuthorizeServerOptions): Promise<void> {\n\tconst { servers } = loadMcpConfig({\n\t\tprojectPath: join(options.cwd ?? process.cwd(), PROJECT_CONFIG_FILENAME),\n\t\tglobalPath: globalMcpConfigPath(options.agentDir),\n\t});\n\tconst server = servers.get(options.serverName);\n\tif (!server) {\n\t\tconst names = [...servers.keys()];\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured. Configured servers: ${names.join(\", \") || \"none\"}. Add it to .mcp.json first.`,\n\t\t);\n\t}\n\tif (server.transport.kind !== \"http\") {\n\t\tthrow new Error(`MCP server \"${options.serverName}\" is a stdio server; OAuth applies to HTTP servers only.`);\n\t}\n\tif (server.auth !== \"oauth\") {\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured for OAuth. Add \"auth\": \"oauth\" to its entry.`,\n\t\t);\n\t}\n\tconst result = await (options.flow ?? runMcpOAuthFlow)({\n\t\tserverName: options.serverName,\n\t\tserverUrl: server.transport.url,\n\t\tcredentials: options.credentials ?? AuthStorage.create(),\n\t\tdeps: { print: options.log, openBrowser: options.openBrowser ?? (() => {}) },\n\t});\n\toptions.log(\n\t\t`Authorized \"${options.serverName}\". Token stored; expires ${new Date(result.expiresAt).toISOString()}.`,\n\t);\n}\n"]}