apex-code 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +7 -8
  3. package/dist/cli/agent-lifecycle.d.ts +24 -0
  4. package/dist/cli/agent-lifecycle.d.ts.map +1 -0
  5. package/dist/cli/agent-lifecycle.js +126 -0
  6. package/dist/cli/agent-lifecycle.js.map +1 -0
  7. package/dist/cli/args.d.ts +13 -12
  8. package/dist/cli/args.d.ts.map +1 -1
  9. package/dist/cli/args.js +80 -30
  10. package/dist/cli/args.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +4 -130
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +1 -1
  17. package/dist/config.js.map +1 -1
  18. package/dist/core/agent-session-services.d.ts.map +1 -1
  19. package/dist/core/agent-session-services.js +0 -8
  20. package/dist/core/agent-session-services.js.map +1 -1
  21. package/dist/core/agent-session.d.ts +160 -0
  22. package/dist/core/agent-session.d.ts.map +1 -1
  23. package/dist/core/agent-session.js +265 -0
  24. package/dist/core/agent-session.js.map +1 -1
  25. package/dist/core/delegation/agents.d.ts.map +1 -1
  26. package/dist/core/delegation/agents.js +2 -1
  27. package/dist/core/delegation/agents.js.map +1 -1
  28. package/dist/core/delegation/runtime.d.ts +674 -1
  29. package/dist/core/delegation/runtime.d.ts.map +1 -1
  30. package/dist/core/delegation/runtime.js +1338 -66
  31. package/dist/core/delegation/runtime.js.map +1 -1
  32. package/dist/core/extensions/loader.d.ts.map +1 -1
  33. package/dist/core/extensions/loader.js +3 -4
  34. package/dist/core/extensions/loader.js.map +1 -1
  35. package/dist/core/extensions/source-runtime.d.ts +4 -5
  36. package/dist/core/extensions/source-runtime.d.ts.map +1 -1
  37. package/dist/core/extensions/source-runtime.js +4 -5
  38. package/dist/core/extensions/source-runtime.js.map +1 -1
  39. package/dist/core/formatter-lifecycle.d.ts +2 -1
  40. package/dist/core/formatter-lifecycle.d.ts.map +1 -1
  41. package/dist/core/formatter-lifecycle.js +2 -1
  42. package/dist/core/formatter-lifecycle.js.map +1 -1
  43. package/dist/core/hooks/types.d.ts +2 -3
  44. package/dist/core/hooks/types.d.ts.map +1 -1
  45. package/dist/core/hooks/types.js +2 -3
  46. package/dist/core/hooks/types.js.map +1 -1
  47. package/dist/core/http-idle-timeout.d.ts +3 -5
  48. package/dist/core/http-idle-timeout.d.ts.map +1 -1
  49. package/dist/core/http-idle-timeout.js +3 -5
  50. package/dist/core/http-idle-timeout.js.map +1 -1
  51. package/dist/core/mcp/config.d.ts +18 -1
  52. package/dist/core/mcp/config.d.ts.map +1 -1
  53. package/dist/core/mcp/config.js +24 -7
  54. package/dist/core/mcp/config.js.map +1 -1
  55. package/dist/core/mcp/connector.d.ts +2 -3
  56. package/dist/core/mcp/connector.d.ts.map +1 -1
  57. package/dist/core/mcp/connector.js.map +1 -1
  58. package/dist/core/mcp/oauth/authorize.d.ts +2 -0
  59. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
  60. package/dist/core/mcp/oauth/authorize.js +5 -2
  61. package/dist/core/mcp/oauth/authorize.js.map +1 -1
  62. package/dist/core/mcp/oauth/mcp-token.d.ts +4 -6
  63. package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -1
  64. package/dist/core/mcp/oauth/mcp-token.js +5 -7
  65. package/dist/core/mcp/oauth/mcp-token.js.map +1 -1
  66. package/dist/core/mcp/runtime.d.ts +1 -0
  67. package/dist/core/mcp/runtime.d.ts.map +1 -1
  68. package/dist/core/mcp/runtime.js +2 -1
  69. package/dist/core/mcp/runtime.js.map +1 -1
  70. package/dist/core/package-manager.d.ts.map +1 -1
  71. package/dist/core/package-manager.js +0 -15
  72. package/dist/core/package-manager.js.map +1 -1
  73. package/dist/core/permissions/store.d.ts +6 -11
  74. package/dist/core/permissions/store.d.ts.map +1 -1
  75. package/dist/core/permissions/store.js +23 -51
  76. package/dist/core/permissions/store.js.map +1 -1
  77. package/dist/core/project-resources.d.ts +62 -0
  78. package/dist/core/project-resources.d.ts.map +1 -0
  79. package/dist/core/project-resources.js +42 -0
  80. package/dist/core/project-resources.js.map +1 -0
  81. package/dist/core/sdk.d.ts +62 -12
  82. package/dist/core/sdk.d.ts.map +1 -1
  83. package/dist/core/sdk.js +313 -44
  84. package/dist/core/sdk.js.map +1 -1
  85. package/dist/core/session-share.d.ts +6 -8
  86. package/dist/core/session-share.d.ts.map +1 -1
  87. package/dist/core/session-share.js +9 -12
  88. package/dist/core/session-share.js.map +1 -1
  89. package/dist/core/settings-manager.d.ts +0 -21
  90. package/dist/core/settings-manager.d.ts.map +1 -1
  91. package/dist/core/settings-manager.js +4 -12
  92. package/dist/core/settings-manager.js.map +1 -1
  93. package/dist/core/tools/bash.d.ts.map +1 -1
  94. package/dist/core/tools/bash.js +0 -47
  95. package/dist/core/tools/bash.js.map +1 -1
  96. package/dist/core/tools/delegate.d.ts +8 -0
  97. package/dist/core/tools/delegate.d.ts.map +1 -1
  98. package/dist/core/tools/delegate.js +33 -3
  99. package/dist/core/tools/delegate.js.map +1 -1
  100. package/dist/core/tools/web-fetch.d.ts.map +1 -1
  101. package/dist/core/tools/web-fetch.js +4 -8
  102. package/dist/core/tools/web-fetch.js.map +1 -1
  103. package/dist/core/tools/web-search-exa.d.ts +3 -6
  104. package/dist/core/tools/web-search-exa.d.ts.map +1 -1
  105. package/dist/core/tools/web-search-exa.js +5 -8
  106. package/dist/core/tools/web-search-exa.js.map +1 -1
  107. package/dist/core/trust-manager.d.ts +6 -5
  108. package/dist/core/trust-manager.d.ts.map +1 -1
  109. package/dist/core/trust-manager.js +42 -19
  110. package/dist/core/trust-manager.js.map +1 -1
  111. package/dist/core/web-search-provider.d.ts +0 -10
  112. package/dist/core/web-search-provider.d.ts.map +1 -1
  113. package/dist/core/web-search-provider.js +2 -16
  114. package/dist/core/web-search-provider.js.map +1 -1
  115. package/dist/core/workspace/git-observer.d.ts +16 -0
  116. package/dist/core/workspace/git-observer.d.ts.map +1 -1
  117. package/dist/core/workspace/git-observer.js +8 -1
  118. package/dist/core/workspace/git-observer.js.map +1 -1
  119. package/dist/core/workspace/git-worktree-owner.d.ts +89 -0
  120. package/dist/core/workspace/git-worktree-owner.d.ts.map +1 -0
  121. package/dist/core/workspace/git-worktree-owner.js +240 -0
  122. package/dist/core/workspace/git-worktree-owner.js.map +1 -0
  123. package/dist/main.d.ts +0 -1
  124. package/dist/main.d.ts.map +1 -1
  125. package/dist/main.js +22 -3
  126. package/dist/main.js.map +1 -1
  127. package/dist/modes/acp/server.d.ts +43 -1
  128. package/dist/modes/acp/server.d.ts.map +1 -1
  129. package/dist/modes/acp/server.js +78 -0
  130. package/dist/modes/acp/server.js.map +1 -1
  131. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  132. package/dist/modes/interactive/components/settings-selector.js +1 -1
  133. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  134. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  135. package/dist/modes/interactive/interactive-mode.js +0 -29
  136. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  137. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  138. package/dist/modes/rpc/rpc-mode.js +38 -0
  139. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  140. package/dist/modes/rpc/rpc-types.d.ts +110 -0
  141. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  142. package/dist/modes/rpc/rpc-types.js.map +1 -1
  143. package/dist/utils/tools-manager.d.ts +0 -25
  144. package/dist/utils/tools-manager.d.ts.map +1 -1
  145. package/dist/utils/tools-manager.js +3 -52
  146. package/dist/utils/tools-manager.js.map +1 -1
  147. package/dist/utils/version-check.d.ts +0 -2
  148. package/dist/utils/version-check.d.ts.map +1 -1
  149. package/dist/utils/version-check.js +0 -2
  150. package/dist/utils/version-check.js.map +1 -1
  151. package/docs/settings.md +2 -7
  152. package/npm-shrinkwrap.json +5 -5
  153. package/package.json +2 -2
  154. package/dist/core/sandbox/bwrap-arguments.d.ts +0 -41
  155. package/dist/core/sandbox/bwrap-arguments.d.ts.map +0 -1
  156. package/dist/core/sandbox/bwrap-arguments.js +0 -136
  157. package/dist/core/sandbox/bwrap-arguments.js.map +0 -1
  158. package/dist/core/sandbox/child-entry.d.ts +0 -2
  159. package/dist/core/sandbox/child-entry.d.ts.map +0 -1
  160. package/dist/core/sandbox/child-entry.js +0 -24
  161. package/dist/core/sandbox/child-entry.js.map +0 -1
  162. package/dist/core/sandbox/cli-launch.d.ts +0 -162
  163. package/dist/core/sandbox/cli-launch.d.ts.map +0 -1
  164. package/dist/core/sandbox/cli-launch.js +0 -366
  165. package/dist/core/sandbox/cli-launch.js.map +0 -1
  166. package/dist/core/sandbox/cli-supervisor.d.ts +0 -31
  167. package/dist/core/sandbox/cli-supervisor.d.ts.map +0 -1
  168. package/dist/core/sandbox/cli-supervisor.js +0 -147
  169. package/dist/core/sandbox/cli-supervisor.js.map +0 -1
  170. package/dist/core/sandbox/default-hosts.d.ts +0 -28
  171. package/dist/core/sandbox/default-hosts.d.ts.map +0 -1
  172. package/dist/core/sandbox/default-hosts.js +0 -69
  173. package/dist/core/sandbox/default-hosts.js.map +0 -1
  174. package/dist/core/sandbox/full-access.d.ts +0 -28
  175. package/dist/core/sandbox/full-access.d.ts.map +0 -1
  176. package/dist/core/sandbox/full-access.js +0 -63
  177. package/dist/core/sandbox/full-access.js.map +0 -1
  178. package/dist/core/sandbox/git-identity.d.ts +0 -57
  179. package/dist/core/sandbox/git-identity.d.ts.map +0 -1
  180. package/dist/core/sandbox/git-identity.js +0 -78
  181. package/dist/core/sandbox/git-identity.js.map +0 -1
  182. package/dist/core/sandbox/host-approval.d.ts +0 -42
  183. package/dist/core/sandbox/host-approval.d.ts.map +0 -1
  184. package/dist/core/sandbox/host-approval.js +0 -99
  185. package/dist/core/sandbox/host-approval.js.map +0 -1
  186. package/dist/core/sandbox/linux-backend.d.ts +0 -48
  187. package/dist/core/sandbox/linux-backend.d.ts.map +0 -1
  188. package/dist/core/sandbox/linux-backend.js +0 -328
  189. package/dist/core/sandbox/linux-backend.js.map +0 -1
  190. package/dist/core/sandbox/macos-backend.d.ts +0 -30
  191. package/dist/core/sandbox/macos-backend.d.ts.map +0 -1
  192. package/dist/core/sandbox/macos-backend.js +0 -385
  193. package/dist/core/sandbox/macos-backend.js.map +0 -1
  194. package/dist/core/sandbox/network-proxy.d.ts +0 -38
  195. package/dist/core/sandbox/network-proxy.d.ts.map +0 -1
  196. package/dist/core/sandbox/network-proxy.js +0 -153
  197. package/dist/core/sandbox/network-proxy.js.map +0 -1
  198. package/dist/core/sandbox/network-refusal.d.ts +0 -24
  199. package/dist/core/sandbox/network-refusal.d.ts.map +0 -1
  200. package/dist/core/sandbox/network-refusal.js +0 -72
  201. package/dist/core/sandbox/network-refusal.js.map +0 -1
  202. package/dist/core/sandbox/policy.d.ts +0 -40
  203. package/dist/core/sandbox/policy.d.ts.map +0 -1
  204. package/dist/core/sandbox/policy.js +0 -55
  205. package/dist/core/sandbox/policy.js.map +0 -1
  206. package/dist/core/sandbox/profiles.d.ts +0 -25
  207. package/dist/core/sandbox/profiles.d.ts.map +0 -1
  208. package/dist/core/sandbox/profiles.js +0 -23
  209. package/dist/core/sandbox/profiles.js.map +0 -1
  210. package/dist/core/sandbox/rpc/command-client.d.ts +0 -28
  211. package/dist/core/sandbox/rpc/command-client.d.ts.map +0 -1
  212. package/dist/core/sandbox/rpc/command-client.js +0 -86
  213. package/dist/core/sandbox/rpc/command-client.js.map +0 -1
  214. package/dist/core/sandbox/rpc/command-proxy.d.ts +0 -48
  215. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +0 -1
  216. package/dist/core/sandbox/rpc/command-proxy.js +0 -120
  217. package/dist/core/sandbox/rpc/command-proxy.js.map +0 -1
  218. package/dist/core/sandbox/rpc/credential-client.d.ts +0 -20
  219. package/dist/core/sandbox/rpc/credential-client.d.ts.map +0 -1
  220. package/dist/core/sandbox/rpc/credential-client.js +0 -170
  221. package/dist/core/sandbox/rpc/credential-client.js.map +0 -1
  222. package/dist/core/sandbox/rpc/credential-proxy.d.ts +0 -44
  223. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +0 -1
  224. package/dist/core/sandbox/rpc/credential-proxy.js +0 -287
  225. package/dist/core/sandbox/rpc/credential-proxy.js.map +0 -1
  226. package/dist/core/sandbox/rpc/framing.d.ts +0 -35
  227. package/dist/core/sandbox/rpc/framing.d.ts.map +0 -1
  228. package/dist/core/sandbox/rpc/framing.js +0 -114
  229. package/dist/core/sandbox/rpc/framing.js.map +0 -1
  230. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +0 -47
  231. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +0 -1
  232. package/dist/core/sandbox/rpc/git-credential-helper.js +0 -222
  233. package/dist/core/sandbox/rpc/git-credential-helper.js.map +0 -1
  234. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +0 -66
  235. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +0 -1
  236. package/dist/core/sandbox/rpc/git-credential-proxy.js +0 -209
  237. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +0 -1
  238. package/dist/core/sandbox/supervisor-temp.d.ts +0 -18
  239. package/dist/core/sandbox/supervisor-temp.d.ts.map +0 -1
  240. package/dist/core/sandbox/supervisor-temp.js +0 -21
  241. package/dist/core/sandbox/supervisor-temp.js.map +0 -1
  242. package/dist/core/sandbox/supervisor.d.ts +0 -62
  243. package/dist/core/sandbox/supervisor.d.ts.map +0 -1
  244. package/dist/core/sandbox/supervisor.js +0 -32
  245. package/dist/core/sandbox/supervisor.js.map +0 -1
  246. package/dist/core/sandbox/terminal-handoff.d.ts +0 -68
  247. package/dist/core/sandbox/terminal-handoff.d.ts.map +0 -1
  248. package/dist/core/sandbox/terminal-handoff.js +0 -221
  249. package/dist/core/sandbox/terminal-handoff.js.map +0 -1
  250. package/dist/core/sandbox/terminal-size.d.ts +0 -53
  251. package/dist/core/sandbox/terminal-size.d.ts.map +0 -1
  252. package/dist/core/sandbox/terminal-size.js +0 -126
  253. package/dist/core/sandbox/terminal-size.js.map +0 -1
  254. package/dist/core/sandbox/violations.d.ts +0 -23
  255. package/dist/core/sandbox/violations.d.ts.map +0 -1
  256. package/dist/core/sandbox/violations.js +0 -30
  257. package/dist/core/sandbox/violations.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"trust-manager.js","sourceRoot":"","sources":["../../src/core/trust-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { CONFIG_DIR_NAME } from \"../config.ts\";\nimport { canonicalizePath, resolvePath } from \"../utils/paths.ts\";\nimport { stripBom } from \"../utils/text.ts\";\n\nexport type ProjectTrustDecision = boolean | null;\n\nexport interface ProjectTrustStoreEntry {\n\tpath: string;\n\tdecision: boolean;\n}\n\nexport interface ProjectTrustUpdate {\n\tpath: string;\n\tdecision: ProjectTrustDecision;\n}\n\nexport interface ProjectTrustOption {\n\tlabel: string;\n\ttrusted: boolean;\n\tupdates: ProjectTrustUpdate[];\n\tsavedPath?: string;\n}\n\ntype TrustFile = Record<string, boolean | null | undefined>;\n\nconst TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [\n\t\"settings.json\",\n\t\"extensions\",\n\t\"skills\",\n\t\"prompts\",\n\t\"themes\",\n\t\"SYSTEM.md\",\n\t\"APPEND_SYSTEM.md\",\n] as const;\n\nfunction normalizeCwd(cwd: string): string {\n\treturn canonicalizePath(resolvePath(cwd));\n}\n\nfunction findNearestTrustEntry(data: TrustFile, cwd: string): ProjectTrustStoreEntry | null {\n\tlet currentDir = normalizeCwd(cwd);\n\twhile (true) {\n\t\tconst value = data[currentDir];\n\t\tif (value === true || value === false) {\n\t\t\treturn { path: currentDir, decision: value };\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\tif (parentDir === currentDir) {\n\t\t\treturn null;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport function getProjectTrustParentPath(cwd: string): string | undefined {\n\tconst trustPath = normalizeCwd(cwd);\n\tconst parentDir = dirname(trustPath);\n\treturn parentDir === trustPath ? undefined : parentDir;\n}\n\nexport function getProjectTrustOptions(cwd: string, options?: { includeSessionOnly?: boolean }): ProjectTrustOption[] {\n\tconst trustPath = normalizeCwd(cwd);\n\tconst trustOptions: ProjectTrustOption[] = [\n\t\t{ label: \"Trust\", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath },\n\t];\n\tconst parentPath = getProjectTrustParentPath(cwd);\n\tif (parentPath !== undefined) {\n\t\ttrustOptions.push({\n\t\t\tlabel: `Trust parent folder (${parentPath})`,\n\t\t\ttrusted: true,\n\t\t\tupdates: [\n\t\t\t\t{ path: parentPath, decision: true },\n\t\t\t\t{ path: trustPath, decision: null },\n\t\t\t],\n\t\t\tsavedPath: parentPath,\n\t\t});\n\t}\n\tif (options?.includeSessionOnly) {\n\t\ttrustOptions.push({ label: \"Trust (this session only)\", trusted: true, updates: [] });\n\t}\n\ttrustOptions.push({\n\t\tlabel: \"Do not trust\",\n\t\ttrusted: false,\n\t\tupdates: [{ path: trustPath, decision: false }],\n\t\tsavedPath: trustPath,\n\t});\n\tif (options?.includeSessionOnly) {\n\t\ttrustOptions.push({ label: \"Do not trust (this session only)\", trusted: false, updates: [] });\n\t}\n\treturn trustOptions;\n}\n\nfunction readTrustFile(path: string): TrustFile {\n\tif (!existsSync(path)) {\n\t\treturn {};\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(stripBom(readFileSync(path, \"utf-8\")));\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthrow new Error(`Failed to read trust store ${path}: ${message}`);\n\t}\n\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new Error(`Invalid trust store ${path}: expected an object`);\n\t}\n\n\tconst data: TrustFile = {};\n\tfor (const [key, value] of Object.entries(parsed)) {\n\t\tif (value !== true && value !== false && value !== null) {\n\t\t\tthrow new Error(`Invalid trust store ${path}: value for ${JSON.stringify(key)} must be true, false, or null`);\n\t\t}\n\t\tdata[key] = value;\n\t}\n\treturn data;\n}\n\nfunction writeTrustFile(path: string, data: TrustFile): void {\n\tconst sorted: TrustFile = {};\n\tfor (const key of Object.keys(data).sort()) {\n\t\tconst value = data[key];\n\t\tif (value === true || value === false || value === null) {\n\t\t\tsorted[key] = value;\n\t\t}\n\t}\n\tmkdirSync(dirname(path), { recursive: true });\n\twriteFileSync(path, `${JSON.stringify(sorted, null, 2)}\\n`, \"utf-8\");\n}\n\nfunction acquireTrustLockSync(path: string): () => void {\n\tconst trustDir = dirname(path);\n\tmkdirSync(trustDir, { recursive: true });\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(trustDir, { realpath: false, lockfilePath: `${path}.lock` });\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t: undefined;\n\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Sleep synchronously to avoid changing trust store callers to async.\n\t\t\t}\n\t\t}\n\t}\n\n\tif (lastError instanceof Error) {\n\t\tthrow lastError;\n\t}\n\tthrow new Error(\"Failed to acquire trust store lock\");\n}\n\nfunction withTrustFileLock<T>(path: string, fn: () => T): T {\n\tconst release = acquireTrustLockSync(path);\n\ttry {\n\t\treturn fn();\n\t} finally {\n\t\trelease();\n\t}\n}\n\n/**\n * Returns true when cwd has project-local resources that must be gated by\n * project trust: trust-requiring entries under cwd/.pi, or .agents/skills in\n * cwd or one of its ancestors. Returns false when no such project resources\n * exist. The user/global ~/.agents/skills directory is always treated as a\n * trusted user resource and is ignored here, even when cwd is $HOME.\n */\nexport function hasTrustRequiringProjectResources(cwd: string): boolean {\n\tconst homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));\n\tconst userAgentsSkillsDir = join(homeDir, \".agents\", \"skills\");\n\tlet currentDir = canonicalizePath(resolvePath(cwd));\n\n\tconst configDir = join(currentDir, CONFIG_DIR_NAME);\n\tif (TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry)))) {\n\t\treturn true;\n\t}\n\n\twhile (true) {\n\t\tconst agentsSkillsDir = join(currentDir, \".agents\", \"skills\");\n\t\tif (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\tif (parentDir === currentDir) {\n\t\t\treturn false;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport class ProjectTrustStore {\n\tprivate trustPath: string;\n\n\tconstructor(agentDir: string) {\n\t\tthis.trustPath = join(resolvePath(agentDir), \"trust.json\");\n\t}\n\n\tget(cwd: string): ProjectTrustDecision {\n\t\treturn this.getEntry(cwd)?.decision ?? null;\n\t}\n\n\tgetEntry(cwd: string): ProjectTrustStoreEntry | null {\n\t\treturn withTrustFileLock(this.trustPath, () => {\n\t\t\tconst data = readTrustFile(this.trustPath);\n\t\t\treturn findNearestTrustEntry(data, cwd);\n\t\t});\n\t}\n\n\tset(cwd: string, decision: ProjectTrustDecision): void {\n\t\tthis.setMany([{ path: cwd, decision }]);\n\t}\n\n\tsetMany(decisions: ProjectTrustUpdate[]): void {\n\t\twithTrustFileLock(this.trustPath, () => {\n\t\t\tconst data = readTrustFile(this.trustPath);\n\t\t\tfor (const { path, decision } of decisions) {\n\t\t\t\tconst key = normalizeCwd(path);\n\t\t\t\tif (decision === null) {\n\t\t\t\t\tdelete data[key];\n\t\t\t\t} else {\n\t\t\t\t\tdata[key] = decision;\n\t\t\t\t}\n\t\t\t}\n\t\t\twriteTrustFile(this.trustPath, data);\n\t\t});\n\t}\n}\n"]}
