@visual-z/xeno 0.8.3 → 0.9.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 (348) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2066
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1713
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3001
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/canvas.ts +0 -96
  312. package/src/tui/ui/colors.ts +0 -70
  313. package/src/tui/ui/composer.ts +0 -200
  314. package/src/tui/ui/control-panel.ts +0 -25
  315. package/src/tui/ui/control-sidebar.ts +0 -49
  316. package/src/tui/ui/dialog.ts +0 -398
  317. package/src/tui/ui/draft.ts +0 -23
  318. package/src/tui/ui/duration.ts +0 -16
  319. package/src/tui/ui/edge.ts +0 -186
  320. package/src/tui/ui/image-size.ts +0 -44
  321. package/src/tui/ui/ink.ts +0 -210
  322. package/src/tui/ui/items.ts +0 -42
  323. package/src/tui/ui/keys.ts +0 -40
  324. package/src/tui/ui/layout.ts +0 -74
  325. package/src/tui/ui/list-model.ts +0 -86
  326. package/src/tui/ui/list-view.ts +0 -172
  327. package/src/tui/ui/mascot.ts +0 -67
  328. package/src/tui/ui/messages.ts +0 -518
  329. package/src/tui/ui/motion.ts +0 -28
  330. package/src/tui/ui/paths.ts +0 -10
  331. package/src/tui/ui/pictures.ts +0 -108
  332. package/src/tui/ui/screen.ts +0 -548
  333. package/src/tui/ui/status.ts +0 -403
  334. package/src/tui/ui/styled.ts +0 -85
  335. package/src/tui/ui/tool-card.ts +0 -150
  336. package/src/tui/width.ts +0 -567
  337. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  338. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  339. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  340. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  341. package/src/vendor/pi-rich/html.ts +0 -51
  342. package/src/vendor/pi-rich/image-convert.ts +0 -49
  343. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  344. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  345. package/src/vendor/pi-rich/image-resize.ts +0 -123
  346. package/src/vendor/pi-rich/mime.ts +0 -116
  347. package/src/vendor/pi-rich/photon.ts +0 -139
  348. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,30 +0,0 @@
