@gajae-code/coding-agent 0.16.7 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -1,47 +1,420 @@
1
1
  import * as fs from "node:fs";
2
+ import type * as fsPromises from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
 
4
5
  const contentCache = new Map<string, string | null>();
5
6
  const dirCache = new Map<string, fs.Dirent[]>();
6
7
 
8
+ /** Authority used when reading from an explicit-home discovery context. */
9
+ export type ReadScope = "project" | "user" | "native";
10
+
11
+ export interface FileIdentity {
12
+ readonly dev: number;
13
+ readonly ino: number;
14
+ }
15
+
16
+ export interface ReadFileOptions {
17
+ /** Canonicalize the file and enforce the supplied home/profile boundary. */
18
+ readonly isolatedHome?: boolean;
19
+ readonly home?: string;
20
+ readonly homeIdentity?: FileIdentity;
21
+ readonly userAgentDir?: string;
22
+ readonly userAgentIdentity?: FileIdentity | null;
23
+ /**
24
+ * Scope of the read. Foreign user/project providers are home-bound; only
25
+ * native/SSH user reads may use an explicitly external agent directory.
26
+ */
27
+ readonly scope?: ReadScope;
28
+ /** Skip the shared lexical cache for this operation. */
29
+ readonly bypassCache?: boolean;
30
+ /** Optional physical root that the target must remain within for this read. */
31
+ readonly containmentRoot?: string;
32
+ /** Identity captured when the containment root was authorized. */
33
+ readonly containmentRootIdentity?: FileIdentity;
34
+ }
35
+
7
36
  function resolvePath(filePath: string): string {
8
37
  return path.resolve(filePath);
9
38
  }
10
39
 