1
+ {"version":3,"file":"trust-manager.js","sourceRoot":"","sources":["../../src/core/trust-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { canonicalizePath, resolvePath } from \"../utils/paths.ts\";\nimport { stripBom } from \"../utils/text.ts\";\nimport { permissionContentConfersAuthority } from \"./permissions/store.ts\";\nimport { PROJECT_RESOURCES, type ProjectResource, projectResourcePath } from \"./project-resources.ts\";\n\nexport type ProjectTrustDecision = boolean | null;\n\nexport interface ProjectTrustStoreEntry {\n\tpath: string;\n\tdecision: boolean;\n}\n\nexport interface ProjectTrustUpdate {\n\tpath: string;\n\tdecision: ProjectTrustDecision;\n}\n\nexport interface ProjectTrustOption {\n\tlabel: string;\n\ttrusted: boolean;\n\tupdates: ProjectTrustUpdate[];\n\tsavedPath?: string;\n}\n\ntype TrustFile = Record<string, boolean | null | undefined>;\n\nfunction resourceConfersAuthority(resource: ProjectResource, absolutePath: string): boolean {\n\tif (!existsSync(absolutePath)) {\n\t\t// `existsSync` follows links, so a broken link is indistinguishable from absence.\n\t\t// A checkout controls the link, and the loader's own lock path creates the target\n\t\t// on first write, so a dangling link is treated as conferring rather than skipped.\n\t\ttry {\n\t\t\tlstatSync(absolutePath);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\tif (resource.authority === \"presence\") return true;\n\t// An unreadable file is treated the same way unparseable content is, because both\n\t// leave us unable to say the resource grants nothing.\n\ttry {\n\t\t// The checkout controls this path, so it may point at a FIFO or a character device.\n\t\t// A synchronous read of one blocks the process before any prompt is rendered, which\n\t\t// turns a trust check into a denial of service. Anything but a regular file confers.\n\t\tif (!lstatSync(absolutePath).isFile()) return true;\n\t\treturn permissionContentConfersAuthority(stripBom(readFileSync(absolutePath, \"utf8\")));\n\t} catch {\n\t\treturn true;\n\t}\n}\n\nfunction normalizeCwd(cwd: string): string {\n\treturn canonicalizePath(resolvePath(cwd));\n}\n\nfunction findNearestTrustEntry(data: TrustFile, cwd: string): ProjectTrustStoreEntry | null {\n\tlet currentDir = normalizeCwd(cwd);\n\twhile (true) {\n\t\tconst value = data[currentDir];\n\t\tif (value === true || value === false) {\n\t\t\treturn { path: currentDir, decision: value };\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\tif (parentDir === currentDir) {\n\t\t\treturn null;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport function getProjectTrustParentPath(cwd: string): string | undefined {\n\tconst trustPath = normalizeCwd(cwd);\n\tconst parentDir = dirname(trustPath);\n\treturn parentDir === trustPath ? undefined : parentDir;\n}\n\nexport function getProjectTrustOptions(cwd: string, options?: { includeSessionOnly?: boolean }): ProjectTrustOption[] {\n\tconst trustPath = normalizeCwd(cwd);\n\tconst trustOptions: ProjectTrustOption[] = [\n\t\t{ label: \"Trust\", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath },\n\t];\n\tconst parentPath = getProjectTrustParentPath(cwd);\n\tif (parentPath !== undefined) {\n\t\ttrustOptions.push({\n\t\t\tlabel: `Trust parent folder (${parentPath})`,\n\t\t\ttrusted: true,\n\t\t\tupdates: [\n\t\t\t\t{ path: parentPath, decision: true },\n\t\t\t\t{ path: trustPath, decision: null },\n\t\t\t],\n\t\t\tsavedPath: parentPath,\n\t\t});\n\t}\n\tif (options?.includeSessionOnly) {\n\t\ttrustOptions.push({ label: \"Trust (this session only)\", trusted: true, updates: [] });\n\t}\n\ttrustOptions.push({\n\t\tlabel: \"Do not trust\",\n\t\ttrusted: false,\n\t\tupdates: [{ path: trustPath, decision: false }],\n\t\tsavedPath: trustPath,\n\t});\n\tif (options?.includeSessionOnly) {\n\t\ttrustOptions.push({ label: \"Do not trust (this session only)\", trusted: false, updates: [] });\n\t}\n\treturn trustOptions;\n}\n\nfunction readTrustFile(path: string): TrustFile {\n\tif (!existsSync(path)) {\n\t\treturn {};\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(stripBom(readFileSync(path, \"utf-8\")));\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthrow new Error(`Failed to read trust store ${path}: ${message}`);\n\t}\n\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new Error(`Invalid trust store ${path}: expected an object`);\n\t}\n\n\tconst data: TrustFile = {};\n\tfor (const [key, value] of Object.entries(parsed)) {\n\t\tif (value !== true && value !== false && value !== null) {\n\t\t\tthrow new Error(`Invalid trust store ${path}: value for ${JSON.stringify(key)} must be true, false, or null`);\n\t\t}\n\t\tdata[key] = value;\n\t}\n\treturn data;\n}\n\nfunction writeTrustFile(path: string, data: TrustFile): void {\n\tconst sorted: TrustFile = {};\n\tfor (const key of Object.keys(data).sort()) {\n\t\tconst value = data[key];\n\t\tif (value === true || value === false || value === null) {\n\t\t\tsorted[key] = value;\n\t\t}\n\t}\n\tmkdirSync(dirname(path), { recursive: true });\n\twriteFileSync(path, `${JSON.stringify(sorted, null, 2)}\\n`, \"utf-8\");\n}\n\nfunction acquireTrustLockSync(path: string): () => void {\n\tconst trustDir = dirname(path);\n\tmkdirSync(trustDir, { recursive: true });\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(trustDir, { realpath: false, lockfilePath: `${path}.lock` });\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t: undefined;\n\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Sleep synchronously to avoid changing trust store callers to async.\n\t\t\t}\n\t\t}\n\t}\n\n\tif (lastError instanceof Error) {\n\t\tthrow lastError;\n\t}\n\tthrow new Error(\"Failed to acquire trust store lock\");\n}\n\nfunction withTrustFileLock<T>(path: string, fn: () => T): T {\n\tconst release = acquireTrustLockSync(path);\n\ttry {\n\t\treturn fn();\n\t} finally {\n\t\trelease();\n\t}\n}\n\n/**\n * Returns true when cwd supplies a project-controlled resource that must be gated by\n * project trust. The gated set is `PROJECT_RESOURCES`, which the loaders read too, plus\n * a `.agents/skills` directory in cwd or one of its ancestors. Returns false when no\n * such resource exists, or when the only ones present grant nothing (an empty\n * permissions file). The user's own ~/.agents/skills is a trusted user resource and is\n * ignored here, even when cwd is $HOME.\n */\nexport function hasTrustRequiringProjectResources(cwd: string): boolean {\n\tconst homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));\n\tconst userAgentsSkillsDir = join(homeDir, \".agents\", \"skills\");\n\tlet currentDir = canonicalizePath(resolvePath(cwd));\n\n\tfor (const resource of PROJECT_RESOURCES) {\n\t\tif (resourceConfersAuthority(resource, projectResourcePath(currentDir, resource))) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\twhile (true) {\n\t\tconst agentsSkillsDir = join(currentDir, \".agents\", \"skills\");\n\t\tif (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst parentDir = dirname(currentDir);\n\t\tif (parentDir === currentDir) {\n\t\t\treturn false;\n\t\t}\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport class ProjectTrustStore {\n\tprivate trustPath: string;\n\n\tconstructor(agentDir: string) {\n\t\tthis.trustPath = join(resolvePath(agentDir), \"trust.json\");\n\t}\n\n\tget(cwd: string): ProjectTrustDecision {\n\t\treturn this.getEntry(cwd)?.decision ?? null;\n\t}\n\n\tgetEntry(cwd: string): ProjectTrustStoreEntry | null {\n\t\treturn withTrustFileLock(this.trustPath, () => {\n\t\t\tconst data = readTrustFile(this.trustPath);\n\t\t\treturn findNearestTrustEntry(data, cwd);\n\t\t});\n\t}\n\n\tset(cwd: string, decision: ProjectTrustDecision): void {\n\t\tthis.setMany([{ path: cwd, decision }]);\n\t}\n\n\tsetMany(decisions: ProjectTrustUpdate[]): void {\n\t\twithTrustFileLock(this.trustPath, () => {\n\t\t\tconst data = readTrustFile(this.trustPath);\n\t\t\tfor (const { path, decision } of decisions) {\n\t\t\t\tconst key = normalizeCwd(path);\n\t\t\t\tif (decision === null) {\n\t\t\t\t\tdelete data[key];\n\t\t\t\t} else {\n\t\t\t\t\tdata[key] = decision;\n\t\t\t\t}\n\t\t\t}\n\t\t\twriteTrustFile(this.trustPath, data);\n\t\t});\n\t}\n}\n"]}
@@ -22,16 +22,6 @@ export declare const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId;
22
22
  * the repository is what the credential store is for.
23
23
  */
24
24
  export declare function webSearchSettingsError(settings?: WebSearchSettings): string | undefined;