1
- import { editablePath } from "./file-checkpoints.ts";
2
- import { open } from "node:fs/promises";
3
- import { constants } from "node:fs";
4
- import { blobAddress } from "./blobs.ts";
5
- import { pathToFileURL } from "node:url";
6
- import { resolve } from "node:path";
7
-
8
- // Operational memory ceiling: a language server is optional and must not read arbitrarily large files.
9
- const MAX_LSP_DOCUMENT = 2 * 1024 * 1024;
10
- export async function readLspDocument(root: string, path: string, signal?: AbortSignal): Promise<{ uri: string; text: string; sha256: string }> {
11
- signal?.throwIfAborted();
12
- const absolute = await editablePath(root, path);
13
- const file = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
14
- try {
15
- const info = await file.stat();
16
- if (!info.isFile() || info.nlink > 1) throw Error("LSP requires an existing ordinary workspace file");
17
- if (info.size > MAX_LSP_DOCUMENT) throw Error("LSP document exceeds 2 MiB");
18
- const bytes = new Uint8Array(MAX_LSP_DOCUMENT + 1); let length = 0;
19
- for (;;) {
20
- signal?.throwIfAborted();
21
- const result = await file.read(bytes, length, bytes.length - length, null);
22
- if (!result.bytesRead) break;
23
- length += result.bytesRead;
24
- if (length > MAX_LSP_DOCUMENT) throw Error("LSP document exceeds 2 MiB");
25
- }
26
- signal?.throwIfAborted();
27
- const content = bytes.subarray(0, length);
28
- return { uri: pathToFileURL(resolve(root, path)).href, text: new TextDecoder("utf8", { fatal: true }).decode(content), sha256: blobAddress(content) };
29
- } finally { await file.close(); }
30
- }
@@ -1,85 +0,0 @@
1
- /** Credential storage is separate from append-only observations: secrets must never become log evidence. */
2
- import { mkdir, open, rename, rm, lstat } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { createHash, randomUUID } from "node:crypto";
5
- import { constants } from "node:fs";
6
- import { isJsonValue, type JsonValue } from "../core/tool-contract.ts";
7
-
8
- export interface McpCredentials {
9
- version: 1; serverUrl: string; redirectUrl: string;
10
- clientIdentity?: string;
11
- clients: Record<string, JsonValue>; tokens: Record<string, JsonValue>; expiresAt: Record<string, number>; activeIssuer: string | null;
12
- }
13
- export interface McpCredentialsStore {
14
- read(serverUrl: string, clientIdentity?: string): Promise<McpCredentials | undefined>;
15
- write(credentials: McpCredentials): Promise<void>;
16
- remove(serverUrl: string, clientIdentity?: string): Promise<void>;
17
- }
18
- // Assumed bound prevents a replaced credential file from becoming an unbounded JSON allocation.
19
- const MAX_CREDENTIAL_BYTES = 1024 * 1024;
20
- function filename(dir: string, url: string, clientIdentity = "") { return join(dir, createHash("sha256").update(JSON.stringify([url, clientIdentity])).digest("hex") + ".json"); }
21
- function record(value: unknown): value is Record<string, JsonValue> {
22
- return value !== null && typeof value === "object" && !Array.isArray(value) && isJsonValue(value);
23
- }
24
- function validate(value: unknown, serverUrl: string, clientIdentity = ""): McpCredentials {
25
- if (!record(value) || value["version"] !== 1 || value["serverUrl"] !== serverUrl || typeof value["redirectUrl"] !== "string"
26
- || (value["clientIdentity"] ?? "") !== clientIdentity
27
- || !record(value["clients"]) || !record(value["tokens"]) || !record(value["expiresAt"])
28
- || Object.values(value["expiresAt"]).some(time => typeof time !== "number" || !Number.isFinite(time))
29
- || value["activeIssuer"] !== null && typeof value["activeIssuer"] !== "string") throw Error("MCP credential record is invalid");
30
- return { version: 1, serverUrl, redirectUrl: value["redirectUrl"], clients: value["clients"], tokens: value["tokens"],
31
- expiresAt: Object.fromEntries(Object.entries(value["expiresAt"]).map(([issuer, time]) => [issuer, Number(time)])), activeIssuer: value["activeIssuer"],
32
- ...(typeof value["clientIdentity"] === "string" ? { clientIdentity: value["clientIdentity"] } : {}) };
33
- }
34
- /** Explicit restricted-file backend, compatible with Codex's fallback; applications may provide an OS vault adapter. */
35
- export class FileMcpCredentialsStore implements McpCredentialsStore {
36
- private readonly dir: string;
37
- private writes = Promise.resolve();
38
- constructor(dir: string) { this.dir = dir; }
39
- private async directory(): Promise<void> {
40
- const stat = await lstat(this.dir);
41
- if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) throw Error("MCP credential directory permissions are invalid");
42
- }
43
- async read(serverUrl: string, clientIdentity?: string): Promise<McpCredentials | undefined> {
44
- const path = filename(this.dir, serverUrl, clientIdentity);
45
- try {
46
- await this.directory();
47
- const stat = await lstat(path);
48
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.size > MAX_CREDENTIAL_BYTES) throw Error("MCP credential file permissions or size are invalid");
49
- const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
50
- try {
51
- const opened = await file.stat();
52
- if (!opened.isFile() || opened.size > MAX_CREDENTIAL_BYTES || (opened.mode & 0o077) !== 0) throw Error("Unsafe credential handle");
53
- return validate(JSON.parse(await file.readFile("utf8")), serverUrl, clientIdentity);
54
- } finally { await file.close(); }
55
- } catch (error) {
56
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return undefined;
57
- throw Error("MCP credential store could not be read safely");
58
- }
59
- }
60
- write(credentials: McpCredentials): Promise<void> {
61
- const serialized = JSON.stringify(validate(credentials, credentials.serverUrl, credentials.clientIdentity));
62
- const target = filename(this.dir, credentials.serverUrl, credentials.clientIdentity);
63
- if (Buffer.byteLength(serialized) > MAX_CREDENTIAL_BYTES) return Promise.reject(Error("MCP credential record exceeds size limit"));
64
- const operation = this.writes.then(async () => {
65
- await mkdir(this.dir, { recursive: true, mode: 0o700 });
66
- await this.directory();
67
- const temporary = join(this.dir, `.${randomUUID()}.tmp`);
68
- try {
69
- const file = await open(temporary, "wx", 0o600);
70
- try { await file.writeFile(serialized); await file.sync(); } finally { await file.close(); }
71
- await rename(temporary, target);
72
- } finally { await rm(temporary, { force: true }); }
73
- });
74
- this.writes = operation.catch(() => {});
75
- return operation;
76
- }
77
- async remove(serverUrl: string, clientIdentity?: string): Promise<void> {
78
- await this.writes;
79
- try { await this.directory(); await rm(filename(this.dir, serverUrl, clientIdentity), { force: true }); }
80
- catch (error) {
81
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return;
82
- throw Error("MCP credential store could not be removed safely");
83
- }
84
- }
85
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * The on-disk catalog overlay. `~/.xeno/models.json`, next to auth.json.
3
- *
4
- * This layer only reads and writes bytes. It does not know what a ModelSpec
5
- * is — that lives in runtime — because store may not import runtime. A
6
- * corrupt file is returned as text plus no error: the parser names the bad
7
- * entry, and discarding the whole file here would be the thing the card
8
- * forbids.
9
- */
10
-
11
- import { mkdir, readFile, rename, writeFile, rm } from "node:fs/promises";
12
- import { randomUUID } from "node:crypto";
13
- import { join } from "node:path";
14
- import { configDir } from "./config.ts";
15
-
16
- export function modelsPath(): string {
17
- return join(configDir(), "models.json");
18
- }
19
- export function catalogCachePath(): string { return join(configDir(), "models-cache.json"); }
20
-
21
- export async function readModelsText(): Promise<{ text: string | null; error: string | null }> {
22
- return readModelFile(modelsPath());
23
- }
24
- export async function readCatalogCacheText(): Promise<{ text: string | null; error: string | null }> {
25
- return readModelFile(catalogCachePath());
26
- }
27
- async function readModelFile(path: string): Promise<{ text: string | null; error: string | null }> {
28
- try {
29
- const text = await readFile(path, "utf8");
30
- return { text, error: null };
31
- } catch (err) {
32
- const absent = typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
33
- return { text: null, error: absent ? null : `cannot read ${path}` };
34
- }
35
- }
36
- export async function readModelSources(): Promise<{ userText: string | null; cacheText: string | null; readWarnings: string[] }> {
37
- const [user, cache] = await Promise.all([readModelsText(), readCatalogCacheText()]);
38
- return { userText: user.text, cacheText: cache.text, readWarnings: [user.error, cache.error].filter((value): value is string => value !== null) };
39
- }
40
-
41
- /**
42
- * Replace, don't truncate. writeFile on the live path empties it first, so a
43
- * crash or a thrown write would leave the user with no catalog. Sidecar then
44
- * rename is atomic on the same filesystem; a failed refresh must leave the
45
- * existing bytes untouched (feat-json-mode-and-header).
46
- */
47
- export async function writeModelsFile(value: unknown): Promise<void> {
48
- return writeModelFile(modelsPath(), value);
49
- }
50
- export async function writeCatalogCache(value: unknown): Promise<void> {
51
- return writeModelFile(catalogCachePath(), value);
52
- }
53
- async function writeModelFile(dest: string, value: unknown): Promise<void> {
54
- await mkdir(configDir(), { recursive: true });
55
- const tmp = `${dest}.${randomUUID()}.tmp`;
56
- try {
57
- await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
58
- await rename(tmp, dest);
59
- } finally { await rm(tmp, { force: true }); }
60
- }
@@ -1,224 +0,0 @@
1
- /** Store complete observations while keeping only a bounded preview in memory. */
2
- import { createReadStream } from "node:fs";
3
- import { open, mkdir, rename, rm } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { createHash, randomUUID } from "node:crypto";
6
- import type { ArtifactRef } from "../core/events.ts";
7
- import { blobFilePath, verifyBlob } from "./blobs.ts";
8
-
9
- /** Byte pages keep very large single-line observations retrievable as well. */
10
- export async function readObservation(dir: string, sha256: string, offset: number, limit: number): Promise<{
11
- sha256: string; bytes: number; offset: number; next: number | null; text: string; base64: string;
12
- }> {
13
- if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1) throw new Error("invalid observation page");
14
- if (!await verifyBlob(dir, sha256)) throw new Error(`missing observation ${sha256}`);
15
- const file = await open(blobFilePath(dir, sha256), "r");
16
- try {
17
- const bytes = (await file.stat()).size;
18
- const buffer = Buffer.alloc(Math.min(limit, Math.max(0, bytes - offset)));
19
- const { bytesRead } = await file.read(buffer, 0, buffer.length, offset);
20
- const data = buffer.subarray(0, bytesRead);
21
- return { sha256, bytes, offset, next: offset + bytesRead < bytes ? offset + bytesRead : null,
22
- text: data.toString("utf8"), base64: data.toString("base64") };
23
- } finally { await file.close(); }
24
- }
25
-
26
- /**
27
- * After the shell exits, how long silence must last before capture stops.
28
- *
29
- * A background child (`server &`) inherits the pipes, so waiting for EOF
30
- * waits for the server -- the call then blocks until the timeout and the
31
- * timeout kills the server with the group. Operational choice, not measured:
32
- * long enough for buffered output of the exited shell to arrive, short
33
- * enough that a background start costs nothing noticeable. pi uses the same
34
- * idle-grace shape (`utils/child-process.ts`).
35
- */
36
- const EXIT_IDLE_MS = 100;
37
- /** Hard bound after exit for a background child that never goes quiet. */
38
- const EXIT_DRAIN_MS = 1_000;
39
-
40
- /**
41
- * Both streams into one file in arrival order.
42
- *
43
- * A build that prints progress on stdout and the error on stderr only makes
44
- * sense interleaved; appending all of stderr after all of stdout (the previous
45
- * shape) separates each error from the step that produced it. Writes are
46
- * serialized on one handle, so interleaving happens at chunk granularity --
47
- * the same granularity a terminal shows.
48
- *
49
- * Capture ends at EOF on both streams, or once `exited` has resolved and the
50
- * streams have gone idle. Ending early cancels the readers only; it signals
51
- * no process, so a detached background child keeps running.
52
- */
53
- async function spoolMerged(
54
- dir: string, streams: readonly ReadableStream<Uint8Array>[], exited: Promise<number>,
55
- onOutput?: (chunk: Uint8Array) => void,
56
- separate = false,
57
- limitBytes?: number,
58
- ): Promise<{ path: string; channels: string[]; complete: boolean }> {
59
- await mkdir(dir, { recursive: true });
60
- const path = join(dir, `.${randomUUID()}.stream`);
61
- const file = await open(path, "wx", 0o600);
62
- const channels: string[] = [];
63
- const channelFiles: Awaited<ReturnType<typeof open>>[] = [];
64
- try {
65
- if (separate) for (let i = 0; i < streams.length; i++) {
66
- const channel = `${path}.${i}`;
67
- channelFiles.push(await open(channel, "wx", 0o600)); channels.push(channel);
68
- }
69
- } catch (error) {
70
- await Promise.allSettled([file.close(), ...channelFiles.map(item => item.close())]);
71
- await Promise.all([path, ...channels].map(item => rm(item, { force: true })));
72
- throw error;
73
- }
74
- const readers = streams.map((stream) => stream.getReader());
75
- let queue: Promise<unknown> = Promise.resolve();
76
- let exitedAt: number | undefined;
77
- let idle: ReturnType<typeof setTimeout> | undefined;
78
- let drain: ReturnType<typeof setTimeout> | undefined;
79
- let finished = false, complete = true, capturedBytes = 0;
80
- const stop = () => { if (finished) return; complete = false; for (const reader of readers) reader.cancel().catch(() => {}); };
81
- const armIdle = () => {
82
- if (finished || exitedAt === undefined) return;
83
- clearTimeout(idle);
84
- idle = setTimeout(stop, EXIT_IDLE_MS);
85
- };
86
- void exited.then(() => { if (finished) return; exitedAt = performance.now(); armIdle(); drain = setTimeout(stop, EXIT_DRAIN_MS); }, () => {});
87
- try {
88
- const drained = await Promise.allSettled(readers.map(async (reader, index) => {
89
- try {
90
- for (;;) {
91
- const result = await reader.read();
92
- if (result.done) return;
93
- armIdle();
94
- queue = queue.then(async () => {
95
- await file.writeFile(result.value);
96
- await channelFiles[index]?.writeFile(result.value);
97
- onOutput?.(result.value);
98
- capturedBytes += result.value.byteLength;
99
- // Retain every delivered chunk, then cancel both readers. At most
100
- // the in-flight crossing chunks exceed the bound; SIGTERM grace
101
- // cannot turn an output limit into unlimited spool growth (R6).
102
- if (limitBytes !== undefined && capturedBytes > limitBytes) stop();
103
- });
104
- await queue;
105
- }
106
- } catch (error) { stop(); throw error; }
107
- }));
108
- await queue;
109
- for (const result of drained) if (result.status === "rejected") throw result.reason;
110
- } catch (error) {
111
- await Promise.allSettled([file.close(), ...channelFiles.map(item => item.close())]);
112
- await Promise.all([path, ...channels].map(item => rm(item, { force: true })));
113
- throw error;
114
- }
115
- finally {
116
- finished = true;
117
- clearTimeout(idle); clearTimeout(drain);
118
- for (const reader of readers) reader.releaseLock();
119
- }
120
- await Promise.all([file.close(), ...channelFiles.map(item => item.close())]);
121
- return { path, channels, complete };
122
- }
123
-
124
- /** Last `limit` bytes of a stream, in O(limit) memory however long it runs. */
125
- class TailWindow {
126
- private readonly parts: Uint8Array[] = [];
127
- private size = 0;
128
- private readonly limit: number;
129
- constructor(limit: number) { this.limit = limit; }
130
- push(chunk: Uint8Array): void {
131
- if (this.limit === 0) return;
132
- const part = chunk.byteLength > this.limit ? chunk.slice(chunk.byteLength - this.limit) : chunk.slice();
133
- this.parts.push(part);
134
- this.size += part.byteLength;
135
- while (this.size - this.parts[0]!.byteLength >= this.limit) this.size -= this.parts.shift()!.byteLength;
136
- }
137
- bytes(): Uint8Array {
138
- const all = Buffer.concat(this.parts);
139
- return all.subarray(Math.max(0, all.byteLength - this.limit));
140
- }
141
- }
142
-
143
- export interface ArchivedObservation {
144
- ref: ArtifactRef;
145
- /** First `previewBytes` bytes, decoded; kept for callers that only page from the start. */
146
- preview: string;
147
- /** First and last `previewBytes` bytes, for a head-and-tail view. */
148
- head: Uint8Array;
149
- tail: Uint8Array;
150
- }
151
-
152
- export async function archiveObservation(
153
- dir: string, source: AsyncIterable<Uint8Array>, previewBytes: number,
154
- ): Promise<ArchivedObservation> {
155
- await mkdir(dir, { recursive: true });
156
- const path = join(dir, `.${randomUUID()}.observation`);
157
- const file = await open(path, "wx", 0o600);
158
- const hash = createHash("sha256");
159
- let bytes = 0;
160
- let kept = 0;
161
- const preview: Uint8Array[] = [];
162
- const tail = new TailWindow(previewBytes);
163
- try {
164
- try {
165
- for await (const chunk of source) {
166
- hash.update(chunk);
167
- bytes += chunk.byteLength;
168
- if (kept < previewBytes) {
169
- const part = chunk.slice(0, previewBytes - kept);
170
- preview.push(part); kept += part.byteLength;
171
- }
172
- tail.push(chunk);
173
- await file.writeFile(chunk);
174
- }
175
- await file.sync();
176
- } finally { await file.close(); }
177
- const sha256 = hash.digest("hex");
178
- const destination = blobFilePath(dir, sha256);
179
- await mkdir(join(dir, sha256.slice(0, 2)), { recursive: true });
180
- await rename(path, destination);
181
- const shard = await open(join(dir, sha256.slice(0, 2)), "r");
182
- try { await shard.sync(); } finally { await shard.close(); }
183
- const head = Buffer.concat(preview);
184
- return {
185
- ref: { sha256, path: destination, bytes },
186
- preview: new TextDecoder().decode(head, { stream: kept < bytes }),
187
- head, tail: tail.bytes(),
188
- };
189
- } finally { await rm(path, { force: true }); }
190
- }
191
-
192
- /**
193
- * The artifact is `exit N\n` followed by the merged output. Nothing that varies
194
- * run to run (wall time) belongs here: the artifact digest is the D9 novelty
195
- * key, and a timestamp in it would make every rerun look like new evidence.
196
- */
197
- export async function captureCommandObservation(
198
- dir: string, stdout: ReadableStream<Uint8Array>, stderr: ReadableStream<Uint8Array>,
199
- exited: Promise<number>, previewBytes: number,
200
- onOutput?: (chunk: Uint8Array) => void,
201
- separate = false,
202
- limitBytes?: number,
203
- ): Promise<ArchivedObservation & { exit: number; prefixBytes: number; channels: ArtifactRef[]; capture: "complete" | "partial" }> {
204
- const merged = await spoolMerged(dir, [stdout, stderr], exited, onOutput, separate, limitBytes);
205
- try {
206
- const exit = await exited;
207
- const prefix = new TextEncoder().encode(`exit ${exit}\n`);
208
- async function* combined(): AsyncGenerator<Uint8Array> {
209
- yield prefix;
210
- for await (const chunk of createReadStream(merged.path)) yield chunk as Uint8Array;
211
- }
212
- async function* channel(path: string): AsyncGenerator<Uint8Array> {
213
- for await (const chunk of createReadStream(path)) yield chunk as Uint8Array;
214
- }
215
- // Drain all archive attempts before removing their source files, including failures.
216
- const attempts = await Promise.allSettled([archiveObservation(dir, combined(), previewBytes),
217
- ...merged.channels.map(path => archiveObservation(dir, channel(path), 0))]);
218
- const stored = attempts.map(result => { if (result.status === "rejected") throw result.reason; return result.value; });
219
- return { ...stored[0]!, exit, prefixBytes: prefix.byteLength, channels: stored.slice(1).map(item => item.ref),
220
- capture: merged.complete ? "complete" : "partial" };
221
- } finally {
222
- await Promise.all([merged.path, ...merged.channels].map(path => rm(path, { force: true })));
223
- }
224
- }
@@ -1,98 +0,0 @@
1
- /** Package storage is local metadata; only runtime/package-manager may install sources. */
2
- import { mkdir, stat } from "node:fs/promises";
3
- import { join, resolve, relative, isAbsolute, sep } from "node:path";
4
- import { homedir } from "node:os";
5
- import { createHash } from "node:crypto";
6
- import { readJsonFile } from "./config.ts";
7
- import { atomicWrite } from "./atomic.ts";
8
- import { serialized } from "./serial.ts";
9
- import { packageConfig, packageDeclaration, parsePackageSource, type PackageDeclaration, type PackageSource } from "../core/packages.ts";
10
- import type { ResourceKind } from "../core/resource-selection.ts";
11
-
12
- export type PackageScope = "user" | "project" | "cli";
13
- export interface PackageRecord { source: string; identity: string; path: string; version?: string; revision?: string }
14
- export interface PackageRoot {
15
- source: PackageSource; declaration: Exclude<PackageDeclaration,string>; scope: PackageScope; base: string;
16
- root?: string; delta?: Exclude<PackageDeclaration,string>; trusted: boolean;
17
- }
18
- export function packageSource(source: string, base: string): PackageSource {
19
- return parsePackageSource(source, path => resolve(base, path.startsWith("~/") ? join(homedir(),path.slice(2)) : path === "~" ? homedir() : path));
20
- }
21
- export function packageKey(source: PackageSource): string {
22
- // Config identity omits versions, but installed references must retain them:
23
- // publishing a new pin cannot invalidate the old declaration before its atomic write.
24
- const version=source.kind==="npm"?source.version:source.kind==="git"?source.ref:undefined;
25
- return createHash("sha256").update(source.id+"\0"+(version??"")).digest("hex");
26
- }
27
- export function packageHome(base: string): string { return join(base,"packages"); }
28
- export function managedPackagePath(base: string, path: string): string {
29
- const home = packageHome(base), target = resolve(home,path), rel = relative(home,target);
30
- if (!rel || rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) throw Error("Package path escapes its managed directory");
31
- return target;
32
- }
33
- export async function packageRecords(base: string): Promise<Record<string, PackageRecord>> {
34
- const read = await readJsonFile<Record<string,PackageRecord>>(join(packageHome(base),"installed.json"));
35
- if (read.error) throw Error(read.error);
36
- if (read.value && (typeof read.value !== "object" || Array.isArray(read.value))) throw Error("Invalid package installation index");
37
- const records=read.value ?? {};
38
- for(const record of Object.values(records)) {
39
- if (!record || typeof record.source !== "string" || typeof record.identity !== "string" || typeof record.path !== "string") throw Error("Invalid package installation entry");
40
- managedPackagePath(base,record.path);
41
- }
42
- return records;
43
- }
44
- export async function savePackageRecord(base: string, source: PackageSource, record: PackageRecord | undefined): Promise<void> {
45
- const path=join(packageHome(base),"installed.json");
46
- await serialized(path,async()=>{const records=await packageRecords(base); if(record) records[packageKey(source)]=record; else delete records[packageKey(source)]; await mkdir(packageHome(base),{recursive:true}); await atomicWrite(path,JSON.stringify(records,null,2)+"\n");});
47
- }
48
- export async function removePackageRecords(base:string,identity:string):Promise<string[]> {
49
- const path=join(packageHome(base),"installed.json");
50
- return serialized(path,async()=>{const records=await packageRecords(base),keys=Object.keys(records).filter(key=>records[key]!.identity===identity);for(const key of keys)delete records[key];await mkdir(packageHome(base),{recursive:true});await atomicWrite(path,JSON.stringify(records,null,2)+"\n");return keys;});
51
- }
52
- export async function installedPackage(source: PackageSource, base: string): Promise<string | undefined> {
53
- if(source.kind==="local") return await stat(source.path).then(()=>source.path,()=>undefined);
54
- const record=(await packageRecords(base))[packageKey(source)];
55
- if (!record || record.identity!==source.id) return undefined;
56
- const installed=packageSource(record.source,base);
57
- if(source.kind==="npm"&&(installed.kind!=="npm"||installed.version!==source.version)||source.kind==="git"&&(installed.kind!=="git"||installed.ref!==source.ref))return undefined;
58
- const path=managedPackagePath(base,record.path); return await stat(path).then(s=>s.isDirectory()?path:undefined,()=>undefined);
59
- }
60
- export async function configuredPackages(base: string): Promise<PackageDeclaration[]> {
61
- const read=await readJsonFile<Record<string,unknown>>(join(base,"settings.json")); if(read.error)throw Error(read.error);
62
- if(read.value!==null&&(typeof read.value!=="object"||Array.isArray(read.value)))throw Error("Settings must be an object");
63
- const list=read.value?.["packages"] ?? []; if(!Array.isArray(list)||!list.every(packageDeclaration))throw Error("Invalid packages configuration"); return list;
64
- }
65
- export async function savePackageDeclaration(base: string, source: PackageSource, declaration: PackageDeclaration | undefined): Promise<void> {
66
- const file=join(base,"settings.json");
67
- await serialized(file,async()=>{
68
- const read=await readJsonFile<Record<string,unknown>>(file); if(read.error)throw Error(read.error);
69
- if(read.value!==null&&(typeof read.value!=="object"||Array.isArray(read.value)))throw Error("Settings must be an object");
70
- const previous=read.value?.["packages"]??[]; if(!Array.isArray(previous)||!previous.every(packageDeclaration))throw Error("Invalid packages configuration");
71
- const packages=previous.filter(entry=>packageSource(packageConfig(entry).source,base).id!==source.id);
72
- if(declaration!==undefined)packages.push(declaration);
73
- await mkdir(base,{recursive:true}); await atomicWrite(file,JSON.stringify({...read.value,packages},null,2)+"\n");
74
- });
75
- }
76
- export async function packageRoots(user: readonly PackageDeclaration[], project: readonly PackageDeclaration[], opts:{userDir:string;projectDir:string;trusted:boolean}, diagnostics:string[]):Promise<PackageRoot[]> {
77
- const roots=new Map<string,PackageRoot>();
78
- for(const scope of ["user","project"] as const) for(const entry of scope==="user"?user:project) {
79
- try {
80
- const declaration=packageConfig(entry),base=scope==="user"?opts.userDir:opts.projectDir,source=packageSource(declaration.source,base),existing=roots.get(source.id);
81
- if(scope==="project"&&existing&&(!opts.trusted||declaration.autoload===false)) { if(opts.trusted)existing.delta=declaration; continue; }
82
- const root=await installedPackage(source,base);
83
- if(!root)diagnostics.push(`Package ${source.source} is not installed; run xeno install ${source.source}${scope==="project"?" --local":""}`);
84
- roots.set(source.id,{source,declaration,scope,base,...(root?{root}:{}),trusted:scope==="user"||opts.trusted});
85
- } catch(error){diagnostics.push(String(error));}
86
- }
87
- return [...roots.values()];
88
- }
89
- export async function packageManifest(root: string): Promise<Partial<Record<ResourceKind, readonly string[]>>> {
90
- if(await stat(root).then(s=>s.isFile(),()=>false)) return {extensions:[root],skills:[],prompts:[],themes:[]};
91
- const read=await readJsonFile<{xeno?:unknown;pi?:unknown}>(join(root,"package.json"));if(read.error)throw Error(read.error);
92
- const value=read.value?.xeno??read.value?.pi;
93
- if(value===undefined)return {extensions:["extensions"],skills:["skills"],prompts:["prompts"],themes:["themes"]};
94
- if(!value||typeof value!=="object"||Array.isArray(value))throw Error("Invalid package resource manifest");
95
- const result:Partial<Record<ResourceKind,readonly string[]>>={};
96
- for(const kind of ["extensions","skills","prompts","themes"] as const){const paths=(value as Record<string,unknown>)[kind]??[];if(!Array.isArray(paths)||paths.some(path=>typeof path!=="string"))throw Error("Invalid package manifest " + kind);result[kind]=paths;}
97
- return result;
98
- }
@@ -1,92 +0,0 @@
1
- /** R4 / P.3: one fully prepared plan shares the existing confined checkpoint CAS. */
2
- import { relative, resolve } from "node:path";
3
- import { parsePatch, applyPatchChunks, patchPaths, PatchError, type PatchOperation } from "../core/patch.ts";
4
- import { sameVersion, changedPaths, type FileChanges } from "../core/checkpoints.ts";
5
- import { unifiedFileDiff } from "../core/review.ts";
6
- import { isFileDigest } from "../core/file-precondition.ts";
7
- import { ToolFault, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
8
- import { editablePath, readEditableFile, applyFileChanges } from "./file-checkpoints.ts";
9
- import { putBlob } from "./blobs.ts";
10
-
11
- export interface PreparedPatch {
12
- root: string;
13
- changes: FileChanges;
14
- operations: readonly PatchOperation[];
15
- diffs: readonly { path: string; destination?: string; kind: "add" | "update" | "move" | "delete"; diff: string }[];
16
- }
17
- export interface PreparePatchOptions {
18
- observedFileVersion?: (absolute: string) => Promise<string | undefined>;
19
- expectedSha256?: Readonly<Record<string, string>>;
20
- signal?: AbortSignal;
21
- }
22
- const decode = (bytes: Uint8Array) => {
23
- try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); }
24
- catch { throw new ToolFault("invalid_input", "patch requires valid UTF-8 files; use a trusted command for binary data", READ_ATTEMPTED); }
25
- };
26
-
27
- /** No workspace write occurs until every source, destination and hunk passes. */
28
- export async function preparePatch(root: string, blobs: string, text: string, options: PreparePatchOptions = {}): Promise<PreparedPatch> {
29
- let operations: PatchOperation[];
30
- try { operations = parsePatch(text); }
31
- catch (error) { if (error instanceof PatchError) throw new ToolFault(error.code, error.message, NOT_STARTED); throw error; }
32
- const names = new Map<string, string>();
33
- const targets = new Set<string>();
34
- for (const path of patchPaths(operations)) {
35
- const absolute = await editablePath(root, path), canonical = relative(resolve(root), absolute);
36
- if (targets.has(canonical) || [...targets].some(existing => existing.startsWith(canonical + "/") || canonical.startsWith(existing + "/")))
37
- throw new ToolFault("invalid_input", `patch has duplicate or overlapping targets: ${path}`, NOT_STARTED);
38
- names.set(path, canonical); targets.add(canonical);
39
- }
40
- const canonical = (path: string) => names.get(path)!;
41
- const observed = new Map<string, Awaited<ReturnType<typeof readEditableFile>>>();
42
- for (const path of targets) {
43
- if (options.signal?.aborted) throw new ToolFault("cancelled", "patch preparation cancelled", NOT_STARTED);
44
- observed.set(path, await readEditableFile(root, path));
45
- }
46
- const changes: FileChanges = Object.create(null) as FileChanges;
47
- const diffs: PreparedPatch["diffs"][number][] = [];
48
- for (const operation of operations) {
49
- const path = canonical(operation.path), source = observed.get(path)!;
50
- if (operation.kind !== "add") {
51
- if (source.bytes === null || source.version === null) throw new ToolFault("not_found", `patch source is missing: ${path}`, READ_ATTEMPTED);
52
- const explicit = options.expectedSha256?.[operation.path] ?? options.expectedSha256?.[path];
53
- const expected = explicit ?? await options.observedFileVersion?.(resolve(root, path));
54
- if (!isFileDigest(expected) || expected !== source.version.sha256)
55
- throw new ToolFault("version_mismatch", `patch source version was not observed or changed: ${path}; read it before editing`, READ_ATTEMPTED, { recover: { tool: "read", input: { path } } });
56
- } else if (source.version !== null) {
57
- throw new ToolFault("version_mismatch", `patch add requires an absent destination: ${path}; existing bytes were preserved`, READ_ATTEMPTED);
58
- }
59
- let destination = path, content: string | null;
60
- const before = source.bytes === null ? null : decode(source.bytes);
61
- if (operation.kind === "delete") content = null;
62
- else if (operation.kind === "add") content = operation.content;
63
- else {
64
- try { content = applyPatchChunks(before!, operation.chunks); }
65
- catch (error) { if (error instanceof PatchError) throw new ToolFault(error.code, `${path}: ${error.message}`, READ_ATTEMPTED); throw error; }
66
- if (operation.movePath !== undefined) {
67
- destination = canonical(operation.movePath);
68
- if (observed.get(destination)!.version !== null) throw new ToolFault("version_mismatch", `patch move requires an absent destination: ${destination}`, READ_ATTEMPTED);
69
- }
70
- }
71
- if (source.bytes !== null) await putBlob(blobs, source.bytes);
72
- const after = content === null ? null : { sha256: await putBlob(blobs, content), mode: source.version?.mode ?? (0o666 & ~process.umask()) };
73
- if (destination !== path) {
74
- changes[path] = { before: source.version, after: null };
75
- changes[destination] = { before: null, after };
76
- } else changes[path] = { before: source.version, after };
77
- diffs.push({ path, ...(destination === path ? {} : { destination }), kind: destination !== path ? "move" : operation.kind,
78
- diff: unifiedFileDiff(path, before, content, destination) });
79
- }
80
- return { root: resolve(root), operations, changes, diffs };
81
- }
82
-
83
- /** The caller persists prepared/applied checkpoint phases around this operation. */
84
- export async function applyPreparedPatch(root: string, blobs: string, prepared: PreparedPatch, signal?: AbortSignal): Promise<void> {
85
- if (resolve(root) !== prepared.root) throw new ToolFault("invalid_input", "prepared patch names another workspace", NOT_STARTED);
86
- if (signal?.aborted) throw new ToolFault("cancelled", "patch application cancelled", NOT_STARTED);
87
- await applyFileChanges(root, blobs, prepared.changes, signal);
88
- for (const path of changedPaths(prepared.changes)) {
89
- if (!sameVersion((await readEditableFile(root, path)).version, prepared.changes[path]!.after))
90
- throw new Error(`file changed concurrently after patch: ${path}; inspect the prepared checkpoint`);
91
- }
92
- }
@@ -1,34 +0,0 @@
1
- /** R13 / D13: path policy sees aliases and actual resources; confinement remains separate. */
2
- import { realpath } from "node:fs/promises";
3
- import { dirname, basename, resolve, relative, isAbsolute } from "node:path";
4
- import { permissionRequest, normalizePermissionPath, type PermissionRequest } from "../core/permissions.ts";
5
-
6
- async function canonicalPath(path: string): Promise<string> {
7
- try { return await realpath(path); }
8
- catch (error) {
9
- if (!error || typeof error !== "object" || !("code" in error) || !["ENOENT", "ENOTDIR"].includes(String(error.code))) throw error;
10
- const parent = dirname(path);
11
- if (parent === path) throw error;
12
- // New files have no realpath yet; existing ancestors still reveal aliases.
13
- return resolve(await canonicalPath(parent), basename(path));
14
- }
15
- }
16
- function resource(root: string, absolute: string): string {
17
- const path = relative(root, absolute);
18
- return normalizePermissionPath(path === ".." || path.startsWith("../") || isAbsolute(path) ? absolute : path || ".");
19
- }
20
-
21
- export async function permissionPaths(root: string, paths: readonly string[]): Promise<string[]> {
22
- const absoluteRoot = resolve(root), canonicalRoot = await canonicalPath(absoluteRoot), resources: string[] = [];
23
- for (const path of paths) {
24
- const absolute = resolve(absoluteRoot, path);
25
- resources.push(resource(absoluteRoot, absolute), resource(canonicalRoot, await canonicalPath(absolute)));
26
- }
27
- return [...new Set(resources)];
28
- }
29
-
30
- /** Models never supply the root or canonical target; the current runtime does. */
31
- export async function canonicalPermissionRequest(root: string, name: string, input: unknown): Promise<PermissionRequest> {
32
- const request = permissionRequest(name, input);
33
- return request.paths ? { ...request, paths: await permissionPaths(root, request.paths) } : request;
34
- }