11
- export async function readFile(filePath: string): Promise<string | null> {
12
- const abs = resolvePath(filePath);
13
- if (contentCache.has(abs)) {
40
+ function isWithinOrEqual(root: string, candidate: string): boolean {
41
+ const relative = path.relative(root, candidate);
42
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
43
+ }
44
+
45
+ async function isHomeIdentityStable(options?: ReadFileOptions): Promise<boolean> {
46
+ if (!options?.isolatedHome || !options.home || !options.homeIdentity) return true;
47
+ try {
48
+ const current = await fs.promises.stat(options.home);
49
+ return current.dev === options.homeIdentity.dev && current.ino === options.homeIdentity.ino;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ async function isUserAgentIdentityStable(options?: ReadFileOptions): Promise<boolean> {
56
+ if (
57
+ !options?.isolatedHome ||
58
+ options.scope !== "native" ||
59
+ !options.userAgentDir ||
60
+ options.userAgentIdentity === undefined
61
+ )
62
+ return true;
63
+ if (options.userAgentIdentity === null) {
64
+ try {
65
+ await fs.promises.lstat(options.userAgentDir);
66
+ return false;
67
+ } catch (error) {
68
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
69
+ }
70
+ }
71
+ try {
72
+ const current = await fs.promises.stat(options.userAgentDir);
73
+ return current.dev === options.userAgentIdentity.dev && current.ino === options.userAgentIdentity.ino;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ async function isContainmentRootIdentityStable(options?: ReadFileOptions): Promise<boolean> {
80
+ if (!options?.isolatedHome || !options.containmentRoot || !options.containmentRootIdentity) return true;
81
+ try {
82
+ const current = await fs.promises.stat(options.containmentRoot);
83
+ return sameFileIdentity(current, options.containmentRootIdentity);
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ async function canonicalizeThroughExistingAncestor(target: string): Promise<string> {
90
+ const resolved = path.resolve(target);
91
+ const suffix: string[] = [];
92
+ let current = resolved;
93
+
94
+ while (true) {
95
+ try {
96
+ const real = await fs.promises.realpath(current);
97
+ return suffix.length > 0 ? path.join(real, ...suffix.reverse()) : real;
98
+ } catch (error) {
99
+ const code = (error as NodeJS.ErrnoException).code;
100
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
101
+ const parent = path.dirname(current);
102
+ if (parent === current) return resolved;
103
+ suffix.push(path.basename(current));
104
+ current = parent;
105
+ }
106
+ }
107
+ }
108
+
109
+ function rootsForRead(options: ReadFileOptions): string[] {
110
+ if (options.scope === "native") {
111
+ return [options.home, options.userAgentDir].filter((root): root is string => typeof root === "string");
112
+ }
113
+ return typeof options.home === "string" ? [options.home] : [];
114
+ }
115
+
116
+ async function resolveReadPath(filePath: string, options?: ReadFileOptions): Promise<string | null> {
117
+ const lexical = resolvePath(filePath);
118
+ if (!options?.isolatedHome) return lexical;
119
+ if (!(await isHomeIdentityStable(options))) return null;
120
+ if (!(await isUserAgentIdentityStable(options))) return null;
121
+ if (!(await isContainmentRootIdentityStable(options))) return null;
122
+ const roots = rootsForRead(options);
123
+ if (roots.length === 0) return null;
124
+ const [canonicalTarget, canonicalRoots] = await Promise.all([
125
+ canonicalizeThroughExistingAncestor(lexical),
126
+ Promise.all(roots.map(canonicalizeThroughExistingAncestor)),
127
+ ]);
128
+ if (!canonicalRoots.some(root => isWithinOrEqual(root, canonicalTarget))) return null;
129
+ if (options.containmentRoot) {
130
+ const canonicalContainmentRoot = await canonicalizeThroughExistingAncestor(options.containmentRoot);
131
+ if (!isWithinOrEqual(canonicalContainmentRoot, canonicalTarget)) return null;
132
+ }
133
+ return canonicalTarget;
134
+ }
135
+
136
+ /**
137
+ * Re-check an isolated path immediately before opening it. Canonicalizing the
138
+ * lexical path once prevents cache poisoning, while this second check also
139
+ * fails closed when an existing leaf is swapped for a symlink between the
140
+ * initial resolution and the actual read.
141
+ */
142
+ async function isCurrentIsolatedPath(abs: string, options?: ReadFileOptions): Promise<boolean> {
143
+ if (!options?.isolatedHome) return true;
144
+ if (!(await isHomeIdentityStable(options))) return false;
145
+ if (!(await isUserAgentIdentityStable(options))) return false;
146
+ if (!(await isContainmentRootIdentityStable(options))) return false;
147
+ try {
148
+ const current = await resolveReadPath(abs, options);
149
+ return current === abs;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ function isSingleLinkRegularFile(stat: fs.Stats): boolean {
156
+ return stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1;
157
+ }
158
+
159
+ /** Reject hard-linked executable or configuration files in isolated discovery. */
160
+ export async function isSingleLinkRegularFileAt(filePath: string): Promise<boolean> {
161
+ try {
162
+ return isSingleLinkRegularFile(await fs.promises.lstat(filePath));
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ function sameFileIdentity(left: Pick<fs.Stats, "dev" | "ino">, right: Pick<fs.Stats, "dev" | "ino">): boolean {
169
+ return left.dev === right.dev && left.ino === right.ino;
170
+ }
171
+
172
+ /**
173
+ * Capture the identity of the currently-authorized path before opening it.
174
+ * Stat first and canonicalize second so a parent replacement in either seam
175
+ * is detected by the descriptor identity check below.
176
+ */
177
+ async function statAuthorizedPath(abs: string, options?: ReadFileOptions): Promise<fs.Stats | null> {
178
+ if (!options?.isolatedHome) return null;
179
+ if (!(await isHomeIdentityStable(options))) return null;
180
+ if (!(await isUserAgentIdentityStable(options))) return null;
181
+ let authorized: fs.Stats;
182
+ try {
183
+ authorized = await fs.promises.stat(abs);
184
+ } catch {
185
+ return null;
186
+ }
187
+ return (await isCurrentIsolatedPath(abs, options)) ? authorized : null;
188
+ }
189
+
190
+ async function validateOpenedPath(
191
+ handle: fsPromises.FileHandle,
192
+ abs: string,
193
+ options: ReadFileOptions | undefined,
194
+ authorized: fs.Stats | null,
195
+ kind: "file" | "directory",
196
+ ): Promise<boolean> {
197
+ if (!options?.isolatedHome) return true;
198
+ if (!(await isHomeIdentityStable(options))) return false;
199
+ if (!(await isUserAgentIdentityStable(options))) return false;
200
+ try {
201
+ const opened = await handle.stat();
202
+ const validKind =
203
+ kind === "file" ? isSingleLinkRegularFile(opened) : opened.isDirectory() && !opened.isSymbolicLink();
204
+ if (!validKind || authorized === null || !sameFileIdentity(opened, authorized)) return false;
205
+ const current = await fs.promises.stat(abs);
206
+ if (!sameFileIdentity(opened, current)) return false;
207
+ return await isCurrentIsolatedPath(abs, options);
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ interface OpenedPath {
214
+ handle: fsPromises.FileHandle;
215
+ authorized: fs.Stats | null;
216
+ }
217
+
218
+ async function openIsolatedFile(abs: string, options?: ReadFileOptions): Promise<OpenedPath | null> {
219
+ let handle: fsPromises.FileHandle | undefined;
220
+ const authorized = await statAuthorizedPath(abs, options);
221
+ if (options?.isolatedHome && authorized === null) return null;
222
+ try {
223
+ const isolatedFlags = options?.isolatedHome ? (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0) : 0;
224
+ handle = await fs.promises.open(abs, fs.constants.O_RDONLY | isolatedFlags);
225
+ if (!(await validateOpenedPath(handle, abs, options, authorized, "file"))) {
226
+ await handle.close();
227
+ return null;
228
+ }
229
+ return { handle, authorized };
230
+ } catch {
231
+ await handle?.close().catch(() => {});
232
+ return null;
233
+ }
234
+ }
235
+
236
+ export async function readFile(filePath: string, options?: ReadFileOptions): Promise<string | null> {
237
+ let abs: string | null;
238
+ try {
239
+ abs = await resolveReadPath(filePath, options);
240
+ } catch {
241
+ return null;
242
+ }
243
+ if (abs === null) return null;
244
+ if (!(await isCurrentIsolatedPath(abs, options))) return null;
245
+ const useCache = !options?.bypassCache && !options?.isolatedHome;
246
+ if (useCache && contentCache.has(abs)) {
14
247
  return contentCache.get(abs) ?? null;
15
248
  }
249
+ if (!options?.isolatedHome) {
250
+ try {
251
+ const content = await Bun.file(abs).text();
252
+ if (useCache) contentCache.set(abs, content);
253
+ return content;
254
+ } catch {
255
+ if (useCache) contentCache.set(abs, null);
256
+ return null;
257
+ }
258
+ }
16
259
 
260
+ const opened = await openIsolatedFile(abs, options);
261
+ if (!opened) {
262
+ if (useCache) contentCache.set(abs, null);
263
+ return null;
264
+ }
17
265
  try {
18
- const content = await Bun.file(abs).text();
19
- contentCache.set(abs, content);
266
+ if (!(await validateOpenedPath(opened.handle, abs, options, opened.authorized, "file"))) return null;
267
+ const content = await opened.handle.readFile({ encoding: "utf8" });
268
+ if (!(await validateOpenedPath(opened.handle, abs, options, opened.authorized, "file"))) return null;
269
+ if (useCache) contentCache.set(abs, content);
20
270
  return content;
21
271
  } catch {
22
- contentCache.set(abs, null);
272
+ if (useCache) contentCache.set(abs, null);
273
+ return null;
274
+ } finally {
275
+ await opened.handle.close();
276
+ }
277
+ }
278
+
279
+ /** Capture a stable physical identity for an authorized containment root. */
280
+ export async function capturePathIdentity(filePath: string): Promise<FileIdentity | null> {
281
+ try {
282
+ const stat = await fs.promises.stat(path.resolve(filePath));
283
+ return { dev: stat.dev, ino: stat.ino };
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+
289
+ /** Read one byte range through the same canonical authority as readFile. */
290
+ export async function readFileSlice(
291
+ filePath: string,
292
+ start: number,
293
+ end: number,
294
+ options?: ReadFileOptions,
295
+ ): Promise<Uint8Array | null> {
296
+ let abs: string | null;
297
+ try {
298
+ abs = await resolveReadPath(filePath, options);
299
+ } catch {
300
+ return null;
301
+ }
302
+ if (abs === null || !(await isCurrentIsolatedPath(abs, options))) return null;
303
+ const opened = await openIsolatedFile(abs, options);
304
+ if (!opened) return null;
305
+ try {
306
+ if (!(await validateOpenedPath(opened.handle, abs, options, opened.authorized, "file"))) return null;
307
+ const length = Math.max(0, end - start);
308
+ const buffer = Buffer.alloc(length);
309
+ const { bytesRead } = await opened.handle.read(buffer, 0, length, start);
310
+ if (!(await validateOpenedPath(opened.handle, abs, options, opened.authorized, "file"))) return null;
311
+ return new Uint8Array(buffer.subarray(0, bytesRead));
312
+ } catch {
313
+ return null;
314
+ } finally {
315
+ await opened.handle.close();
316
+ }
317
+ }
318
+
319
+ /** Read file size through the same canonical authority as readFile. */
320
+ export async function readFileSize(filePath: string, options?: ReadFileOptions): Promise<number | null> {
321
+ let abs: string | null;
322
+ try {
323
+ abs = await resolveReadPath(filePath, options);
324
+ } catch {
325
+ return null;
326
+ }
327
+ if (abs === null || !(await isCurrentIsolatedPath(abs, options))) return null;
328
+ const opened = await openIsolatedFile(abs, options);
329
+ if (!opened) return null;
330
+ try {
331
+ if (!(await validateOpenedPath(opened.handle, abs, options, opened.authorized, "file"))) return null;
332
+ const size = (await opened.handle.stat()).size;
333
+ if (!(await validateOpenedPath(opened.handle, abs, options, opened.authorized, "file"))) return null;
334
+ return size;
335
+ } catch {
23
336
  return null;
337
+ } finally {
338
+ await opened.handle.close();
24
339
  }
25
340
  }
26
341
 
27
- export async function readDirEntries(dirPath: string): Promise<fs.Dirent[]> {
28
- const abs = resolvePath(dirPath);
29
- if (dirCache.has(abs)) {
342
+ async function validateDirectoryPath(
343
+ abs: string,
344
+ options: ReadFileOptions | undefined,
345
+ authorized: fs.Stats | null,
346
+ ): Promise<boolean> {
347
+ if (!options?.isolatedHome) return true;
348
+ if (authorized === null) return false;
349
+ if (!(await isHomeIdentityStable(options))) return false;
350
+ if (!(await isUserAgentIdentityStable(options))) return false;
351
+ try {
352
+ const current = await fs.promises.stat(abs);
353
+ return (
354
+ current.isDirectory() && sameFileIdentity(current, authorized) && (await isCurrentIsolatedPath(abs, options))
355
+ );
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+
361
+ export async function readDirEntries(dirPath: string, options?: ReadFileOptions): Promise<fs.Dirent[]> {
362
+ let abs: string | null;
363
+ try {
364
+ abs = await resolveReadPath(dirPath, options);
365
+ } catch {
366
+ return [];
367
+ }
368
+ if (abs === null) return [];
369
+ if (!(await isCurrentIsolatedPath(abs, options))) return [];
370
+ const useCache = !options?.bypassCache && !options?.isolatedHome;
371
+ if (useCache && dirCache.has(abs)) {
30
372
  return dirCache.get(abs) ?? [];
31
373
  }
32
374
 
375
+ let handle: fsPromises.FileHandle | undefined;
376
+ const authorized = await statAuthorizedPath(abs, options);
377
+ if (options?.isolatedHome && authorized === null) return [];
33
378
  try {
34
- const entries = await fs.promises.readdir(abs, { withFileTypes: true });
35
- dirCache.set(abs, entries);
379
+ if (!options?.isolatedHome) {
380
+ const entries = await fs.promises.readdir(abs, { withFileTypes: true });
381
+ if (useCache) dirCache.set(abs, entries);
382
+ return entries;
383
+ }
384
+ // Linux can enumerate a pinned directory descriptor through /proc. Node
385
+ // does not expose an equivalent descriptor path on macOS or Windows, so
386
+ // use direct readdir only after validating the authorized path both before
387
+ // and after the read. This keeps valid explicit-home directories visible
388
+ // instead of silently treating them as empty on those platforms.
389
+ if (options?.isolatedHome && process.platform !== "linux") {
390
+ const entries = await fs.promises.readdir(abs, { withFileTypes: true });
391
+ if (!(await validateDirectoryPath(abs, options, authorized))) return [];
392
+ if (useCache) dirCache.set(abs, entries);
393
+ return entries;
394
+ }
395
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
396
+ const directoryOnly = fs.constants.O_DIRECTORY ?? 0;
397
+ handle = await fs.promises.open(abs, fs.constants.O_RDONLY | noFollow | directoryOnly);
398
+ if (!(await validateOpenedPath(handle, abs, options, authorized, "directory"))) return [];
399
+ const directoryPath = process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : null;
400
+ if (directoryPath === null) return [];
401
+ if (!(await validateOpenedPath(handle, abs, options, authorized, "directory"))) return [];
402
+ const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true });
403
+ if (!(await isHomeIdentityStable(options))) return [];
404
+ if (!(await isUserAgentIdentityStable(options))) return [];
405
+ if (!(await validateOpenedPath(handle, abs, options, authorized, "directory"))) return [];
406
+ if (useCache) dirCache.set(abs, entries);
36
407
  return entries;
37
408
  } catch {
38
- dirCache.set(abs, []);
409
+ if (useCache) dirCache.set(abs, []);
39
410
  return [];
411
+ } finally {
412
+ await handle?.close().catch(() => {});
40
413
  }
41
414
  }
42
415
 
43
- export async function readDir(dirPath: string): Promise<string[]> {
44
- const entries = await readDirEntries(dirPath);
416
+ export async function readDir(dirPath: string, options?: ReadFileOptions): Promise<string[]> {
417
+ const entries = await readDirEntries(dirPath, options);
45
418
  return entries.map(entry => entry.name);
46
419
  }
47
420
 
@@ -71,15 +444,20 @@ export async function walkUp(
71
444
  * Returns the directory containing `.git` (the repo root), or null if not in a git repo.
72
445
  * Results are based on the cached readDirEntries, so repeated calls are cheap.
73
446
  */
74
- export async function findRepoRoot(startDir: string): Promise<string | null> {
447
+ export async function findRepoRoot(
448
+ startDir: string,
449
+ stopAt?: string,
450
+ options?: ReadFileOptions,
451
+ ): Promise<string | null> {
75
452
  let current = resolvePath(startDir);
453
+ const stop = stopAt ? resolvePath(stopAt) : undefined;
76
454
  while (true) {
77
- const entries = await readDirEntries(current);
455
+ const entries = await readDirEntries(current, options);
78
456
  if (entries.some(e => e.name === ".git")) {
79
457
  return current;
80
458
  }
81
459
  const parent = path.dirname(current);
82
- if (parent === current) return null;
460
+ if (parent === current || current === stop) return null;
83
461
  current = parent;
84
462
  }
85
463
  }