25
- /**
26
- * The host `web_search` needs on the sandbox allowlist, or undefined when no
27
- * credential is configured for the selected backend.
28
- *
29
- * Deliberately checks whether the key *reference* resolves rather than resolving it:
30
- * the supervisor calls this on every launch, and a `!command` reference would
31
- * otherwise spawn a shell before the child even starts. That check is an environment
32
- * lookup for a `$VAR` reference and free for a command reference.
33
- */
34
- export declare function resolveWebSearchHost(settings: WebSearchSettings | undefined, env?: Record<string, string>, authPath?: string): string | undefined;
35
25
  /**
36
26
  * Build the configured `web_search` backend, or undefined when no credential
37
27
  * resolves.
@@ -1 +1 @@
1
- {"version":3,"file":"web-search-provider.d.ts","sourceRoot":"","sources":["../../src/core/web-search-provider.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAGjE,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAsCxC;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,mBAA2B,CAAC;AAwBtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAQvF;AAiBD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,QAAQ,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAGpB;AAED;;;;;;;;GAQG;AACH;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAIjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAChD,YAAY,EAAE,MAAM,iBAAiB,GAAG,SAAS,EACjD,QAAQ,CAAC,EAAE,MAAM,GACf,mBAAmB,CAarB;AAED,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,QAAQ,CAAC,EAAE,MAAM,GACf,mBAAmB,GAAG,SAAS,CASjC","sourcesContent":["import { readStoredCredential } from \"./auth-storage.ts\";\nimport {\n\tgetConfigValueEnvVarNames,\n\tisCommandConfigValue,\n\tisConfigValueConfigured,\n\tresolveConfigValue,\n} from \"./resolve-config-value.ts\";\nimport type { WebSearchSettings } from \"./settings-manager.ts\";\nimport type { WebSearchOperations } from \"./tools/web-search.ts\";\nimport { createExaWebSearchOperations, EXA_SEARCH_HOST } from \"./tools/web-search-exa.ts\";\n\nexport type WebSearchProviderId = \"exa\";\n\n/**\n * One entry per backend `web_search` can speak to.\n *\n * A table rather than a branch because the swap is the point: the `web_search` tool\n * owns the contract and the permission grammar, the backend owns nothing but an HTTP\n * call, and adding the second backend should be a row here rather than a new code\n * path through the resolver.\n */\ninterface WebSearchProviderDefinition {\n\t/**\n\t * Config-value reference used when settings name no key. Held as a reference,\n\t * never a literal: a resolved key must never reach a file the settings loader\n\t * writes back.\n\t */\n\treadonly defaultApiKeyReference: string;\n\t/** Host the sandbox must allow before this backend can reach anything. */\n\treadonly host: string;\n\t/** `auth.json` key holding a credential saved from the settings panel. */\n\treadonly credentialId: string;\n\tcreate(options: {\n\t\tapiKey: string;\n\t\tendpoint?: string;\n\t\tnumResults?: number;\n\t\tsnippetMaxCharacters?: number;\n\t}): WebSearchOperations;\n}\n\nconst WEB_SEARCH_PROVIDERS: Record<WebSearchProviderId, WebSearchProviderDefinition> = {\n\texa: {\n\t\tdefaultApiKeyReference: \"$EXA_API_KEY\",\n\t\thost: EXA_SEARCH_HOST,\n\t\tcredentialId: \"exa\",\n\t\tcreate: createExaWebSearchOperations,\n\t},\n};\n\n/**\n * Backend used when settings name none. `web_search` still reports itself\n * unconfigured unless this backend's key resolves, so the default selects which\n * credential is looked for, not whether the tool reaches the network.\n */\nexport const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = \"exa\";\n\nfunction definitionFor(settings: WebSearchSettings | undefined): WebSearchProviderDefinition {\n\treturn WEB_SEARCH_PROVIDERS[settings?.provider ?? DEFAULT_WEB_SEARCH_PROVIDER];\n}\n\n/** `auth.json` key holding a stored credential for the selected backend. */\nfunction webSearchCredentialId(settings?: WebSearchSettings): string {\n\treturn definitionFor(settings).credentialId;\n}\n\n/**\n * The reference stored by the settings panel, or undefined when nothing is saved.\n *\n * Whatever was saved stays a reference rather than being resolved here, so the one\n * resolution below covers a literal key, a `$VAR`, and a `!command` alike. An OAuth\n * credential under the same key is not an API key and is ignored rather than coerced.\n */\nfunction storedApiKeyReference(settings: WebSearchSettings | undefined, authPath?: string): string | undefined {\n\tconst credential = readStoredCredential(webSearchCredentialId(settings), authPath);\n\tif (credential?.type !== \"api_key\") return undefined;\n\treturn credential.key?.trim() || undefined;\n}\n\n/**\n * Reject a literal secret configured in settings.\n *\n * `webSearch.apiKey` is read from project settings as well as global ones, and this\n * repository deliberately does not gitignore `<project>/.apex-code/` -- that directory\n * is meant to be versioned and shared with a team. A literal key written there would be\n * committed and handed to everyone who clones the repository.\n *\n * A reference (`$VAR`, `${VAR}`, `!command`) names where the secret lives without\n * containing it, so it is safe to commit. AGENTS.md already states that no credential\n * belongs in a file the settings loader writes; this makes that rule enforced rather\n * than documented. `auth.json` still accepts literals, because holding secrets outside\n * the repository is what the credential store is for.\n */\nexport function webSearchSettingsError(settings?: WebSearchSettings): string | undefined {\n\tconst configured = settings?.apiKey?.trim();\n\tif (!configured) return undefined;\n\tif (isCommandConfigValue(configured)) return undefined;\n\tif (getConfigValueEnvVarNames(configured).length > 0) return undefined;\n\t// The value itself is never echoed: this message reaches the model, and from there a\n\t// transcript. Naming the setting is enough to act on.\n\treturn `webSearch.apiKey must be a reference, not a literal key, because project settings are versioned and shared. Use \"$EXA_API_KEY\", \"\\${YOUR_VAR}\", or \"!your-command\", and keep the secret in your environment or credential store.`;\n}\n\n/**\n * Pick one reference, then resolve it once.\n *\n * Order is explicit configuration, then saved credential, then ambient environment.\n * A user who just typed a key into the settings dialog expects it to beat a stale\n * export they have forgotten about, and an explicit `webSearch.apiKey` in settings is\n * a direct instruction that should beat both.\n */\nfunction apiKeyReferenceFor(settings: WebSearchSettings | undefined, authPath?: string): string {\n\t// A refused literal falls through rather than being honoured, so a committed key is\n\t// inert instead of quietly working for whoever cloned the repository.\n\tconst configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();\n\treturn configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;\n}\n\n/**\n * The host `web_search` needs on the sandbox allowlist, or undefined when no\n * credential is configured for the selected backend.\n *\n * Deliberately checks whether the key *reference* resolves rather than resolving it:\n * the supervisor calls this on every launch, and a `!command` reference would\n * otherwise spawn a shell before the child even starts. That check is an environment\n * lookup for a `$VAR` reference and free for a command reference.\n */\nexport function resolveWebSearchHost(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): string | undefined {\n\tconst definition = definitionFor(settings);\n\treturn isConfigValueConfigured(apiKeyReferenceFor(settings, authPath), env) ? definition.host : undefined;\n}\n\n/**\n * Build the configured `web_search` backend, or undefined when no credential\n * resolves.\n *\n * Undefined leaves the tool on its unconfigured operations, which throw a\n * model-readable \"not configured\" error. That is deliberately not the same thing as\n * hiding the tool: a session that silently lacks `web_search` cannot tell the model\n * why, and the throw names the missing piece.\n */\n/**\n * What to tell the model, and through it the user, when nothing is configured.\n *\n * Named here rather than in `web-search.ts` because only this module knows which\n * backend was selected and therefore which credential to name. The audience is a\n * person who asked the agent to search something, so it names the two things they\n * can actually do, not the SDK option an embedder would pass.\n */\nexport function unconfiguredWebSearchMessage(settings?: WebSearchSettings): string {\n\tconst definition = definitionFor(settings);\n\tconst variable = definition.defaultApiKeyReference.replace(/^\\$/, \"\");\n\treturn `web_search has no API key configured. Set ${variable} in the environment, or run /settings and add a ${definition.credentialId} key.`;\n}\n\n/**\n * A `web_search` backend that resolves its credential on each call.\n *\n * The session builds its tool registry once, at construction, so operations captured\n * there would pin whatever was configured at startup. A key saved from the settings\n * panel would then appear to save and do nothing until restart, which is worse than\n * having no settings row at all. Resolving per call costs one small file read and\n * keeps `resolveConfigValue`'s cache for command references.\n */\nexport function createDeferredWebSearchOperations(\n\treadSettings: () => WebSearchSettings | undefined,\n\tauthPath?: string,\n): WebSearchOperations {\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tconst settings = readSettings();\n\t\t\t// Reported ahead of the unconfigured message: \"no key\" and \"your key is in the\n\t\t\t// wrong place\" need different fixes.\n\t\t\tconst settingsError = webSearchSettingsError(settings);\n\t\t\tif (settingsError) throw new Error(settingsError);\n\t\t\tconst operations = resolveWebSearchOperations(settings, undefined, authPath);\n\t\t\tif (!operations) throw new Error(unconfiguredWebSearchMessage(settings));\n\t\t\treturn operations.search(query, signal);\n\t\t},\n\t};\n}\n\nexport function resolveWebSearchOperations(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): WebSearchOperations | undefined {\n\tconst apiKey = resolveConfigValue(apiKeyReferenceFor(settings, authPath), env)?.trim();\n\tif (!apiKey) return undefined;\n\treturn definitionFor(settings).create({\n\t\tapiKey,\n\t\t...(settings?.endpoint === undefined ? {} : { endpoint: settings.endpoint }),\n\t\t...(settings?.numResults === undefined ? {} : { numResults: settings.numResults }),\n\t\t...(settings?.snippetMaxCharacters === undefined ? {} : { snippetMaxCharacters: settings.snippetMaxCharacters }),\n\t});\n}\n"]}
1
+ {"version":3,"file":"web-search-provider.d.ts","sourceRoot":"","sources":["../../src/core/web-search-provider.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAGjE,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAmCxC;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,mBAA2B,CAAC;AAwBtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAQvF;AAiBD;;;;;;;;GAQG;AACH;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAIjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAChD,YAAY,EAAE,MAAM,iBAAiB,GAAG,SAAS,EACjD,QAAQ,CAAC,EAAE,MAAM,GACf,mBAAmB,CAarB;AAED,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,QAAQ,CAAC,EAAE,MAAM,GACf,mBAAmB,GAAG,SAAS,CASjC","sourcesContent":["import { readStoredCredential } from \"./auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue, resolveConfigValue } from \"./resolve-config-value.ts\";\nimport type { WebSearchSettings } from \"./settings-manager.ts\";\nimport type { WebSearchOperations } from \"./tools/web-search.ts\";\nimport { createExaWebSearchOperations } from \"./tools/web-search-exa.ts\";\n\nexport type WebSearchProviderId = \"exa\";\n\n/**\n * One entry per backend `web_search` can speak to.\n *\n * A table rather than a branch because the swap is the point: the `web_search` tool\n * owns the contract and the permission grammar, the backend owns nothing but an HTTP\n * call, and adding the second backend should be a row here rather than a new code\n * path through the resolver.\n */\ninterface WebSearchProviderDefinition {\n\t/**\n\t * Config-value reference used when settings name no key. Held as a reference,\n\t * never a literal: a resolved key must never reach a file the settings loader\n\t * writes back.\n\t */\n\treadonly defaultApiKeyReference: string;\n\t/** `auth.json` key holding a credential saved from the settings panel. */\n\treadonly credentialId: string;\n\tcreate(options: {\n\t\tapiKey: string;\n\t\tendpoint?: string;\n\t\tnumResults?: number;\n\t\tsnippetMaxCharacters?: number;\n\t}): WebSearchOperations;\n}\n\nconst WEB_SEARCH_PROVIDERS: Record<WebSearchProviderId, WebSearchProviderDefinition> = {\n\texa: {\n\t\tdefaultApiKeyReference: \"$EXA_API_KEY\",\n\t\tcredentialId: \"exa\",\n\t\tcreate: createExaWebSearchOperations,\n\t},\n};\n\n/**\n * Backend used when settings name none. `web_search` still reports itself\n * unconfigured unless this backend's key resolves, so the default selects which\n * credential is looked for, not whether the tool reaches the network.\n */\nexport const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = \"exa\";\n\nfunction definitionFor(settings: WebSearchSettings | undefined): WebSearchProviderDefinition {\n\treturn WEB_SEARCH_PROVIDERS[settings?.provider ?? DEFAULT_WEB_SEARCH_PROVIDER];\n}\n\n/** `auth.json` key holding a stored credential for the selected backend. */\nfunction webSearchCredentialId(settings?: WebSearchSettings): string {\n\treturn definitionFor(settings).credentialId;\n}\n\n/**\n * The reference stored by the settings panel, or undefined when nothing is saved.\n *\n * Whatever was saved stays a reference rather than being resolved here, so the one\n * resolution below covers a literal key, a `$VAR`, and a `!command` alike. An OAuth\n * credential under the same key is not an API key and is ignored rather than coerced.\n */\nfunction storedApiKeyReference(settings: WebSearchSettings | undefined, authPath?: string): string | undefined {\n\tconst credential = readStoredCredential(webSearchCredentialId(settings), authPath);\n\tif (credential?.type !== \"api_key\") return undefined;\n\treturn credential.key?.trim() || undefined;\n}\n\n/**\n * Reject a literal secret configured in settings.\n *\n * `webSearch.apiKey` is read from project settings as well as global ones, and this\n * repository deliberately does not gitignore `<project>/.apex-code/` -- that directory\n * is meant to be versioned and shared with a team. A literal key written there would be\n * committed and handed to everyone who clones the repository.\n *\n * A reference (`$VAR`, `${VAR}`, `!command`) names where the secret lives without\n * containing it, so it is safe to commit. AGENTS.md already states that no credential\n * belongs in a file the settings loader writes; this makes that rule enforced rather\n * than documented. `auth.json` still accepts literals, because holding secrets outside\n * the repository is what the credential store is for.\n */\nexport function webSearchSettingsError(settings?: WebSearchSettings): string | undefined {\n\tconst configured = settings?.apiKey?.trim();\n\tif (!configured) return undefined;\n\tif (isCommandConfigValue(configured)) return undefined;\n\tif (getConfigValueEnvVarNames(configured).length > 0) return undefined;\n\t// The value itself is never echoed: this message reaches the model, and from there a\n\t// transcript. Naming the setting is enough to act on.\n\treturn `webSearch.apiKey must be a reference, not a literal key, because project settings are versioned and shared. Use \"$EXA_API_KEY\", \"\\${YOUR_VAR}\", or \"!your-command\", and keep the secret in your environment or credential store.`;\n}\n\n/**\n * Pick one reference, then resolve it once.\n *\n * Order is explicit configuration, then saved credential, then ambient environment.\n * A user who just typed a key into the settings dialog expects it to beat a stale\n * export they have forgotten about, and an explicit `webSearch.apiKey` in settings is\n * a direct instruction that should beat both.\n */\nfunction apiKeyReferenceFor(settings: WebSearchSettings | undefined, authPath?: string): string {\n\t// A refused literal falls through rather than being honoured, so a committed key is\n\t// inert instead of quietly working for whoever cloned the repository.\n\tconst configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();\n\treturn configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;\n}\n\n/**\n * Build the configured `web_search` backend, or undefined when no credential\n * resolves.\n *\n * Undefined leaves the tool on its unconfigured operations, which throw a\n * model-readable \"not configured\" error. That is deliberately not the same thing as\n * hiding the tool: a session that silently lacks `web_search` cannot tell the model\n * why, and the throw names the missing piece.\n */\n/**\n * What to tell the model, and through it the user, when nothing is configured.\n *\n * Named here rather than in `web-search.ts` because only this module knows which\n * backend was selected and therefore which credential to name. The audience is a\n * person who asked the agent to search something, so it names the two things they\n * can actually do, not the SDK option an embedder would pass.\n */\nexport function unconfiguredWebSearchMessage(settings?: WebSearchSettings): string {\n\tconst definition = definitionFor(settings);\n\tconst variable = definition.defaultApiKeyReference.replace(/^\\$/, \"\");\n\treturn `web_search has no API key configured. Set ${variable} in the environment, or run /settings and add a ${definition.credentialId} key.`;\n}\n\n/**\n * A `web_search` backend that resolves its credential on each call.\n *\n * The session builds its tool registry once, at construction, so operations captured\n * there would pin whatever was configured at startup. A key saved from the settings\n * panel would then appear to save and do nothing until restart, which is worse than\n * having no settings row at all. Resolving per call costs one small file read and\n * keeps `resolveConfigValue`'s cache for command references.\n */\nexport function createDeferredWebSearchOperations(\n\treadSettings: () => WebSearchSettings | undefined,\n\tauthPath?: string,\n): WebSearchOperations {\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tconst settings = readSettings();\n\t\t\t// Reported ahead of the unconfigured message: \"no key\" and \"your key is in the\n\t\t\t// wrong place\" need different fixes.\n\t\t\tconst settingsError = webSearchSettingsError(settings);\n\t\t\tif (settingsError) throw new Error(settingsError);\n\t\t\tconst operations = resolveWebSearchOperations(settings, undefined, authPath);\n\t\t\tif (!operations) throw new Error(unconfiguredWebSearchMessage(settings));\n\t\t\treturn operations.search(query, signal);\n\t\t},\n\t};\n}\n\nexport function resolveWebSearchOperations(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): WebSearchOperations | undefined {\n\tconst apiKey = resolveConfigValue(apiKeyReferenceFor(settings, authPath), env)?.trim();\n\tif (!apiKey) return undefined;\n\treturn definitionFor(settings).create({\n\t\tapiKey,\n\t\t...(settings?.endpoint === undefined ? {} : { endpoint: settings.endpoint }),\n\t\t...(settings?.numResults === undefined ? {} : { numResults: settings.numResults }),\n\t\t...(settings?.snippetMaxCharacters === undefined ? {} : { snippetMaxCharacters: settings.snippetMaxCharacters }),\n\t});\n}\n"]}
@@ -1,10 +1,9 @@
1
1
  import { readStoredCredential } from "./auth-storage.js";
2
- import { getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, resolveConfigValue, } from "./resolve-config-value.js";
3
- import { createExaWebSearchOperations, EXA_SEARCH_HOST } from "./tools/web-search-exa.js";
2
+ import { getConfigValueEnvVarNames, isCommandConfigValue, resolveConfigValue } from "./resolve-config-value.js";
3
+ import { createExaWebSearchOperations } from "./tools/web-search-exa.js";
4
4
  const WEB_SEARCH_PROVIDERS = {
5
5
  exa: {
6
6
  defaultApiKeyReference: "$EXA_API_KEY",
7
- host: EXA_SEARCH_HOST,
8
7
  credentialId: "exa",
9
8
  create: createExaWebSearchOperations,
10
9
  },
@@ -75,19 +74,6 @@ function apiKeyReferenceFor(settings, authPath) {
75
74
  const configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();
76
75
  return configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;
77
76
  }
78
- /**
79
- * The host `web_search` needs on the sandbox allowlist, or undefined when no
80
- * credential is configured for the selected backend.
81
- *
82
- * Deliberately checks whether the key *reference* resolves rather than resolving it:
83
- * the supervisor calls this on every launch, and a `!command` reference would
84
- * otherwise spawn a shell before the child even starts. That check is an environment
85
- * lookup for a `$VAR` reference and free for a command reference.
86
- */
87
- export function resolveWebSearchHost(settings, env, authPath) {
88
- const definition = definitionFor(settings);
89
- return isConfigValueConfigured(apiKeyReferenceFor(settings, authPath), env) ? definition.host : undefined;
90
- }
91
77
  /**
92
78
  * Build the configured `web_search` backend, or undefined when no credential
93
79
  * resolves.
@@ -1 +1 @@
1
- {"version":3,"file":"web-search-provider.js","sourceRoot":"","sources":["../../src/core/web-search-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { readStoredCredential } from \"./auth-storage.ts\";\nimport {\n\tgetConfigValueEnvVarNames,\n\tisCommandConfigValue,\n\tisConfigValueConfigured,\n\tresolveConfigValue,\n} from \"./resolve-config-value.ts\";\nimport type { WebSearchSettings } from \"./settings-manager.ts\";\nimport type { WebSearchOperations } from \"./tools/web-search.ts\";\nimport { createExaWebSearchOperations, EXA_SEARCH_HOST } from \"./tools/web-search-exa.ts\";\n\nexport type WebSearchProviderId = \"exa\";\n\n/**\n * One entry per backend `web_search` can speak to.\n *\n * A table rather than a branch because the swap is the point: the `web_search` tool\n * owns the contract and the permission grammar, the backend owns nothing but an HTTP\n * call, and adding the second backend should be a row here rather than a new code\n * path through the resolver.\n */\ninterface WebSearchProviderDefinition {\n\t/**\n\t * Config-value reference used when settings name no key. Held as a reference,\n\t * never a literal: a resolved key must never reach a file the settings loader\n\t * writes back.\n\t */\n\treadonly defaultApiKeyReference: string;\n\t/** Host the sandbox must allow before this backend can reach anything. */\n\treadonly host: string;\n\t/** `auth.json` key holding a credential saved from the settings panel. */\n\treadonly credentialId: string;\n\tcreate(options: {\n\t\tapiKey: string;\n\t\tendpoint?: string;\n\t\tnumResults?: number;\n\t\tsnippetMaxCharacters?: number;\n\t}): WebSearchOperations;\n}\n\nconst WEB_SEARCH_PROVIDERS: Record<WebSearchProviderId, WebSearchProviderDefinition> = {\n\texa: {\n\t\tdefaultApiKeyReference: \"$EXA_API_KEY\",\n\t\thost: EXA_SEARCH_HOST,\n\t\tcredentialId: \"exa\",\n\t\tcreate: createExaWebSearchOperations,\n\t},\n};\n\n/**\n * Backend used when settings name none. `web_search` still reports itself\n * unconfigured unless this backend's key resolves, so the default selects which\n * credential is looked for, not whether the tool reaches the network.\n */\nexport const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = \"exa\";\n\nfunction definitionFor(settings: WebSearchSettings | undefined): WebSearchProviderDefinition {\n\treturn WEB_SEARCH_PROVIDERS[settings?.provider ?? DEFAULT_WEB_SEARCH_PROVIDER];\n}\n\n/** `auth.json` key holding a stored credential for the selected backend. */\nfunction webSearchCredentialId(settings?: WebSearchSettings): string {\n\treturn definitionFor(settings).credentialId;\n}\n\n/**\n * The reference stored by the settings panel, or undefined when nothing is saved.\n *\n * Whatever was saved stays a reference rather than being resolved here, so the one\n * resolution below covers a literal key, a `$VAR`, and a `!command` alike. An OAuth\n * credential under the same key is not an API key and is ignored rather than coerced.\n */\nfunction storedApiKeyReference(settings: WebSearchSettings | undefined, authPath?: string): string | undefined {\n\tconst credential = readStoredCredential(webSearchCredentialId(settings), authPath);\n\tif (credential?.type !== \"api_key\") return undefined;\n\treturn credential.key?.trim() || undefined;\n}\n\n/**\n * Reject a literal secret configured in settings.\n *\n * `webSearch.apiKey` is read from project settings as well as global ones, and this\n * repository deliberately does not gitignore `<project>/.apex-code/` -- that directory\n * is meant to be versioned and shared with a team. A literal key written there would be\n * committed and handed to everyone who clones the repository.\n *\n * A reference (`$VAR`, `${VAR}`, `!command`) names where the secret lives without\n * containing it, so it is safe to commit. AGENTS.md already states that no credential\n * belongs in a file the settings loader writes; this makes that rule enforced rather\n * than documented. `auth.json` still accepts literals, because holding secrets outside\n * the repository is what the credential store is for.\n */\nexport function webSearchSettingsError(settings?: WebSearchSettings): string | undefined {\n\tconst configured = settings?.apiKey?.trim();\n\tif (!configured) return undefined;\n\tif (isCommandConfigValue(configured)) return undefined;\n\tif (getConfigValueEnvVarNames(configured).length > 0) return undefined;\n\t// The value itself is never echoed: this message reaches the model, and from there a\n\t// transcript. Naming the setting is enough to act on.\n\treturn `webSearch.apiKey must be a reference, not a literal key, because project settings are versioned and shared. Use \"$EXA_API_KEY\", \"\\${YOUR_VAR}\", or \"!your-command\", and keep the secret in your environment or credential store.`;\n}\n\n/**\n * Pick one reference, then resolve it once.\n *\n * Order is explicit configuration, then saved credential, then ambient environment.\n * A user who just typed a key into the settings dialog expects it to beat a stale\n * export they have forgotten about, and an explicit `webSearch.apiKey` in settings is\n * a direct instruction that should beat both.\n */\nfunction apiKeyReferenceFor(settings: WebSearchSettings | undefined, authPath?: string): string {\n\t// A refused literal falls through rather than being honoured, so a committed key is\n\t// inert instead of quietly working for whoever cloned the repository.\n\tconst configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();\n\treturn configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;\n}\n\n/**\n * The host `web_search` needs on the sandbox allowlist, or undefined when no\n * credential is configured for the selected backend.\n *\n * Deliberately checks whether the key *reference* resolves rather than resolving it:\n * the supervisor calls this on every launch, and a `!command` reference would\n * otherwise spawn a shell before the child even starts. That check is an environment\n * lookup for a `$VAR` reference and free for a command reference.\n */\nexport function resolveWebSearchHost(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): string | undefined {\n\tconst definition = definitionFor(settings);\n\treturn isConfigValueConfigured(apiKeyReferenceFor(settings, authPath), env) ? definition.host : undefined;\n}\n\n/**\n * Build the configured `web_search` backend, or undefined when no credential\n * resolves.\n *\n * Undefined leaves the tool on its unconfigured operations, which throw a\n * model-readable \"not configured\" error. That is deliberately not the same thing as\n * hiding the tool: a session that silently lacks `web_search` cannot tell the model\n * why, and the throw names the missing piece.\n */\n/**\n * What to tell the model, and through it the user, when nothing is configured.\n *\n * Named here rather than in `web-search.ts` because only this module knows which\n * backend was selected and therefore which credential to name. The audience is a\n * person who asked the agent to search something, so it names the two things they\n * can actually do, not the SDK option an embedder would pass.\n */\nexport function unconfiguredWebSearchMessage(settings?: WebSearchSettings): string {\n\tconst definition = definitionFor(settings);\n\tconst variable = definition.defaultApiKeyReference.replace(/^\\$/, \"\");\n\treturn `web_search has no API key configured. Set ${variable} in the environment, or run /settings and add a ${definition.credentialId} key.`;\n}\n\n/**\n * A `web_search` backend that resolves its credential on each call.\n *\n * The session builds its tool registry once, at construction, so operations captured\n * there would pin whatever was configured at startup. A key saved from the settings\n * panel would then appear to save and do nothing until restart, which is worse than\n * having no settings row at all. Resolving per call costs one small file read and\n * keeps `resolveConfigValue`'s cache for command references.\n */\nexport function createDeferredWebSearchOperations(\n\treadSettings: () => WebSearchSettings | undefined,\n\tauthPath?: string,\n): WebSearchOperations {\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tconst settings = readSettings();\n\t\t\t// Reported ahead of the unconfigured message: \"no key\" and \"your key is in the\n\t\t\t// wrong place\" need different fixes.\n\t\t\tconst settingsError = webSearchSettingsError(settings);\n\t\t\tif (settingsError) throw new Error(settingsError);\n\t\t\tconst operations = resolveWebSearchOperations(settings, undefined, authPath);\n\t\t\tif (!operations) throw new Error(unconfiguredWebSearchMessage(settings));\n\t\t\treturn operations.search(query, signal);\n\t\t},\n\t};\n}\n\nexport function resolveWebSearchOperations(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): WebSearchOperations | undefined {\n\tconst apiKey = resolveConfigValue(apiKeyReferenceFor(settings, authPath), env)?.trim();\n\tif (!apiKey) return undefined;\n\treturn definitionFor(settings).create({\n\t\tapiKey,\n\t\t...(settings?.endpoint === undefined ? {} : { endpoint: settings.endpoint }),\n\t\t...(settings?.numResults === undefined ? {} : { numResults: settings.numResults }),\n\t\t...(settings?.snippetMaxCharacters === undefined ? {} : { snippetMaxCharacters: settings.snippetMaxCharacters }),\n\t});\n}\n"]}
1
+ {"version":3,"file":"web-search-provider.js","sourceRoot":"","sources":["../../src/core/web-search-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { readStoredCredential } from \"./auth-storage.ts\";\nimport { getConfigValueEnvVarNames, isCommandConfigValue, resolveConfigValue } from \"./resolve-config-value.ts\";\nimport type { WebSearchSettings } from \"./settings-manager.ts\";\nimport type { WebSearchOperations } from \"./tools/web-search.ts\";\nimport { createExaWebSearchOperations } from \"./tools/web-search-exa.ts\";\n\nexport type WebSearchProviderId = \"exa\";\n\n/**\n * One entry per backend `web_search` can speak to.\n *\n * A table rather than a branch because the swap is the point: the `web_search` tool\n * owns the contract and the permission grammar, the backend owns nothing but an HTTP\n * call, and adding the second backend should be a row here rather than a new code\n * path through the resolver.\n */\ninterface WebSearchProviderDefinition {\n\t/**\n\t * Config-value reference used when settings name no key. Held as a reference,\n\t * never a literal: a resolved key must never reach a file the settings loader\n\t * writes back.\n\t */\n\treadonly defaultApiKeyReference: string;\n\t/** `auth.json` key holding a credential saved from the settings panel. */\n\treadonly credentialId: string;\n\tcreate(options: {\n\t\tapiKey: string;\n\t\tendpoint?: string;\n\t\tnumResults?: number;\n\t\tsnippetMaxCharacters?: number;\n\t}): WebSearchOperations;\n}\n\nconst WEB_SEARCH_PROVIDERS: Record<WebSearchProviderId, WebSearchProviderDefinition> = {\n\texa: {\n\t\tdefaultApiKeyReference: \"$EXA_API_KEY\",\n\t\tcredentialId: \"exa\",\n\t\tcreate: createExaWebSearchOperations,\n\t},\n};\n\n/**\n * Backend used when settings name none. `web_search` still reports itself\n * unconfigured unless this backend's key resolves, so the default selects which\n * credential is looked for, not whether the tool reaches the network.\n */\nexport const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = \"exa\";\n\nfunction definitionFor(settings: WebSearchSettings | undefined): WebSearchProviderDefinition {\n\treturn WEB_SEARCH_PROVIDERS[settings?.provider ?? DEFAULT_WEB_SEARCH_PROVIDER];\n}\n\n/** `auth.json` key holding a stored credential for the selected backend. */\nfunction webSearchCredentialId(settings?: WebSearchSettings): string {\n\treturn definitionFor(settings).credentialId;\n}\n\n/**\n * The reference stored by the settings panel, or undefined when nothing is saved.\n *\n * Whatever was saved stays a reference rather than being resolved here, so the one\n * resolution below covers a literal key, a `$VAR`, and a `!command` alike. An OAuth\n * credential under the same key is not an API key and is ignored rather than coerced.\n */\nfunction storedApiKeyReference(settings: WebSearchSettings | undefined, authPath?: string): string | undefined {\n\tconst credential = readStoredCredential(webSearchCredentialId(settings), authPath);\n\tif (credential?.type !== \"api_key\") return undefined;\n\treturn credential.key?.trim() || undefined;\n}\n\n/**\n * Reject a literal secret configured in settings.\n *\n * `webSearch.apiKey` is read from project settings as well as global ones, and this\n * repository deliberately does not gitignore `<project>/.apex-code/` -- that directory\n * is meant to be versioned and shared with a team. A literal key written there would be\n * committed and handed to everyone who clones the repository.\n *\n * A reference (`$VAR`, `${VAR}`, `!command`) names where the secret lives without\n * containing it, so it is safe to commit. AGENTS.md already states that no credential\n * belongs in a file the settings loader writes; this makes that rule enforced rather\n * than documented. `auth.json` still accepts literals, because holding secrets outside\n * the repository is what the credential store is for.\n */\nexport function webSearchSettingsError(settings?: WebSearchSettings): string | undefined {\n\tconst configured = settings?.apiKey?.trim();\n\tif (!configured) return undefined;\n\tif (isCommandConfigValue(configured)) return undefined;\n\tif (getConfigValueEnvVarNames(configured).length > 0) return undefined;\n\t// The value itself is never echoed: this message reaches the model, and from there a\n\t// transcript. Naming the setting is enough to act on.\n\treturn `webSearch.apiKey must be a reference, not a literal key, because project settings are versioned and shared. Use \"$EXA_API_KEY\", \"\\${YOUR_VAR}\", or \"!your-command\", and keep the secret in your environment or credential store.`;\n}\n\n/**\n * Pick one reference, then resolve it once.\n *\n * Order is explicit configuration, then saved credential, then ambient environment.\n * A user who just typed a key into the settings dialog expects it to beat a stale\n * export they have forgotten about, and an explicit `webSearch.apiKey` in settings is\n * a direct instruction that should beat both.\n */\nfunction apiKeyReferenceFor(settings: WebSearchSettings | undefined, authPath?: string): string {\n\t// A refused literal falls through rather than being honoured, so a committed key is\n\t// inert instead of quietly working for whoever cloned the repository.\n\tconst configured = webSearchSettingsError(settings) ? undefined : settings?.apiKey?.trim();\n\treturn configured || storedApiKeyReference(settings, authPath) || definitionFor(settings).defaultApiKeyReference;\n}\n\n/**\n * Build the configured `web_search` backend, or undefined when no credential\n * resolves.\n *\n * Undefined leaves the tool on its unconfigured operations, which throw a\n * model-readable \"not configured\" error. That is deliberately not the same thing as\n * hiding the tool: a session that silently lacks `web_search` cannot tell the model\n * why, and the throw names the missing piece.\n */\n/**\n * What to tell the model, and through it the user, when nothing is configured.\n *\n * Named here rather than in `web-search.ts` because only this module knows which\n * backend was selected and therefore which credential to name. The audience is a\n * person who asked the agent to search something, so it names the two things they\n * can actually do, not the SDK option an embedder would pass.\n */\nexport function unconfiguredWebSearchMessage(settings?: WebSearchSettings): string {\n\tconst definition = definitionFor(settings);\n\tconst variable = definition.defaultApiKeyReference.replace(/^\\$/, \"\");\n\treturn `web_search has no API key configured. Set ${variable} in the environment, or run /settings and add a ${definition.credentialId} key.`;\n}\n\n/**\n * A `web_search` backend that resolves its credential on each call.\n *\n * The session builds its tool registry once, at construction, so operations captured\n * there would pin whatever was configured at startup. A key saved from the settings\n * panel would then appear to save and do nothing until restart, which is worse than\n * having no settings row at all. Resolving per call costs one small file read and\n * keeps `resolveConfigValue`'s cache for command references.\n */\nexport function createDeferredWebSearchOperations(\n\treadSettings: () => WebSearchSettings | undefined,\n\tauthPath?: string,\n): WebSearchOperations {\n\treturn {\n\t\tasync search(query, signal) {\n\t\t\tconst settings = readSettings();\n\t\t\t// Reported ahead of the unconfigured message: \"no key\" and \"your key is in the\n\t\t\t// wrong place\" need different fixes.\n\t\t\tconst settingsError = webSearchSettingsError(settings);\n\t\t\tif (settingsError) throw new Error(settingsError);\n\t\t\tconst operations = resolveWebSearchOperations(settings, undefined, authPath);\n\t\t\tif (!operations) throw new Error(unconfiguredWebSearchMessage(settings));\n\t\t\treturn operations.search(query, signal);\n\t\t},\n\t};\n}\n\nexport function resolveWebSearchOperations(\n\tsettings: WebSearchSettings | undefined,\n\tenv?: Record<string, string>,\n\tauthPath?: string,\n): WebSearchOperations | undefined {\n\tconst apiKey = resolveConfigValue(apiKeyReferenceFor(settings, authPath), env)?.trim();\n\tif (!apiKey) return undefined;\n\treturn definitionFor(settings).create({\n\t\tapiKey,\n\t\t...(settings?.endpoint === undefined ? {} : { endpoint: settings.endpoint }),\n\t\t...(settings?.numResults === undefined ? {} : { numResults: settings.numResults }),\n\t\t...(settings?.snippetMaxCharacters === undefined ? {} : { snippetMaxCharacters: settings.snippetMaxCharacters }),\n\t});\n}\n"]}
@@ -17,6 +17,22 @@ export interface GitObserveOptions {
17
17
  */
18
18
  excludePaths?: string[];
19
19
  }
20
+ export interface GitRunResult {
21
+ ok: boolean;
22
+ code: number | null;
23
+ stdout: Buffer;
24
+ stderr: string;
25
+ timedOut: boolean;
26
+ spawnError: boolean;
27
+ }
28
+ /**
29
+ * The workspace subsystem's one git exec seam: shell-less argv (no quoting
30
+ * bugs, Windows-safe), pinned `core.autocrlf=false`, optional index locks
31
+ * disabled, per-command timeout, abort support. The observer above uses it
32
+ * read-only; `git-worktree-owner.ts` reuses it for the worktree mutations so
33
+ * the subsystem never grows a second spawn pattern.
34
+ */
35
+ export declare function runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult>;
20
36
  /**
21
37
  * Observe a Git workspace without mutating it. Returns a version-1
22
38
  * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`
@@ -1 +1 @@
1
- {"version":3,"file":"git-observer.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/git-observer.ts"],"names":[],"mappings":"AAIA,OAAO,EAIN,KAAK,oBAAoB,EACzB,MAAM,YAAY,CAAC;AAoBpB,MAAM,WAAW,iBAAiB;IACjC,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAmLD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAClC,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;IAChB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC9B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACT,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACpC,GACC,kBAAkB,CA2BpB;AAED,wBAAsB,mBAAmB,CACxC,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAyI/B","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstatSync, readFileSync, readlinkSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, win32 as pathWin32, relative, sep } from \"node:path\";\nimport {\n\tWORKSPACE_RECORD_VERSION,\n\ttype WorkspaceStateCoverage,\n\ttype WorkspaceStatePath,\n\ttype WorkspaceStateRecord,\n} from \"./state.ts\";\n\n/**\n * Read-only Git workspace observation (spec 2026-09-01-harness-correctness-\n * and-workspace-state.md § 1).\n *\n * The adapter never mutates repository state: `HEAD`, the current branch, the\n * index, the worktree, the stash, and local config are only read. Optional\n * index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`\n * cannot refresh the on-disk index. It never walks the workspace; all path\n * knowledge comes from git. Symlinks are hashed as link text (what git\n * stores), never followed. Submodule (gitlink) paths carry no worktree hash.\n */\n\nconst DEFAULT_MAX_PATHS = 200;\nconst DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst GIT_CONFIG = [\"-c\", \"core.autocrlf=false\", \"-c\", \"status.renames=true\"];\nconst UNMERGED_XY = new Set([\"DD\", \"AU\", \"UD\", \"UA\", \"DU\", \"AA\", \"UU\"]);\n\nexport interface GitObserveOptions {\n\t/** Compute per-path content hashes of present files (default true). */\n\thashPaths?: boolean;\n\t/** Maximum number of reported paths (default 200). */\n\tmaxPaths?: number;\n\t/** Skip hashing files larger than this many bytes (default 5 MiB). */\n\tmaxHashBytes?: number;\n\t/** Per-command timeout in milliseconds (default 10 000). */\n\ttimeoutMs?: number;\n\t/** Cancels the observation; the record reports `failed`. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Repository-relative path prefixes to omit from `paths` (exact match or\n\t * directory prefix). Used for harness bookkeeping that lives inside the\n\t * workspace — e.g. the session directory — which is not workspace state.\n\t */\n\texcludePaths?: string[];\n}\n\ninterface GitRunResult {\n\tok: boolean;\n\tcode: number | null;\n\tstdout: Buffer;\n\tstderr: string;\n\ttimedOut: boolean;\n\tspawnError: boolean;\n}\n\nfunction runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult> {\n\treturn new Promise((resolve) => {\n\t\tlet stdout = Buffer.alloc(0);\n\t\tlet stderr = \"\";\n\t\tlet timedOut = false;\n\t\tlet settled = false;\n\t\tlet child: ChildProcess;\n\t\tconst finish = (code: number | null, spawnError: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve({\n\t\t\t\tok: !spawnError && !timedOut && code === 0,\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\ttimedOut,\n\t\t\t\tspawnError,\n\t\t\t});\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tchild = spawn(\"git\", [...GIT_CONFIG, ...args], {\n\t\t\t\tcwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t// Observation must leave the on-disk index untouched.\n\t\t\t\tenv: { ...process.env, GIT_OPTIONAL_LOCKS: \"0\" },\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ ok: false, code: null, stdout, stderr: \"\", timedOut: false, spawnError: true });\n\t\t\treturn;\n\t\t}\n\t\tif (signal) {\n\t\t\tif (signal.aborted) onAbort();\n\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstdout = Buffer.concat([stdout, chunk]);\n\t\t});\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString(\"utf-8\");\n\t\t});\n\t\tchild.on(\"error\", () => finish(null, true));\n\t\tchild.on(\"close\", (code) => finish(code, false));\n\t});\n}\n\nfunction emptyCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: false,\n\t\tstaged: false,\n\t\tunstaged: false,\n\t\tuntracked: false,\n\t\tignored: false,\n\t\thashes: false,\n\t\tpatch: false,\n\t};\n}\n\nfunction fullCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: true,\n\t\tstaged: true,\n\t\tunstaged: true,\n\t\tuntracked: true,\n\t\tignored: true,\n\t\thashes: true,\n\t\tpatch: false,\n\t};\n}\n\ninterface ParsedStatusPath {\n\tx: string;\n\ty: string;\n\tpath: string;\n\tpreviousPath?: string;\n}\n\nfunction parsePorcelainZ(stdout: Buffer): ParsedStatusPath[] {\n\tconst fields = stdout.toString(\"utf-8\").split(\"\\0\");\n\tconst parsed: ParsedStatusPath[] = [];\n\tlet i = 0;\n\twhile (i < fields.length) {\n\t\tconst record = fields[i++];\n\t\tif (!record || record.length < 4) continue;\n\t\tconst entry: ParsedStatusPath = { x: record[0], y: record[1], path: record.slice(3) };\n\t\tif (entry.x === \"R\" || entry.x === \"C\" || entry.y === \"R\" || entry.y === \"C\") {\n\t\t\tentry.previousPath = fields[i++];\n\t\t}\n\t\tparsed.push(entry);\n\t}\n\treturn parsed;\n}\n\nfunction classify(entry: ParsedStatusPath): {\n\tkind: WorkspaceStatePath[\"kind\"];\n\tstaged: boolean;\n\tunstaged: boolean;\n\tignored: boolean;\n} {\n\tconst { x, y } = entry;\n\tif (x === \"?\" && y === \"?\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: false };\n\t}\n\tif (x === \"!\" && y === \"!\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: true };\n\t}\n\tif (UNMERGED_XY.has(`${x}${y}`)) {\n\t\treturn {\n\t\t\tkind: x === \"D\" && y === \"D\" ? \"deleted\" : \"modified\",\n\t\t\tstaged: true,\n\t\t\tunstaged: true,\n\t\t\tignored: false,\n\t\t};\n\t}\n\tlet kind: WorkspaceStatePath[\"kind\"] = \"modified\";\n\tif (x === \"A\" || y === \"A\" || x === \"C\" || y === \"C\") kind = \"added\";\n\telse if (x === \"D\" || y === \"D\") kind = \"deleted\";\n\telse if (x === \"R\" || y === \"R\") kind = \"renamed\";\n\treturn {\n\t\tkind,\n\t\tstaged: x !== \" \" && x !== \"?\" && x !== \"!\",\n\t\tunstaged: y !== \" \" && y !== \"?\" && y !== \"!\",\n\t\tignored: false,\n\t};\n}\n\n/** Hash one workspace path: regular files by bytes, symlinks by link text. */\nfunction hashWorkspacePath(\n\ttoplevel: string,\n\tpath: string,\n\tmaxBytes: number,\n): { hash?: string; skipped: \"size\" | \"error\" | false } {\n\ttry {\n\t\tconst full = `${toplevel}/${path}`;\n\t\tconst stat = lstatSync(full);\n\t\tif (stat.isFile()) {\n\t\t\tif (stat.size > maxBytes) return { skipped: \"size\" };\n\t\t\treturn { hash: `sha256:${createHash(\"sha256\").update(readFileSync(full)).digest(\"hex\")}`, skipped: false };\n\t\t}\n\t\tif (stat.isSymbolicLink()) {\n\t\t\treturn {\n\t\t\t\thash: `sha256:${createHash(\"sha256\").update(readlinkSync(full)).digest(\"hex\")}`,\n\t\t\t\tskipped: false,\n\t\t\t};\n\t\t}\n\t\t// Directories (submodule gitlinks) have no worktree content to hash;\n\t\t// that is expected, not a coverage gap.\n\t\tif (stat.isDirectory()) return { skipped: false };\n\t\treturn { skipped: \"error\" };\n\t} catch {\n\t\treturn { skipped: \"error\" };\n\t}\n}\n\nfunction prefixRelative(prefix: string, path: string): string {\n\tif (prefix === \"\" || prefix === \".\") return path;\n\treturn `${prefix}/${path}`;\n}\n\n/**\n * Observe a Git workspace without mutating it. Returns a version-1\n * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`\n * when git could not run or the observation was cancelled, and `incomplete`\n * when a limit truncated the capture. `observed` never implies more than\n * `coverage` states.\n */\nexport interface ToplevelComparison {\n\t/** Whether the physically resolved workspace root sits under the toplevel. */\n\tinside: boolean;\n\t/** Root path relative to the toplevel; strip this prefix from porcelain paths. */\n\tprefix: string;\n\t/** Physically resolved toplevel; base for absolute-path work like hashing. */\n\ttoplevelReal: string;\n}\n\n/**\n * Decide containment of the workspace root inside git's toplevel after both\n * sides are physically resolved. Windows spellings diverge from what Node's\n * realpath returns for the same directory (short 8.3 names, drive-letter\n * case), so the win32 branch resolves through the native resolver, folds\n * case for the comparison, and uses win32 path semantics. The platform and\n * resolver are injectable so tests can exercise those branches anywhere.\n */\nexport function compareToplevel(\n\tworkspaceRoot: string,\n\ttoplevel: string,\n\toptions?: {\n\t\tplatform?: NodeJS.Platform;\n\t\trealpath?: (path: string) => string;\n\t},\n): ToplevelComparison {\n\tconst platform = options?.platform ?? process.platform;\n\tconst isWin32 = platform === \"win32\";\n\tconst realpath =\n\t\toptions?.realpath ??\n\t\t(isWin32\n\t\t\t? (p: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn realpathSync.native(p);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn realpathSync(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: realpathSync);\n\tconst rootReal = realpath(workspaceRoot);\n\tconst toplevelReal = realpath(toplevel);\n\tconst pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };\n\tconst fold = (p: string) => (isWin32 ? p.toLowerCase() : p);\n\tconst prefix = pathMod.relative(toplevelReal, rootReal);\n\tconst inside = !(prefix === \"..\" || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));\n\tif (isWin32 && inside) {\n\t\t// Recompute the strip prefix under case folding so a divergent drive or\n\t\t// directory case still yields the clean \"\" or relative form.\n\t\tconst folded = pathMod.relative(fold(toplevelReal), fold(rootReal));\n\t\treturn { inside, prefix: folded, toplevelReal };\n\t}\n\treturn { inside, prefix, toplevelReal };\n}\n\nexport async function observeWorkspaceGit(\n\tworkspaceRoot: string,\n\toptions?: GitObserveOptions,\n): Promise<WorkspaceStateRecord> {\n\tconst maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;\n\tconst maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst hashPaths = options?.hashPaths ?? true;\n\tconst signal = options?.signal;\n\tconst excludePaths = (options?.excludePaths ?? [])\n\t\t.map((p) => p.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\"))\n\t\t.filter((p) => p.length > 0);\n\tconst isExcluded = (repoPath: string): boolean =>\n\t\texcludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));\n\n\tconst warnings: string[] = [];\n\tconst failed = (message: string): WorkspaceStateRecord => ({\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: \"failed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\tcoverage: emptyCoverage(),\n\t\tpaths: [],\n\t\twarnings: [message],\n\t});\n\n\tif (signal?.aborted) {\n\t\treturn failed(\"workspace observation cancelled\");\n\t}\n\n\tconst head = await runGit(workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], timeoutMs, signal);\n\tif (head.spawnError) return failed(\"git could not be executed\");\n\tif (head.timedOut) return failed(\"workspace observation timed out\");\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (!head.ok) {\n\t\tif (/not a git repository/i.test(head.stderr) || head.code === 128) {\n\t\t\treturn {\n\t\t\t\t...failed(\"not a git repository\"),\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t};\n\t\t}\n\t\treturn failed(\"git rev-parse failed\");\n\t}\n\t// git resolves the toplevel physically (macOS /tmp -> /private/tmp,\n\t// Windows drive-letter case and short names), so containment is decided\n\t// through compareToplevel; otherwise a divergent spelling of the same\n\t// directory would fail the observation or corrupt every reported path.\n\tconst toplevel = head.stdout.toString(\"utf-8\").trim();\n\tconst comparison = compareToplevel(workspaceRoot, toplevel);\n\tif (!comparison.inside) {\n\t\treturn failed(\"workspace root is outside the repository toplevel\");\n\t}\n\tconst insidePrefix = comparison.prefix;\n\n\tconst headCommit = await runGit(workspaceRoot, [\"rev-parse\", \"--verify\", \"HEAD\"], timeoutMs, signal);\n\tconst mergeHead = await runGit(workspaceRoot, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"], timeoutMs, signal);\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (mergeHead.ok) warnings.push(\"merge in progress\");\n\tconst branch = await runGit(workspaceRoot, [\"branch\", \"--show-current\"], timeoutMs, signal);\n\n\tconst headCommitValue = headCommit.ok ? headCommit.stdout.toString(\"utf-8\").trim() : undefined;\n\tif (!headCommit.ok) warnings.push(\"unborn HEAD (no commits)\");\n\tconst branchValue = branch.ok ? branch.stdout.toString(\"utf-8\").trim() : \"\";\n\tif (branchValue === \"\") warnings.push(\"detached HEAD\");\n\n\t// Index baseline.\n\tconst index = await runGit(workspaceRoot, [\"ls-files\", \"--stage\", \"-z\"], timeoutMs, signal);\n\tif (!index.ok) return failed(\"git ls-files failed\");\n\tconst indexDigest = `sha256:${createHash(\"sha256\").update(index.stdout).digest(\"hex\")}`;\n\n\t// Staged / unstaged / untracked / ignored change sets.\n\tconst status = await runGit(\n\t\tworkspaceRoot,\n\t\t[\"status\", \"--porcelain\", \"-z\", \"--untracked-files=all\", \"--ignored=matching\"],\n\t\ttimeoutMs,\n\t\tsignal,\n\t);\n\tif (!status.ok) return failed(\"git status failed\");\n\n\tconst paths: WorkspaceStatePath[] = [];\n\tlet truncated = false;\n\tlet skippedHashes = 0;\n\tfor (const entry of parsePorcelainZ(status.stdout)) {\n\t\tif (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (paths.length >= maxPaths) {\n\t\t\ttruncated = true;\n\t\t\twarnings.push(`path list truncated at ${maxPaths} paths`);\n\t\t\tbreak;\n\t\t}\n\t\tconst { kind, staged, unstaged, ignored } = classify(entry);\n\t\tconst workspacePath: WorkspaceStatePath = {\n\t\t\tpath: prefixRelative(insidePrefix, entry.path),\n\t\t\tkind,\n\t\t\tstaged,\n\t\t\tunstaged,\n\t\t\t...(ignored ? { ignored: true } : {}),\n\t\t\t...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),\n\t\t};\n\t\tif (hashPaths && kind !== \"deleted\") {\n\t\t\tconst { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);\n\t\t\tif (hash) workspacePath.contentHash = hash;\n\t\t\telse if (skipped) skippedHashes++;\n\t\t}\n\t\tpaths.push(workspacePath);\n\t}\n\tif (skippedHashes > 0) warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);\n\n\t// Digest over exactly the paths this record includes, so a later\n\t// comparison can cheaply detect that the covered state moved.\n\tconst digestInput = paths\n\t\t.map(\n\t\t\t(p) =>\n\t\t\t\t`${p.path}\\0${p.kind}\\0${p.staged ? \"S\" : \"\"}${p.unstaged ? \"U\" : \"\"}${p.ignored ? \"I\" : \"\"}\\0${p.contentHash ?? \"\"}\\n`,\n\t\t)\n\t\t.sort()\n\t\t.join(\"\");\n\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: truncated || skippedHashes > 0 ? \"incomplete\" : \"observed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\t// `hashes` reflects the configured policy: a disabled hash pass is a\n\t\t// coverage statement, not an incomplete capture.\n\t\tcoverage: { ...fullCoverage(), hashes: hashPaths },\n\t\tbase: {\n\t\t\t...(headCommitValue ? { headCommit: headCommitValue } : {}),\n\t\t\t...(branchValue ? { branch: branchValue } : {}),\n\t\t\tindexDigest,\n\t\t\t...(truncated ? {} : { worktreeDigest: `sha256:${createHash(\"sha256\").update(digestInput).digest(\"hex\")}` }),\n\t\t},\n\t\tpaths,\n\t\twarnings,\n\t};\n}\n"]}
1
+ {"version":3,"file":"git-observer.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/git-observer.ts"],"names":[],"mappings":"AAIA,OAAO,EAIN,KAAK,oBAAoB,EACzB,MAAM,YAAY,CAAC;AAoBpB,MAAM,WAAW,iBAAiB;IACjC,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAqDlH;AAmHD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAClC,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;IAChB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC9B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACT,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACpC,GACC,kBAAkB,CA2BpB;AAED,wBAAsB,mBAAmB,CACxC,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAyI/B","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstatSync, readFileSync, readlinkSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, win32 as pathWin32, relative, sep } from \"node:path\";\nimport {\n\tWORKSPACE_RECORD_VERSION,\n\ttype WorkspaceStateCoverage,\n\ttype WorkspaceStatePath,\n\ttype WorkspaceStateRecord,\n} from \"./state.ts\";\n\n/**\n * Read-only Git workspace observation (spec 2026-09-01-harness-correctness-\n * and-workspace-state.md § 1).\n *\n * The adapter never mutates repository state: `HEAD`, the current branch, the\n * index, the worktree, the stash, and local config are only read. Optional\n * index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`\n * cannot refresh the on-disk index. It never walks the workspace; all path\n * knowledge comes from git. Symlinks are hashed as link text (what git\n * stores), never followed. Submodule (gitlink) paths carry no worktree hash.\n */\n\nconst DEFAULT_MAX_PATHS = 200;\nconst DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst GIT_CONFIG = [\"-c\", \"core.autocrlf=false\", \"-c\", \"status.renames=true\"];\nconst UNMERGED_XY = new Set([\"DD\", \"AU\", \"UD\", \"UA\", \"DU\", \"AA\", \"UU\"]);\n\nexport interface GitObserveOptions {\n\t/** Compute per-path content hashes of present files (default true). */\n\thashPaths?: boolean;\n\t/** Maximum number of reported paths (default 200). */\n\tmaxPaths?: number;\n\t/** Skip hashing files larger than this many bytes (default 5 MiB). */\n\tmaxHashBytes?: number;\n\t/** Per-command timeout in milliseconds (default 10 000). */\n\ttimeoutMs?: number;\n\t/** Cancels the observation; the record reports `failed`. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Repository-relative path prefixes to omit from `paths` (exact match or\n\t * directory prefix). Used for harness bookkeeping that lives inside the\n\t * workspace — e.g. the session directory — which is not workspace state.\n\t */\n\texcludePaths?: string[];\n}\n\nexport interface GitRunResult {\n\tok: boolean;\n\tcode: number | null;\n\tstdout: Buffer;\n\tstderr: string;\n\ttimedOut: boolean;\n\tspawnError: boolean;\n}\n\n/**\n * The workspace subsystem's one git exec seam: shell-less argv (no quoting\n * bugs, Windows-safe), pinned `core.autocrlf=false`, optional index locks\n * disabled, per-command timeout, abort support. The observer above uses it\n * read-only; `git-worktree-owner.ts` reuses it for the worktree mutations so\n * the subsystem never grows a second spawn pattern.\n */\nexport function runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult> {\n\treturn new Promise((resolve) => {\n\t\tlet stdout = Buffer.alloc(0);\n\t\tlet stderr = \"\";\n\t\tlet timedOut = false;\n\t\tlet settled = false;\n\t\tlet child: ChildProcess;\n\t\tconst finish = (code: number | null, spawnError: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve({\n\t\t\t\tok: !spawnError && !timedOut && code === 0,\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\ttimedOut,\n\t\t\t\tspawnError,\n\t\t\t});\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tchild = spawn(\"git\", [...GIT_CONFIG, ...args], {\n\t\t\t\tcwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t// Observation must leave the on-disk index untouched.\n\t\t\t\tenv: { ...process.env, GIT_OPTIONAL_LOCKS: \"0\" },\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ ok: false, code: null, stdout, stderr: \"\", timedOut: false, spawnError: true });\n\t\t\treturn;\n\t\t}\n\t\tif (signal) {\n\t\t\tif (signal.aborted) onAbort();\n\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstdout = Buffer.concat([stdout, chunk]);\n\t\t});\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString(\"utf-8\");\n\t\t});\n\t\tchild.on(\"error\", () => finish(null, true));\n\t\tchild.on(\"close\", (code) => finish(code, false));\n\t});\n}\n\nfunction emptyCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: false,\n\t\tstaged: false,\n\t\tunstaged: false,\n\t\tuntracked: false,\n\t\tignored: false,\n\t\thashes: false,\n\t\tpatch: false,\n\t};\n}\n\nfunction fullCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: true,\n\t\tstaged: true,\n\t\tunstaged: true,\n\t\tuntracked: true,\n\t\tignored: true,\n\t\thashes: true,\n\t\tpatch: false,\n\t};\n}\n\ninterface ParsedStatusPath {\n\tx: string;\n\ty: string;\n\tpath: string;\n\tpreviousPath?: string;\n}\n\nfunction parsePorcelainZ(stdout: Buffer): ParsedStatusPath[] {\n\tconst fields = stdout.toString(\"utf-8\").split(\"\\0\");\n\tconst parsed: ParsedStatusPath[] = [];\n\tlet i = 0;\n\twhile (i < fields.length) {\n\t\tconst record = fields[i++];\n\t\tif (!record || record.length < 4) continue;\n\t\tconst entry: ParsedStatusPath = { x: record[0], y: record[1], path: record.slice(3) };\n\t\tif (entry.x === \"R\" || entry.x === \"C\" || entry.y === \"R\" || entry.y === \"C\") {\n\t\t\tentry.previousPath = fields[i++];\n\t\t}\n\t\tparsed.push(entry);\n\t}\n\treturn parsed;\n}\n\nfunction classify(entry: ParsedStatusPath): {\n\tkind: WorkspaceStatePath[\"kind\"];\n\tstaged: boolean;\n\tunstaged: boolean;\n\tignored: boolean;\n} {\n\tconst { x, y } = entry;\n\tif (x === \"?\" && y === \"?\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: false };\n\t}\n\tif (x === \"!\" && y === \"!\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: true };\n\t}\n\tif (UNMERGED_XY.has(`${x}${y}`)) {\n\t\treturn {\n\t\t\tkind: x === \"D\" && y === \"D\" ? \"deleted\" : \"modified\",\n\t\t\tstaged: true,\n\t\t\tunstaged: true,\n\t\t\tignored: false,\n\t\t};\n\t}\n\tlet kind: WorkspaceStatePath[\"kind\"] = \"modified\";\n\tif (x === \"A\" || y === \"A\" || x === \"C\" || y === \"C\") kind = \"added\";\n\telse if (x === \"D\" || y === \"D\") kind = \"deleted\";\n\telse if (x === \"R\" || y === \"R\") kind = \"renamed\";\n\treturn {\n\t\tkind,\n\t\tstaged: x !== \" \" && x !== \"?\" && x !== \"!\",\n\t\tunstaged: y !== \" \" && y !== \"?\" && y !== \"!\",\n\t\tignored: false,\n\t};\n}\n\n/** Hash one workspace path: regular files by bytes, symlinks by link text. */\nfunction hashWorkspacePath(\n\ttoplevel: string,\n\tpath: string,\n\tmaxBytes: number,\n): { hash?: string; skipped: \"size\" | \"error\" | false } {\n\ttry {\n\t\tconst full = `${toplevel}/${path}`;\n\t\tconst stat = lstatSync(full);\n\t\tif (stat.isFile()) {\n\t\t\tif (stat.size > maxBytes) return { skipped: \"size\" };\n\t\t\treturn { hash: `sha256:${createHash(\"sha256\").update(readFileSync(full)).digest(\"hex\")}`, skipped: false };\n\t\t}\n\t\tif (stat.isSymbolicLink()) {\n\t\t\treturn {\n\t\t\t\thash: `sha256:${createHash(\"sha256\").update(readlinkSync(full)).digest(\"hex\")}`,\n\t\t\t\tskipped: false,\n\t\t\t};\n\t\t}\n\t\t// Directories (submodule gitlinks) have no worktree content to hash;\n\t\t// that is expected, not a coverage gap.\n\t\tif (stat.isDirectory()) return { skipped: false };\n\t\treturn { skipped: \"error\" };\n\t} catch {\n\t\treturn { skipped: \"error\" };\n\t}\n}\n\nfunction prefixRelative(prefix: string, path: string): string {\n\tif (prefix === \"\" || prefix === \".\") return path;\n\treturn `${prefix}/${path}`;\n}\n\n/**\n * Observe a Git workspace without mutating it. Returns a version-1\n * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`\n * when git could not run or the observation was cancelled, and `incomplete`\n * when a limit truncated the capture. `observed` never implies more than\n * `coverage` states.\n */\nexport interface ToplevelComparison {\n\t/** Whether the physically resolved workspace root sits under the toplevel. */\n\tinside: boolean;\n\t/** Root path relative to the toplevel; strip this prefix from porcelain paths. */\n\tprefix: string;\n\t/** Physically resolved toplevel; base for absolute-path work like hashing. */\n\ttoplevelReal: string;\n}\n\n/**\n * Decide containment of the workspace root inside git's toplevel after both\n * sides are physically resolved. Windows spellings diverge from what Node's\n * realpath returns for the same directory (short 8.3 names, drive-letter\n * case), so the win32 branch resolves through the native resolver, folds\n * case for the comparison, and uses win32 path semantics. The platform and\n * resolver are injectable so tests can exercise those branches anywhere.\n */\nexport function compareToplevel(\n\tworkspaceRoot: string,\n\ttoplevel: string,\n\toptions?: {\n\t\tplatform?: NodeJS.Platform;\n\t\trealpath?: (path: string) => string;\n\t},\n): ToplevelComparison {\n\tconst platform = options?.platform ?? process.platform;\n\tconst isWin32 = platform === \"win32\";\n\tconst realpath =\n\t\toptions?.realpath ??\n\t\t(isWin32\n\t\t\t? (p: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn realpathSync.native(p);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn realpathSync(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: realpathSync);\n\tconst rootReal = realpath(workspaceRoot);\n\tconst toplevelReal = realpath(toplevel);\n\tconst pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };\n\tconst fold = (p: string) => (isWin32 ? p.toLowerCase() : p);\n\tconst prefix = pathMod.relative(toplevelReal, rootReal);\n\tconst inside = !(prefix === \"..\" || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));\n\tif (isWin32 && inside) {\n\t\t// Recompute the strip prefix under case folding so a divergent drive or\n\t\t// directory case still yields the clean \"\" or relative form.\n\t\tconst folded = pathMod.relative(fold(toplevelReal), fold(rootReal));\n\t\treturn { inside, prefix: folded, toplevelReal };\n\t}\n\treturn { inside, prefix, toplevelReal };\n}\n\nexport async function observeWorkspaceGit(\n\tworkspaceRoot: string,\n\toptions?: GitObserveOptions,\n): Promise<WorkspaceStateRecord> {\n\tconst maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;\n\tconst maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst hashPaths = options?.hashPaths ?? true;\n\tconst signal = options?.signal;\n\tconst excludePaths = (options?.excludePaths ?? [])\n\t\t.map((p) => p.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\"))\n\t\t.filter((p) => p.length > 0);\n\tconst isExcluded = (repoPath: string): boolean =>\n\t\texcludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));\n\n\tconst warnings: string[] = [];\n\tconst failed = (message: string): WorkspaceStateRecord => ({\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: \"failed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\tcoverage: emptyCoverage(),\n\t\tpaths: [],\n\t\twarnings: [message],\n\t});\n\n\tif (signal?.aborted) {\n\t\treturn failed(\"workspace observation cancelled\");\n\t}\n\n\tconst head = await runGit(workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], timeoutMs, signal);\n\tif (head.spawnError) return failed(\"git could not be executed\");\n\tif (head.timedOut) return failed(\"workspace observation timed out\");\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (!head.ok) {\n\t\tif (/not a git repository/i.test(head.stderr) || head.code === 128) {\n\t\t\treturn {\n\t\t\t\t...failed(\"not a git repository\"),\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t};\n\t\t}\n\t\treturn failed(\"git rev-parse failed\");\n\t}\n\t// git resolves the toplevel physically (macOS /tmp -> /private/tmp,\n\t// Windows drive-letter case and short names), so containment is decided\n\t// through compareToplevel; otherwise a divergent spelling of the same\n\t// directory would fail the observation or corrupt every reported path.\n\tconst toplevel = head.stdout.toString(\"utf-8\").trim();\n\tconst comparison = compareToplevel(workspaceRoot, toplevel);\n\tif (!comparison.inside) {\n\t\treturn failed(\"workspace root is outside the repository toplevel\");\n\t}\n\tconst insidePrefix = comparison.prefix;\n\n\tconst headCommit = await runGit(workspaceRoot, [\"rev-parse\", \"--verify\", \"HEAD\"], timeoutMs, signal);\n\tconst mergeHead = await runGit(workspaceRoot, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"], timeoutMs, signal);\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (mergeHead.ok) warnings.push(\"merge in progress\");\n\tconst branch = await runGit(workspaceRoot, [\"branch\", \"--show-current\"], timeoutMs, signal);\n\n\tconst headCommitValue = headCommit.ok ? headCommit.stdout.toString(\"utf-8\").trim() : undefined;\n\tif (!headCommit.ok) warnings.push(\"unborn HEAD (no commits)\");\n\tconst branchValue = branch.ok ? branch.stdout.toString(\"utf-8\").trim() : \"\";\n\tif (branchValue === \"\") warnings.push(\"detached HEAD\");\n\n\t// Index baseline.\n\tconst index = await runGit(workspaceRoot, [\"ls-files\", \"--stage\", \"-z\"], timeoutMs, signal);\n\tif (!index.ok) return failed(\"git ls-files failed\");\n\tconst indexDigest = `sha256:${createHash(\"sha256\").update(index.stdout).digest(\"hex\")}`;\n\n\t// Staged / unstaged / untracked / ignored change sets.\n\tconst status = await runGit(\n\t\tworkspaceRoot,\n\t\t[\"status\", \"--porcelain\", \"-z\", \"--untracked-files=all\", \"--ignored=matching\"],\n\t\ttimeoutMs,\n\t\tsignal,\n\t);\n\tif (!status.ok) return failed(\"git status failed\");\n\n\tconst paths: WorkspaceStatePath[] = [];\n\tlet truncated = false;\n\tlet skippedHashes = 0;\n\tfor (const entry of parsePorcelainZ(status.stdout)) {\n\t\tif (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (paths.length >= maxPaths) {\n\t\t\ttruncated = true;\n\t\t\twarnings.push(`path list truncated at ${maxPaths} paths`);\n\t\t\tbreak;\n\t\t}\n\t\tconst { kind, staged, unstaged, ignored } = classify(entry);\n\t\tconst workspacePath: WorkspaceStatePath = {\n\t\t\tpath: prefixRelative(insidePrefix, entry.path),\n\t\t\tkind,\n\t\t\tstaged,\n\t\t\tunstaged,\n\t\t\t...(ignored ? { ignored: true } : {}),\n\t\t\t...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),\n\t\t};\n\t\tif (hashPaths && kind !== \"deleted\") {\n\t\t\tconst { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);\n\t\t\tif (hash) workspacePath.contentHash = hash;\n\t\t\telse if (skipped) skippedHashes++;\n\t\t}\n\t\tpaths.push(workspacePath);\n\t}\n\tif (skippedHashes > 0) warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);\n\n\t// Digest over exactly the paths this record includes, so a later\n\t// comparison can cheaply detect that the covered state moved.\n\tconst digestInput = paths\n\t\t.map(\n\t\t\t(p) =>\n\t\t\t\t`${p.path}\\0${p.kind}\\0${p.staged ? \"S\" : \"\"}${p.unstaged ? \"U\" : \"\"}${p.ignored ? \"I\" : \"\"}\\0${p.contentHash ?? \"\"}\\n`,\n\t\t)\n\t\t.sort()\n\t\t.join(\"\");\n\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: truncated || skippedHashes > 0 ? \"incomplete\" : \"observed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\t// `hashes` reflects the configured policy: a disabled hash pass is a\n\t\t// coverage statement, not an incomplete capture.\n\t\tcoverage: { ...fullCoverage(), hashes: hashPaths },\n\t\tbase: {\n\t\t\t...(headCommitValue ? { headCommit: headCommitValue } : {}),\n\t\t\t...(branchValue ? { branch: branchValue } : {}),\n\t\t\tindexDigest,\n\t\t\t...(truncated ? {} : { worktreeDigest: `sha256:${createHash(\"sha256\").update(digestInput).digest(\"hex\")}` }),\n\t\t},\n\t\tpaths,\n\t\twarnings,\n\t};\n}\n"]}
@@ -19,7 +19,14 @@ const DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;
19
19
  const DEFAULT_TIMEOUT_MS = 10_000;
20
20
  const GIT_CONFIG = ["-c", "core.autocrlf=false", "-c", "status.renames=true"];
21
21
  const UNMERGED_XY = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
22
- function runGit(cwd, args, timeoutMs, signal) {
22
+ /**
23
+ * The workspace subsystem's one git exec seam: shell-less argv (no quoting
24
+ * bugs, Windows-safe), pinned `core.autocrlf=false`, optional index locks
25
+ * disabled, per-command timeout, abort support. The observer above uses it
26
+ * read-only; `git-worktree-owner.ts` reuses it for the worktree mutations so
27
+ * the subsystem never grows a second spawn pattern.
28
+ */
29
+ export function runGit(cwd, args, timeoutMs, signal) {
23
30
  return new Promise((resolve) => {
24
31
  let stdout = Buffer.alloc(0);
25
32
  let stderr = "";
@@ -1 +1 @@
1
- {"version":3,"file":"git-observer.js","sourceRoot":"","sources":["../../../src/core/workspace/git-observer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstatSync, readFileSync, readlinkSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, win32 as pathWin32, relative, sep } from \"node:path\";\nimport {\n\tWORKSPACE_RECORD_VERSION,\n\ttype WorkspaceStateCoverage,\n\ttype WorkspaceStatePath,\n\ttype WorkspaceStateRecord,\n} from \"./state.ts\";\n\n/**\n * Read-only Git workspace observation (spec 2026-09-01-harness-correctness-\n * and-workspace-state.md § 1).\n *\n * The adapter never mutates repository state: `HEAD`, the current branch, the\n * index, the worktree, the stash, and local config are only read. Optional\n * index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`\n * cannot refresh the on-disk index. It never walks the workspace; all path\n * knowledge comes from git. Symlinks are hashed as link text (what git\n * stores), never followed. Submodule (gitlink) paths carry no worktree hash.\n */\n\nconst DEFAULT_MAX_PATHS = 200;\nconst DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst GIT_CONFIG = [\"-c\", \"core.autocrlf=false\", \"-c\", \"status.renames=true\"];\nconst UNMERGED_XY = new Set([\"DD\", \"AU\", \"UD\", \"UA\", \"DU\", \"AA\", \"UU\"]);\n\nexport interface GitObserveOptions {\n\t/** Compute per-path content hashes of present files (default true). */\n\thashPaths?: boolean;\n\t/** Maximum number of reported paths (default 200). */\n\tmaxPaths?: number;\n\t/** Skip hashing files larger than this many bytes (default 5 MiB). */\n\tmaxHashBytes?: number;\n\t/** Per-command timeout in milliseconds (default 10 000). */\n\ttimeoutMs?: number;\n\t/** Cancels the observation; the record reports `failed`. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Repository-relative path prefixes to omit from `paths` (exact match or\n\t * directory prefix). Used for harness bookkeeping that lives inside the\n\t * workspace — e.g. the session directory — which is not workspace state.\n\t */\n\texcludePaths?: string[];\n}\n\ninterface GitRunResult {\n\tok: boolean;\n\tcode: number | null;\n\tstdout: Buffer;\n\tstderr: string;\n\ttimedOut: boolean;\n\tspawnError: boolean;\n}\n\nfunction runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult> {\n\treturn new Promise((resolve) => {\n\t\tlet stdout = Buffer.alloc(0);\n\t\tlet stderr = \"\";\n\t\tlet timedOut = false;\n\t\tlet settled = false;\n\t\tlet child: ChildProcess;\n\t\tconst finish = (code: number | null, spawnError: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve({\n\t\t\t\tok: !spawnError && !timedOut && code === 0,\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\ttimedOut,\n\t\t\t\tspawnError,\n\t\t\t});\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tchild = spawn(\"git\", [...GIT_CONFIG, ...args], {\n\t\t\t\tcwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t// Observation must leave the on-disk index untouched.\n\t\t\t\tenv: { ...process.env, GIT_OPTIONAL_LOCKS: \"0\" },\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ ok: false, code: null, stdout, stderr: \"\", timedOut: false, spawnError: true });\n\t\t\treturn;\n\t\t}\n\t\tif (signal) {\n\t\t\tif (signal.aborted) onAbort();\n\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstdout = Buffer.concat([stdout, chunk]);\n\t\t});\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString(\"utf-8\");\n\t\t});\n\t\tchild.on(\"error\", () => finish(null, true));\n\t\tchild.on(\"close\", (code) => finish(code, false));\n\t});\n}\n\nfunction emptyCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: false,\n\t\tstaged: false,\n\t\tunstaged: false,\n\t\tuntracked: false,\n\t\tignored: false,\n\t\thashes: false,\n\t\tpatch: false,\n\t};\n}\n\nfunction fullCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: true,\n\t\tstaged: true,\n\t\tunstaged: true,\n\t\tuntracked: true,\n\t\tignored: true,\n\t\thashes: true,\n\t\tpatch: false,\n\t};\n}\n\ninterface ParsedStatusPath {\n\tx: string;\n\ty: string;\n\tpath: string;\n\tpreviousPath?: string;\n}\n\nfunction parsePorcelainZ(stdout: Buffer): ParsedStatusPath[] {\n\tconst fields = stdout.toString(\"utf-8\").split(\"\\0\");\n\tconst parsed: ParsedStatusPath[] = [];\n\tlet i = 0;\n\twhile (i < fields.length) {\n\t\tconst record = fields[i++];\n\t\tif (!record || record.length < 4) continue;\n\t\tconst entry: ParsedStatusPath = { x: record[0], y: record[1], path: record.slice(3) };\n\t\tif (entry.x === \"R\" || entry.x === \"C\" || entry.y === \"R\" || entry.y === \"C\") {\n\t\t\tentry.previousPath = fields[i++];\n\t\t}\n\t\tparsed.push(entry);\n\t}\n\treturn parsed;\n}\n\nfunction classify(entry: ParsedStatusPath): {\n\tkind: WorkspaceStatePath[\"kind\"];\n\tstaged: boolean;\n\tunstaged: boolean;\n\tignored: boolean;\n} {\n\tconst { x, y } = entry;\n\tif (x === \"?\" && y === \"?\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: false };\n\t}\n\tif (x === \"!\" && y === \"!\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: true };\n\t}\n\tif (UNMERGED_XY.has(`${x}${y}`)) {\n\t\treturn {\n\t\t\tkind: x === \"D\" && y === \"D\" ? \"deleted\" : \"modified\",\n\t\t\tstaged: true,\n\t\t\tunstaged: true,\n\t\t\tignored: false,\n\t\t};\n\t}\n\tlet kind: WorkspaceStatePath[\"kind\"] = \"modified\";\n\tif (x === \"A\" || y === \"A\" || x === \"C\" || y === \"C\") kind = \"added\";\n\telse if (x === \"D\" || y === \"D\") kind = \"deleted\";\n\telse if (x === \"R\" || y === \"R\") kind = \"renamed\";\n\treturn {\n\t\tkind,\n\t\tstaged: x !== \" \" && x !== \"?\" && x !== \"!\",\n\t\tunstaged: y !== \" \" && y !== \"?\" && y !== \"!\",\n\t\tignored: false,\n\t};\n}\n\n/** Hash one workspace path: regular files by bytes, symlinks by link text. */\nfunction hashWorkspacePath(\n\ttoplevel: string,\n\tpath: string,\n\tmaxBytes: number,\n): { hash?: string; skipped: \"size\" | \"error\" | false } {\n\ttry {\n\t\tconst full = `${toplevel}/${path}`;\n\t\tconst stat = lstatSync(full);\n\t\tif (stat.isFile()) {\n\t\t\tif (stat.size > maxBytes) return { skipped: \"size\" };\n\t\t\treturn { hash: `sha256:${createHash(\"sha256\").update(readFileSync(full)).digest(\"hex\")}`, skipped: false };\n\t\t}\n\t\tif (stat.isSymbolicLink()) {\n\t\t\treturn {\n\t\t\t\thash: `sha256:${createHash(\"sha256\").update(readlinkSync(full)).digest(\"hex\")}`,\n\t\t\t\tskipped: false,\n\t\t\t};\n\t\t}\n\t\t// Directories (submodule gitlinks) have no worktree content to hash;\n\t\t// that is expected, not a coverage gap.\n\t\tif (stat.isDirectory()) return { skipped: false };\n\t\treturn { skipped: \"error\" };\n\t} catch {\n\t\treturn { skipped: \"error\" };\n\t}\n}\n\nfunction prefixRelative(prefix: string, path: string): string {\n\tif (prefix === \"\" || prefix === \".\") return path;\n\treturn `${prefix}/${path}`;\n}\n\n/**\n * Observe a Git workspace without mutating it. Returns a version-1\n * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`\n * when git could not run or the observation was cancelled, and `incomplete`\n * when a limit truncated the capture. `observed` never implies more than\n * `coverage` states.\n */\nexport interface ToplevelComparison {\n\t/** Whether the physically resolved workspace root sits under the toplevel. */\n\tinside: boolean;\n\t/** Root path relative to the toplevel; strip this prefix from porcelain paths. */\n\tprefix: string;\n\t/** Physically resolved toplevel; base for absolute-path work like hashing. */\n\ttoplevelReal: string;\n}\n\n/**\n * Decide containment of the workspace root inside git's toplevel after both\n * sides are physically resolved. Windows spellings diverge from what Node's\n * realpath returns for the same directory (short 8.3 names, drive-letter\n * case), so the win32 branch resolves through the native resolver, folds\n * case for the comparison, and uses win32 path semantics. The platform and\n * resolver are injectable so tests can exercise those branches anywhere.\n */\nexport function compareToplevel(\n\tworkspaceRoot: string,\n\ttoplevel: string,\n\toptions?: {\n\t\tplatform?: NodeJS.Platform;\n\t\trealpath?: (path: string) => string;\n\t},\n): ToplevelComparison {\n\tconst platform = options?.platform ?? process.platform;\n\tconst isWin32 = platform === \"win32\";\n\tconst realpath =\n\t\toptions?.realpath ??\n\t\t(isWin32\n\t\t\t? (p: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn realpathSync.native(p);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn realpathSync(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: realpathSync);\n\tconst rootReal = realpath(workspaceRoot);\n\tconst toplevelReal = realpath(toplevel);\n\tconst pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };\n\tconst fold = (p: string) => (isWin32 ? p.toLowerCase() : p);\n\tconst prefix = pathMod.relative(toplevelReal, rootReal);\n\tconst inside = !(prefix === \"..\" || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));\n\tif (isWin32 && inside) {\n\t\t// Recompute the strip prefix under case folding so a divergent drive or\n\t\t// directory case still yields the clean \"\" or relative form.\n\t\tconst folded = pathMod.relative(fold(toplevelReal), fold(rootReal));\n\t\treturn { inside, prefix: folded, toplevelReal };\n\t}\n\treturn { inside, prefix, toplevelReal };\n}\n\nexport async function observeWorkspaceGit(\n\tworkspaceRoot: string,\n\toptions?: GitObserveOptions,\n): Promise<WorkspaceStateRecord> {\n\tconst maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;\n\tconst maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst hashPaths = options?.hashPaths ?? true;\n\tconst signal = options?.signal;\n\tconst excludePaths = (options?.excludePaths ?? [])\n\t\t.map((p) => p.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\"))\n\t\t.filter((p) => p.length > 0);\n\tconst isExcluded = (repoPath: string): boolean =>\n\t\texcludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));\n\n\tconst warnings: string[] = [];\n\tconst failed = (message: string): WorkspaceStateRecord => ({\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: \"failed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\tcoverage: emptyCoverage(),\n\t\tpaths: [],\n\t\twarnings: [message],\n\t});\n\n\tif (signal?.aborted) {\n\t\treturn failed(\"workspace observation cancelled\");\n\t}\n\n\tconst head = await runGit(workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], timeoutMs, signal);\n\tif (head.spawnError) return failed(\"git could not be executed\");\n\tif (head.timedOut) return failed(\"workspace observation timed out\");\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (!head.ok) {\n\t\tif (/not a git repository/i.test(head.stderr) || head.code === 128) {\n\t\t\treturn {\n\t\t\t\t...failed(\"not a git repository\"),\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t};\n\t\t}\n\t\treturn failed(\"git rev-parse failed\");\n\t}\n\t// git resolves the toplevel physically (macOS /tmp -> /private/tmp,\n\t// Windows drive-letter case and short names), so containment is decided\n\t// through compareToplevel; otherwise a divergent spelling of the same\n\t// directory would fail the observation or corrupt every reported path.\n\tconst toplevel = head.stdout.toString(\"utf-8\").trim();\n\tconst comparison = compareToplevel(workspaceRoot, toplevel);\n\tif (!comparison.inside) {\n\t\treturn failed(\"workspace root is outside the repository toplevel\");\n\t}\n\tconst insidePrefix = comparison.prefix;\n\n\tconst headCommit = await runGit(workspaceRoot, [\"rev-parse\", \"--verify\", \"HEAD\"], timeoutMs, signal);\n\tconst mergeHead = await runGit(workspaceRoot, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"], timeoutMs, signal);\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (mergeHead.ok) warnings.push(\"merge in progress\");\n\tconst branch = await runGit(workspaceRoot, [\"branch\", \"--show-current\"], timeoutMs, signal);\n\n\tconst headCommitValue = headCommit.ok ? headCommit.stdout.toString(\"utf-8\").trim() : undefined;\n\tif (!headCommit.ok) warnings.push(\"unborn HEAD (no commits)\");\n\tconst branchValue = branch.ok ? branch.stdout.toString(\"utf-8\").trim() : \"\";\n\tif (branchValue === \"\") warnings.push(\"detached HEAD\");\n\n\t// Index baseline.\n\tconst index = await runGit(workspaceRoot, [\"ls-files\", \"--stage\", \"-z\"], timeoutMs, signal);\n\tif (!index.ok) return failed(\"git ls-files failed\");\n\tconst indexDigest = `sha256:${createHash(\"sha256\").update(index.stdout).digest(\"hex\")}`;\n\n\t// Staged / unstaged / untracked / ignored change sets.\n\tconst status = await runGit(\n\t\tworkspaceRoot,\n\t\t[\"status\", \"--porcelain\", \"-z\", \"--untracked-files=all\", \"--ignored=matching\"],\n\t\ttimeoutMs,\n\t\tsignal,\n\t);\n\tif (!status.ok) return failed(\"git status failed\");\n\n\tconst paths: WorkspaceStatePath[] = [];\n\tlet truncated = false;\n\tlet skippedHashes = 0;\n\tfor (const entry of parsePorcelainZ(status.stdout)) {\n\t\tif (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (paths.length >= maxPaths) {\n\t\t\ttruncated = true;\n\t\t\twarnings.push(`path list truncated at ${maxPaths} paths`);\n\t\t\tbreak;\n\t\t}\n\t\tconst { kind, staged, unstaged, ignored } = classify(entry);\n\t\tconst workspacePath: WorkspaceStatePath = {\n\t\t\tpath: prefixRelative(insidePrefix, entry.path),\n\t\t\tkind,\n\t\t\tstaged,\n\t\t\tunstaged,\n\t\t\t...(ignored ? { ignored: true } : {}),\n\t\t\t...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),\n\t\t};\n\t\tif (hashPaths && kind !== \"deleted\") {\n\t\t\tconst { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);\n\t\t\tif (hash) workspacePath.contentHash = hash;\n\t\t\telse if (skipped) skippedHashes++;\n\t\t}\n\t\tpaths.push(workspacePath);\n\t}\n\tif (skippedHashes > 0) warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);\n\n\t// Digest over exactly the paths this record includes, so a later\n\t// comparison can cheaply detect that the covered state moved.\n\tconst digestInput = paths\n\t\t.map(\n\t\t\t(p) =>\n\t\t\t\t`${p.path}\\0${p.kind}\\0${p.staged ? \"S\" : \"\"}${p.unstaged ? \"U\" : \"\"}${p.ignored ? \"I\" : \"\"}\\0${p.contentHash ?? \"\"}\\n`,\n\t\t)\n\t\t.sort()\n\t\t.join(\"\");\n\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: truncated || skippedHashes > 0 ? \"incomplete\" : \"observed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\t// `hashes` reflects the configured policy: a disabled hash pass is a\n\t\t// coverage statement, not an incomplete capture.\n\t\tcoverage: { ...fullCoverage(), hashes: hashPaths },\n\t\tbase: {\n\t\t\t...(headCommitValue ? { headCommit: headCommitValue } : {}),\n\t\t\t...(branchValue ? { branch: branchValue } : {}),\n\t\t\tindexDigest,\n\t\t\t...(truncated ? {} : { worktreeDigest: `sha256:${createHash(\"sha256\").update(digestInput).digest(\"hex\")}` }),\n\t\t},\n\t\tpaths,\n\t\twarnings,\n\t};\n}\n"]}
1
+ {"version":3,"file":"git-observer.js","sourceRoot":"","sources":["../../../src/core/workspace/git-observer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstatSync, readFileSync, readlinkSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, win32 as pathWin32, relative, sep } from \"node:path\";\nimport {\n\tWORKSPACE_RECORD_VERSION,\n\ttype WorkspaceStateCoverage,\n\ttype WorkspaceStatePath,\n\ttype WorkspaceStateRecord,\n} from \"./state.ts\";\n\n/**\n * Read-only Git workspace observation (spec 2026-09-01-harness-correctness-\n * and-workspace-state.md § 1).\n *\n * The adapter never mutates repository state: `HEAD`, the current branch, the\n * index, the worktree, the stash, and local config are only read. Optional\n * index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`\n * cannot refresh the on-disk index. It never walks the workspace; all path\n * knowledge comes from git. Symlinks are hashed as link text (what git\n * stores), never followed. Submodule (gitlink) paths carry no worktree hash.\n */\n\nconst DEFAULT_MAX_PATHS = 200;\nconst DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst GIT_CONFIG = [\"-c\", \"core.autocrlf=false\", \"-c\", \"status.renames=true\"];\nconst UNMERGED_XY = new Set([\"DD\", \"AU\", \"UD\", \"UA\", \"DU\", \"AA\", \"UU\"]);\n\nexport interface GitObserveOptions {\n\t/** Compute per-path content hashes of present files (default true). */\n\thashPaths?: boolean;\n\t/** Maximum number of reported paths (default 200). */\n\tmaxPaths?: number;\n\t/** Skip hashing files larger than this many bytes (default 5 MiB). */\n\tmaxHashBytes?: number;\n\t/** Per-command timeout in milliseconds (default 10 000). */\n\ttimeoutMs?: number;\n\t/** Cancels the observation; the record reports `failed`. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Repository-relative path prefixes to omit from `paths` (exact match or\n\t * directory prefix). Used for harness bookkeeping that lives inside the\n\t * workspace — e.g. the session directory — which is not workspace state.\n\t */\n\texcludePaths?: string[];\n}\n\nexport interface GitRunResult {\n\tok: boolean;\n\tcode: number | null;\n\tstdout: Buffer;\n\tstderr: string;\n\ttimedOut: boolean;\n\tspawnError: boolean;\n}\n\n/**\n * The workspace subsystem's one git exec seam: shell-less argv (no quoting\n * bugs, Windows-safe), pinned `core.autocrlf=false`, optional index locks\n * disabled, per-command timeout, abort support. The observer above uses it\n * read-only; `git-worktree-owner.ts` reuses it for the worktree mutations so\n * the subsystem never grows a second spawn pattern.\n */\nexport function runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult> {\n\treturn new Promise((resolve) => {\n\t\tlet stdout = Buffer.alloc(0);\n\t\tlet stderr = \"\";\n\t\tlet timedOut = false;\n\t\tlet settled = false;\n\t\tlet child: ChildProcess;\n\t\tconst finish = (code: number | null, spawnError: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve({\n\t\t\t\tok: !spawnError && !timedOut && code === 0,\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\ttimedOut,\n\t\t\t\tspawnError,\n\t\t\t});\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tchild = spawn(\"git\", [...GIT_CONFIG, ...args], {\n\t\t\t\tcwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t// Observation must leave the on-disk index untouched.\n\t\t\t\tenv: { ...process.env, GIT_OPTIONAL_LOCKS: \"0\" },\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ ok: false, code: null, stdout, stderr: \"\", timedOut: false, spawnError: true });\n\t\t\treturn;\n\t\t}\n\t\tif (signal) {\n\t\t\tif (signal.aborted) onAbort();\n\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstdout = Buffer.concat([stdout, chunk]);\n\t\t});\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString(\"utf-8\");\n\t\t});\n\t\tchild.on(\"error\", () => finish(null, true));\n\t\tchild.on(\"close\", (code) => finish(code, false));\n\t});\n}\n\nfunction emptyCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: false,\n\t\tstaged: false,\n\t\tunstaged: false,\n\t\tuntracked: false,\n\t\tignored: false,\n\t\thashes: false,\n\t\tpatch: false,\n\t};\n}\n\nfunction fullCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: true,\n\t\tstaged: true,\n\t\tunstaged: true,\n\t\tuntracked: true,\n\t\tignored: true,\n\t\thashes: true,\n\t\tpatch: false,\n\t};\n}\n\ninterface ParsedStatusPath {\n\tx: string;\n\ty: string;\n\tpath: string;\n\tpreviousPath?: string;\n}\n\nfunction parsePorcelainZ(stdout: Buffer): ParsedStatusPath[] {\n\tconst fields = stdout.toString(\"utf-8\").split(\"\\0\");\n\tconst parsed: ParsedStatusPath[] = [];\n\tlet i = 0;\n\twhile (i < fields.length) {\n\t\tconst record = fields[i++];\n\t\tif (!record || record.length < 4) continue;\n\t\tconst entry: ParsedStatusPath = { x: record[0], y: record[1], path: record.slice(3) };\n\t\tif (entry.x === \"R\" || entry.x === \"C\" || entry.y === \"R\" || entry.y === \"C\") {\n\t\t\tentry.previousPath = fields[i++];\n\t\t}\n\t\tparsed.push(entry);\n\t}\n\treturn parsed;\n}\n\nfunction classify(entry: ParsedStatusPath): {\n\tkind: WorkspaceStatePath[\"kind\"];\n\tstaged: boolean;\n\tunstaged: boolean;\n\tignored: boolean;\n} {\n\tconst { x, y } = entry;\n\tif (x === \"?\" && y === \"?\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: false };\n\t}\n\tif (x === \"!\" && y === \"!\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: true };\n\t}\n\tif (UNMERGED_XY.has(`${x}${y}`)) {\n\t\treturn {\n\t\t\tkind: x === \"D\" && y === \"D\" ? \"deleted\" : \"modified\",\n\t\t\tstaged: true,\n\t\t\tunstaged: true,\n\t\t\tignored: false,\n\t\t};\n\t}\n\tlet kind: WorkspaceStatePath[\"kind\"] = \"modified\";\n\tif (x === \"A\" || y === \"A\" || x === \"C\" || y === \"C\") kind = \"added\";\n\telse if (x === \"D\" || y === \"D\") kind = \"deleted\";\n\telse if (x === \"R\" || y === \"R\") kind = \"renamed\";\n\treturn {\n\t\tkind,\n\t\tstaged: x !== \" \" && x !== \"?\" && x !== \"!\",\n\t\tunstaged: y !== \" \" && y !== \"?\" && y !== \"!\",\n\t\tignored: false,\n\t};\n}\n\n/** Hash one workspace path: regular files by bytes, symlinks by link text. */\nfunction hashWorkspacePath(\n\ttoplevel: string,\n\tpath: string,\n\tmaxBytes: number,\n): { hash?: string; skipped: \"size\" | \"error\" | false } {\n\ttry {\n\t\tconst full = `${toplevel}/${path}`;\n\t\tconst stat = lstatSync(full);\n\t\tif (stat.isFile()) {\n\t\t\tif (stat.size > maxBytes) return { skipped: \"size\" };\n\t\t\treturn { hash: `sha256:${createHash(\"sha256\").update(readFileSync(full)).digest(\"hex\")}`, skipped: false };\n\t\t}\n\t\tif (stat.isSymbolicLink()) {\n\t\t\treturn {\n\t\t\t\thash: `sha256:${createHash(\"sha256\").update(readlinkSync(full)).digest(\"hex\")}`,\n\t\t\t\tskipped: false,\n\t\t\t};\n\t\t}\n\t\t// Directories (submodule gitlinks) have no worktree content to hash;\n\t\t// that is expected, not a coverage gap.\n\t\tif (stat.isDirectory()) return { skipped: false };\n\t\treturn { skipped: \"error\" };\n\t} catch {\n\t\treturn { skipped: \"error\" };\n\t}\n}\n\nfunction prefixRelative(prefix: string, path: string): string {\n\tif (prefix === \"\" || prefix === \".\") return path;\n\treturn `${prefix}/${path}`;\n}\n\n/**\n * Observe a Git workspace without mutating it. Returns a version-1\n * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`\n * when git could not run or the observation was cancelled, and `incomplete`\n * when a limit truncated the capture. `observed` never implies more than\n * `coverage` states.\n */\nexport interface ToplevelComparison {\n\t/** Whether the physically resolved workspace root sits under the toplevel. */\n\tinside: boolean;\n\t/** Root path relative to the toplevel; strip this prefix from porcelain paths. */\n\tprefix: string;\n\t/** Physically resolved toplevel; base for absolute-path work like hashing. */\n\ttoplevelReal: string;\n}\n\n/**\n * Decide containment of the workspace root inside git's toplevel after both\n * sides are physically resolved. Windows spellings diverge from what Node's\n * realpath returns for the same directory (short 8.3 names, drive-letter\n * case), so the win32 branch resolves through the native resolver, folds\n * case for the comparison, and uses win32 path semantics. The platform and\n * resolver are injectable so tests can exercise those branches anywhere.\n */\nexport function compareToplevel(\n\tworkspaceRoot: string,\n\ttoplevel: string,\n\toptions?: {\n\t\tplatform?: NodeJS.Platform;\n\t\trealpath?: (path: string) => string;\n\t},\n): ToplevelComparison {\n\tconst platform = options?.platform ?? process.platform;\n\tconst isWin32 = platform === \"win32\";\n\tconst realpath =\n\t\toptions?.realpath ??\n\t\t(isWin32\n\t\t\t? (p: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn realpathSync.native(p);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn realpathSync(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: realpathSync);\n\tconst rootReal = realpath(workspaceRoot);\n\tconst toplevelReal = realpath(toplevel);\n\tconst pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };\n\tconst fold = (p: string) => (isWin32 ? p.toLowerCase() : p);\n\tconst prefix = pathMod.relative(toplevelReal, rootReal);\n\tconst inside = !(prefix === \"..\" || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));\n\tif (isWin32 && inside) {\n\t\t// Recompute the strip prefix under case folding so a divergent drive or\n\t\t// directory case still yields the clean \"\" or relative form.\n\t\tconst folded = pathMod.relative(fold(toplevelReal), fold(rootReal));\n\t\treturn { inside, prefix: folded, toplevelReal };\n\t}\n\treturn { inside, prefix, toplevelReal };\n}\n\nexport async function observeWorkspaceGit(\n\tworkspaceRoot: string,\n\toptions?: GitObserveOptions,\n): Promise<WorkspaceStateRecord> {\n\tconst maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;\n\tconst maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst hashPaths = options?.hashPaths ?? true;\n\tconst signal = options?.signal;\n\tconst excludePaths = (options?.excludePaths ?? [])\n\t\t.map((p) => p.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\"))\n\t\t.filter((p) => p.length > 0);\n\tconst isExcluded = (repoPath: string): boolean =>\n\t\texcludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));\n\n\tconst warnings: string[] = [];\n\tconst failed = (message: string): WorkspaceStateRecord => ({\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: \"failed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\tcoverage: emptyCoverage(),\n\t\tpaths: [],\n\t\twarnings: [message],\n\t});\n\n\tif (signal?.aborted) {\n\t\treturn failed(\"workspace observation cancelled\");\n\t}\n\n\tconst head = await runGit(workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], timeoutMs, signal);\n\tif (head.spawnError) return failed(\"git could not be executed\");\n\tif (head.timedOut) return failed(\"workspace observation timed out\");\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (!head.ok) {\n\t\tif (/not a git repository/i.test(head.stderr) || head.code === 128) {\n\t\t\treturn {\n\t\t\t\t...failed(\"not a git repository\"),\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t};\n\t\t}\n\t\treturn failed(\"git rev-parse failed\");\n\t}\n\t// git resolves the toplevel physically (macOS /tmp -> /private/tmp,\n\t// Windows drive-letter case and short names), so containment is decided\n\t// through compareToplevel; otherwise a divergent spelling of the same\n\t// directory would fail the observation or corrupt every reported path.\n\tconst toplevel = head.stdout.toString(\"utf-8\").trim();\n\tconst comparison = compareToplevel(workspaceRoot, toplevel);\n\tif (!comparison.inside) {\n\t\treturn failed(\"workspace root is outside the repository toplevel\");\n\t}\n\tconst insidePrefix = comparison.prefix;\n\n\tconst headCommit = await runGit(workspaceRoot, [\"rev-parse\", \"--verify\", \"HEAD\"], timeoutMs, signal);\n\tconst mergeHead = await runGit(workspaceRoot, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"], timeoutMs, signal);\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (mergeHead.ok) warnings.push(\"merge in progress\");\n\tconst branch = await runGit(workspaceRoot, [\"branch\", \"--show-current\"], timeoutMs, signal);\n\n\tconst headCommitValue = headCommit.ok ? headCommit.stdout.toString(\"utf-8\").trim() : undefined;\n\tif (!headCommit.ok) warnings.push(\"unborn HEAD (no commits)\");\n\tconst branchValue = branch.ok ? branch.stdout.toString(\"utf-8\").trim() : \"\";\n\tif (branchValue === \"\") warnings.push(\"detached HEAD\");\n\n\t// Index baseline.\n\tconst index = await runGit(workspaceRoot, [\"ls-files\", \"--stage\", \"-z\"], timeoutMs, signal);\n\tif (!index.ok) return failed(\"git ls-files failed\");\n\tconst indexDigest = `sha256:${createHash(\"sha256\").update(index.stdout).digest(\"hex\")}`;\n\n\t// Staged / unstaged / untracked / ignored change sets.\n\tconst status = await runGit(\n\t\tworkspaceRoot,\n\t\t[\"status\", \"--porcelain\", \"-z\", \"--untracked-files=all\", \"--ignored=matching\"],\n\t\ttimeoutMs,\n\t\tsignal,\n\t);\n\tif (!status.ok) return failed(\"git status failed\");\n\n\tconst paths: WorkspaceStatePath[] = [];\n\tlet truncated = false;\n\tlet skippedHashes = 0;\n\tfor (const entry of parsePorcelainZ(status.stdout)) {\n\t\tif (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (paths.length >= maxPaths) {\n\t\t\ttruncated = true;\n\t\t\twarnings.push(`path list truncated at ${maxPaths} paths`);\n\t\t\tbreak;\n\t\t}\n\t\tconst { kind, staged, unstaged, ignored } = classify(entry);\n\t\tconst workspacePath: WorkspaceStatePath = {\n\t\t\tpath: prefixRelative(insidePrefix, entry.path),\n\t\t\tkind,\n\t\t\tstaged,\n\t\t\tunstaged,\n\t\t\t...(ignored ? { ignored: true } : {}),\n\t\t\t...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),\n\t\t};\n\t\tif (hashPaths && kind !== \"deleted\") {\n\t\t\tconst { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);\n\t\t\tif (hash) workspacePath.contentHash = hash;\n\t\t\telse if (skipped) skippedHashes++;\n\t\t}\n\t\tpaths.push(workspacePath);\n\t}\n\tif (skippedHashes > 0) warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);\n\n\t// Digest over exactly the paths this record includes, so a later\n\t// comparison can cheaply detect that the covered state moved.\n\tconst digestInput = paths\n\t\t.map(\n\t\t\t(p) =>\n\t\t\t\t`${p.path}\\0${p.kind}\\0${p.staged ? \"S\" : \"\"}${p.unstaged ? \"U\" : \"\"}${p.ignored ? \"I\" : \"\"}\\0${p.contentHash ?? \"\"}\\n`,\n\t\t)\n\t\t.sort()\n\t\t.join(\"\");\n\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: truncated || skippedHashes > 0 ? \"incomplete\" : \"observed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\t// `hashes` reflects the configured policy: a disabled hash pass is a\n\t\t// coverage statement, not an incomplete capture.\n\t\tcoverage: { ...fullCoverage(), hashes: hashPaths },\n\t\tbase: {\n\t\t\t...(headCommitValue ? { headCommit: headCommitValue } : {}),\n\t\t\t...(branchValue ? { branch: branchValue } : {}),\n\t\t\tindexDigest,\n\t\t\t...(truncated ? {} : { worktreeDigest: `sha256:${createHash(\"sha256\").update(digestInput).digest(\"hex\")}` }),\n\t\t},\n\t\tpaths,\n\t\twarnings,\n\t};\n}\n"]}
@@ -0,0 +1,89 @@
1
+ import type { ChildWorkspaceRequest, WorkspaceReleaseOutcome } from "../delegation/runtime.ts";
2
+ export interface GitWorktreeWorkspaceOwnerOptions {
3
+ /** Per-command timeout in milliseconds (default 15 000). */
4
+ timeoutMs?: number;
5
+ }
6
+ /**
7
+ * The production workspace owner for worktree isolation (spec
8
+ * 2026-09-09-run-and-child-session-architecture.md, "Parallel work and
9
+ * ownership"). The workspace subsystem owns git; delegation stays git-free:
10
+ * `prepare()` creates one linked worktree per child at
11
+ * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` on branch
12
+ * `apex-child-<short-sessionId>` and returns the request with `root` set, which
13
+ * is how the child session's cwd ends up inside the worktree. `release()`
14
+ * removes it again. Refusals are throws: outside a git repository (or without
15
+ * a git executable) `prepare()` throws, and the delegation runtime surfaces
16
+ * that throw as the delegation's failure.
17
+ *
18
+ * Cleanup choices, deliberately:
19
+ * - `git worktree remove` runs without `--force` and there is no automatic
20
+ * fallback. When the plain remove refuses -- typically a dirty tree, since a
21
+ * child that wrote files leaves untracked content behind -- the tree is
22
+ * inspected and *kept*, reported as a `kept: "dirty"` release outcome so
23
+ * uncommitted child work is preserved. Only an explicit
24
+ * `release(id, { force: true })` from the caller force-removes. The child's
25
+ * branch ref survives either way, so the parent can still integrate through
26
+ * an explicit git workflow, and this owner never merges, commits, or pushes
27
+ * anything.
28
+ * - A worktree that is already gone is tolerated, so releasing twice, or
29
+ * releasing a failed delegation whose `prepare()` died before creating
30
+ * anything, are both no-ops.
31
+ * - The worktrees directory is kept out of the parent checkout's `git status`
32
+ * through the repository-local `.git/info/exclude` -- the one ignore channel
33
+ * that cannot touch tracked project files such as `.gitignore` (which the
34
+ * spec forbids the coordinator from editing). Left unignored, the linked
35
+ * worktrees would report as untracked churn and spuriously retire
36
+ * verification results. Best effort only: a failure here never fails the
37
+ * worktree itself.
38
+ */
39
+ export declare class GitWorktreeWorkspaceOwner {
40
+ private readonly workspaceRoot;
41
+ private readonly timeoutMs;
42
+ constructor(workspaceRoot: string, options?: GitWorktreeWorkspaceOwnerOptions);
43
+ /** The worktree directory this owner uses for one child session. */
44
+ private worktreeDir;
45
+ /**
46
+ * Create the child's worktree and return the request with `root` set to its
47
+ * path. Throws (refusing the delegation) when the workspace is not a git
48
+ * repository, git is unavailable, or the worktree cannot be created.
49
+ */
50
+ prepare(request: ChildWorkspaceRequest & {
51
+ sessionId: string;
52
+ }): Promise<ChildWorkspaceRequest & {
53
+ root: string;
54
+ }>;
55
+ /**
56
+ * Remove the child's worktree safely. Plain `git worktree remove` first; if
57
+ * it refuses, the tree's status decides the outcome: uncommitted or
58
+ * untracked content means the tree is kept ("dirty") so child work is
59
+ * never destroyed silently; a clean tree that still cannot be removed is
60
+ * reported as "failed". `--force` runs only when the caller passes
61
+ * `options.force` -- the explicit escape hatch, never an automatic
62
+ * fallback. Already-removed worktrees are tolerated and never throw.
63
+ */
64
+ release(sessionId: string, options?: {
65
+ force?: boolean;
66
+ }): Promise<WorkspaceReleaseOutcome>;
67
+ /**
68
+ * Read-only verification for explicit workspace recovery (spec
69
+ * 2026-09-09-run-and-child-session-architecture.md, "Workspace states and
70
+ * explicit recovery"): confirm `root` is still THIS owner's worktree for
71
+ * `sessionId` in the CURRENT workspace. Three checks, each refusal naming
72
+ * the check: (layout) the recorded path is exactly the owner's
73
+ * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` and exists;
74
+ * (admin entry) `<root>/.git` is a linked-worktree pointer file whose gitdir
75
+ * resolves into the current repository's `.git/worktrees`; (branch) the
76
+ * checked-out branch is `apex-child-<short-sessionId>`. Never creates,
77
+ * checks out, resets, or force-removes anything -- git inspection is
78
+ * read-only (`rev-parse`, `status`) plus the administrative entry read.
79
+ * Returns the worktree's dirty state so callers can report preserved
80
+ * uncommitted work without a second inspection.
81
+ */
82
+ verify(sessionId: string, root: string): Promise<{
83
+ dirty: boolean;
84
+ }>;
85
+ private currentGitDir;
86
+ private requireRepository;
87
+ private keepWorktreesUntracked;
88
+ }
89
+ //# sourceMappingURL=git-worktree-owner.d.ts.map