@aiwg/cli 2026.10.3 → 2026.10.5

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 (279) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/model-capabilities.v1.json +96 -4
  3. package/bin/aiwg.mjs +50 -27
  4. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  5. package/dist/src/artifacts/browser-export.js +6 -0
  6. package/dist/src/artifacts/checksum-manifest.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +173 -92
  8. package/dist/src/artifacts/citation-table-schema.js +59 -0
  9. package/dist/src/artifacts/cli.js +69 -18
  10. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  11. package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
  12. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  13. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  14. package/dist/src/artifacts/discovery-eval.js +6 -4
  15. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  16. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  17. package/dist/src/artifacts/framework-capabilities.js +171 -0
  18. package/dist/src/artifacts/framework-index-paths.js +73 -0
  19. package/dist/src/artifacts/framework-source-digest.js +75 -0
  20. package/dist/src/artifacts/index-builder.js +286 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +130 -0
  23. package/dist/src/artifacts/index-refresh.js +591 -0
  24. package/dist/src/artifacts/index-worker.mjs +11 -0
  25. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  26. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  27. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  28. package/dist/src/artifacts/query-engine.js +410 -88
  29. package/dist/src/artifacts/relations-builder.js +168 -0
  30. package/dist/src/artifacts/relations.js +148 -0
  31. package/dist/src/artifacts/stats.js +5 -2
  32. package/dist/src/artifacts/types.js +30 -10
  33. package/dist/src/artifacts/watcher.js +1 -1
  34. package/dist/src/auth/credential-store.js +2 -1
  35. package/dist/src/catalog/cli.d.mts +5 -0
  36. package/dist/src/channel/manager.d.mts +20 -0
  37. package/dist/src/channel/manager.mjs +19 -2
  38. package/dist/src/cli/command-log.js +2 -3
  39. package/dist/src/cli/handlers/artifacts.js +3 -0
  40. package/dist/src/cli/handlers/auth.js +3 -0
  41. package/dist/src/cli/handlers/decision.js +19 -12
  42. package/dist/src/cli/handlers/diagnose.js +4 -3
  43. package/dist/src/cli/handlers/effect.js +4 -2
  44. package/dist/src/cli/handlers/evidence.js +3 -0
  45. package/dist/src/cli/handlers/help.js +1 -0
  46. package/dist/src/cli/handlers/index.js +3 -1
  47. package/dist/src/cli/handlers/init.js +4 -1
  48. package/dist/src/cli/handlers/lint.js +5 -0
  49. package/dist/src/cli/handlers/models.js +104 -8
  50. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  51. package/dist/src/cli/handlers/refresh.js +12 -0
  52. package/dist/src/cli/handlers/resource-versions.js +3 -0
  53. package/dist/src/cli/handlers/rules.js +195 -0
  54. package/dist/src/cli/handlers/run.js +4 -0
  55. package/dist/src/cli/handlers/runtime-info.js +1 -2
  56. package/dist/src/cli/handlers/script-runner.js +3 -2
  57. package/dist/src/cli/handlers/session.js +19 -9
  58. package/dist/src/cli/handlers/sessions.js +137 -43
  59. package/dist/src/cli/handlers/setup-manifest.js +39 -44
  60. package/dist/src/cli/handlers/setup.js +41 -0
  61. package/dist/src/cli/handlers/steward.js +11 -0
  62. package/dist/src/cli/handlers/subcommands.js +57 -2
  63. package/dist/src/cli/handlers/uhp.js +3 -0
  64. package/dist/src/cli/handlers/use.js +294 -55
  65. package/dist/src/cli/handlers/utilities.js +26 -10
  66. package/dist/src/cli/handlers/version.js +8 -0
  67. package/dist/src/cli/router.js +15 -1
  68. package/dist/src/cli/services/deployment-verification.js +19 -9
  69. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  70. package/dist/src/cli/skill-usage.js +2 -3
  71. package/dist/src/config/aiwg-config.js +87 -37
  72. package/dist/src/config/cli.js +111 -5
  73. package/dist/src/config/decision-config.js +106 -0
  74. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  75. package/dist/src/config/gitignore.js +50 -0
  76. package/dist/src/config/index-graph-overrides.js +36 -0
  77. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  78. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  79. package/dist/src/config/user-config-dir.d.mts +9 -0
  80. package/dist/src/config/user-config.js +2 -1
  81. package/dist/src/config/user-registry.js +23 -15
  82. package/dist/src/config/user-state.js +7 -0
  83. package/dist/src/decision/ask.js +203 -43
  84. package/dist/src/decision/context-live-qualification.js +2 -2
  85. package/dist/src/decision/driver.js +13 -8
  86. package/dist/src/decision/ensemble/aggregate.js +23 -10
  87. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  88. package/dist/src/decision/entry.js +2 -15
  89. package/dist/src/decision/graph-live-qualification.js +1 -1
  90. package/dist/src/decision/graph-run.js +9 -0
  91. package/dist/src/decision/heldout/collector.js +5 -13
  92. package/dist/src/decision/heldout/contract.js +30 -7
  93. package/dist/src/decision/jev-credentials.js +15 -1
  94. package/dist/src/decision/ledger.js +131 -28
  95. package/dist/src/decision/offload-activation.js +137 -0
  96. package/dist/src/decision/offload.js +550 -0
  97. package/dist/src/decision/qualification/load.js +4 -6
  98. package/dist/src/decision/savings.js +28 -0
  99. package/dist/src/effects/types.js +5 -1
  100. package/dist/src/effects/verifiers/git.js +2 -1
  101. package/dist/src/effects/verifiers/tracker.js +26 -14
  102. package/dist/src/extensions/commands/definitions.js +19 -0
  103. package/dist/src/extensions/project-local-remove.js +36 -1
  104. package/dist/src/gates/providers/loader.js +61 -17
  105. package/dist/src/installation/manager-command.d.mts +18 -0
  106. package/dist/src/installation/manager-command.mjs +14 -2
  107. package/dist/src/installation/manager.d.mts +49 -0
  108. package/dist/src/installation/manager.mjs +78 -3
  109. package/dist/src/issues/cli.js +4 -4
  110. package/dist/src/issues/local.js +24 -5
  111. package/dist/src/jobs/runner.js +1 -1
  112. package/dist/src/lint/cli.js +27 -1
  113. package/dist/src/lint/loader.js +2 -1
  114. package/dist/src/lint/reporters.js +25 -1
  115. package/dist/src/lint/rule-activation.js +43 -0
  116. package/dist/src/lint/runner.js +137 -62
  117. package/dist/src/mcp/adapters/codex-runtime.js +7 -1
  118. package/dist/src/mcp/claude-install.d.mts +4 -0
  119. package/dist/src/mcp/claude-install.mjs +41 -0
  120. package/dist/src/mcp/cli.d.mts +1 -0
  121. package/dist/src/mcp/cli.mjs +36 -11
  122. package/dist/src/mcp/config-file.d.mts +28 -0
  123. package/dist/src/mcp/config-layers.d.mts +36 -0
  124. package/dist/src/mcp/credentials.d.mts +51 -0
  125. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  126. package/dist/src/mcp/grok-build-config.mjs +1 -1
  127. package/dist/src/mcp/helpers.mjs +3 -2
  128. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  129. package/dist/src/mcp/muse-native-config.mjs +1 -0
  130. package/dist/src/mcp/omp-config.d.mts +4 -0
  131. package/dist/src/mcp/registry.js +3 -93
  132. package/dist/src/mcp/registry.mjs +3 -87
  133. package/dist/src/mcp/server-config.d.mts +3 -0
  134. package/dist/src/mcp/server-config.mjs +89 -0
  135. package/dist/src/mcp/server.mjs +37 -375
  136. package/dist/src/mcp/test-simple.mjs +2 -1
  137. package/dist/src/mcp/toml-editor.d.mts +4 -0
  138. package/dist/src/mcp/toml-string.d.mts +1 -0
  139. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  140. package/dist/src/mcp/tool-filters.d.mts +46 -0
  141. package/dist/src/mcp/tools/decision.mjs +12 -11
  142. package/dist/src/mcp/tools/discovery.mjs +62 -55
  143. package/dist/src/mcp/tools/orchestration.mjs +12 -2
  144. package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
  145. package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
  146. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
  147. package/dist/src/mission-protocol/codecs.js +8 -0
  148. package/dist/src/models/config-loader.js +7 -2
  149. package/dist/src/models/model-capabilities.v1.json +96 -4
  150. package/dist/src/models/model-discovery.js +76 -11
  151. package/dist/src/models/provider-policy.js +101 -7
  152. package/dist/src/models/qualification-digest.d.mts +2 -0
  153. package/dist/src/models/resolver.js +12 -10
  154. package/dist/src/models/worker-direct-readiness.js +65 -44
  155. package/dist/src/models/worker-dispatch-offer.js +3 -3
  156. package/dist/src/models/worker-health-readiness.js +16 -3
  157. package/dist/src/models/worker-installation.d.mts +39 -0
  158. package/dist/src/models/worker-installation.mjs +135 -28
  159. package/dist/src/process/bounded-child-process.d.mts +84 -0
  160. package/dist/src/process/bounded-child-process.mjs +43 -0
  161. package/dist/src/process/cli-command.d.mts +16 -0
  162. package/dist/src/process/signal-child.d.mts +6 -0
  163. package/dist/src/process/signal-child.mjs +7 -0
  164. package/dist/src/providers/deployed-skill-roots.js +20 -0
  165. package/dist/src/providers/grok-build-acp.js +3 -2
  166. package/dist/src/providers/grok-build-headless.js +12 -4
  167. package/dist/src/providers/grok-build-inspect.js +1 -0
  168. package/dist/src/providers/omp-agent.d.mts +8 -0
  169. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  170. package/dist/src/providers/omp-paths.d.mts +13 -0
  171. package/dist/src/providers/skill-ownership.d.mts +9 -0
  172. package/dist/src/providers/skill-ownership.mjs +88 -0
  173. package/dist/src/rules/activation.d.mts +21 -0
  174. package/dist/src/rules/activation.mjs +62 -0
  175. package/dist/src/rules/inline-budget.d.mts +26 -0
  176. package/dist/src/rules/inline-budget.mjs +66 -0
  177. package/dist/src/rules/inventory.js +129 -0
  178. package/dist/src/rules/pin-state.js +73 -0
  179. package/dist/src/rules/pins.d.mts +14 -0
  180. package/dist/src/rules/pins.mjs +112 -0
  181. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  182. package/dist/src/serve/mission-conductor.js +38 -12
  183. package/dist/src/serve/worker-invocation.js +30 -10
  184. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  185. package/dist/src/sessions/adapter-registry.js +23 -19
  186. package/dist/src/sessions/adapters/claude.js +39 -54
  187. package/dist/src/sessions/adapters/codex.js +95 -67
  188. package/dist/src/sessions/adapters/copilot.js +4 -4
  189. package/dist/src/sessions/adapters/cursor.js +40 -57
  190. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  191. package/dist/src/sessions/adapters/factory.js +31 -27
  192. package/dist/src/sessions/adapters/generic.js +10 -10
  193. package/dist/src/sessions/adapters/grok-build.js +11 -11
  194. package/dist/src/sessions/adapters/grokbot.js +24 -13
  195. package/dist/src/sessions/adapters/hermes.js +27 -29
  196. package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
  197. package/dist/src/sessions/adapters/muse.js +150 -93
  198. package/dist/src/sessions/adapters/omp.js +28 -14
  199. package/dist/src/sessions/adapters/openclaw.js +89 -79
  200. package/dist/src/sessions/adapters/opencode.js +27 -24
  201. package/dist/src/sessions/adapters/openhuman.js +18 -60
  202. package/dist/src/sessions/adapters/pi.js +29 -14
  203. package/dist/src/sessions/adapters/warp.js +3 -3
  204. package/dist/src/sessions/adapters/windsurf.js +19 -48
  205. package/dist/src/sessions/batch-contracts.js +16 -8
  206. package/dist/src/sessions/batch-import.js +252 -12
  207. package/dist/src/sessions/contracts.js +50 -3
  208. package/dist/src/sessions/coverage-preflight.js +27 -2
  209. package/dist/src/sessions/export-plan.js +4 -3
  210. package/dist/src/sessions/import-exclusions.js +72 -0
  211. package/dist/src/sessions/import-lease.js +34 -18
  212. package/dist/src/sessions/import-limits.js +37 -0
  213. package/dist/src/sessions/importer.js +60 -38
  214. package/dist/src/sessions/lifecycle.js +45 -0
  215. package/dist/src/sessions/policy.js +145 -26
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +105 -0
  218. package/dist/src/sessions/repository.js +151 -54
  219. package/dist/src/sessions/workspace-discovery.js +292 -60
  220. package/dist/src/sessions/workspace-matching.js +107 -0
  221. package/dist/src/skills/importer.js +1 -0
  222. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  223. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  224. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  225. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  226. package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
  227. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  228. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  229. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
  230. package/dist/src/storage/backends/fs.js +18 -76
  231. package/dist/src/storage/backends/obsidian.js +1 -0
  232. package/dist/src/tracker/forge-http.js +2 -1
  233. package/dist/src/update/checker.d.mts +2 -0
  234. package/dist/src/update/service.d.mts +22 -0
  235. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  236. package/dist/src/utils/contained-filesystem.mjs +683 -0
  237. package/dist/src/utils/temporary-directory.js +36 -0
  238. package/dist/src/writing/example-generator.js +1 -1
  239. package/package.json +15 -2
  240. package/schemas/models/model-catalog.v1.schema.json +1 -1
  241. package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
  242. package/tools/_resolve-impl.mjs +13 -15
  243. package/tools/agents/deploy-agents.mjs +49 -21
  244. package/tools/agents/providers/base.mjs +104 -134
  245. package/tools/agents/providers/claude.mjs +8 -24
  246. package/tools/agents/providers/codex.mjs +18 -11
  247. package/tools/agents/providers/cursor.mjs +19 -18
  248. package/tools/agents/providers/grok-build.mjs +3 -1
  249. package/tools/agents/providers/hermes.mjs +57 -47
  250. package/tools/agents/providers/openclaw.mjs +9 -4
  251. package/tools/agents/providers/pi.mjs +5 -0
  252. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  253. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  254. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  255. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  256. package/tools/agents/providers/warp.mjs +20 -26
  257. package/tools/cli/decision-offload-report.mjs +40 -0
  258. package/tools/cli/workspace-status.mjs +862 -0
  259. package/tools/plugin/package-plugins.mjs +186 -8
  260. package/tools/plugin/plugin-links.mjs +114 -0
  261. package/tools/providers/antigravity-transport.mjs +3 -1
  262. package/tools/providers/deepseek-harness-transport.mjs +6 -4
  263. package/tools/providers/omp-transport.mjs +4 -2
  264. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  265. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  266. package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
  267. package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
  268. package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
  269. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  270. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  271. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  272. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  273. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  274. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  275. package/tools/ralph-external/process-monitor.mjs +1017 -0
  276. package/tools/ralph-external/session-launcher.mjs +5057 -0
  277. package/tools/scripts/resource-preflight.mjs +541 -0
  278. package/tools/skills/deploy-skills-codex.mjs +2 -1
  279. package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
@@ -0,0 +1,683 @@
1
+ import fs from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { randomUUID } from 'node:crypto';
4
+ import path from 'node:path';
5
+
6
+ // Extracted from MCP artifact access; default diagnostics and write semantics
7
+ // remain compatible. Storage opts into hardlink refusal and root creation.
8
+ function assertArtifactContained(root, target) {
9
+ const relative = path.relative(root, target);
10
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
11
+ throw new Error('Artifact path must stay beneath the artifact root');
12
+ }
13
+ }
14
+
15
+ // Bigint stats preserve nanosecond birth times and exact device/inode values.
16
+ // Platforms without birth time (zero/absent on both sides) fall back to dev+ino.
17
+ function sameIdentity(a, b) {
18
+ return a.dev === b.dev && a.ino === b.ino
19
+ && (a.birthtimeNs || 0n) === (b.birthtimeNs || 0n);
20
+ }
21
+
22
+ function assertIdentity(info) {
23
+ const integer = value => typeof value === 'bigint' || Number.isSafeInteger(value);
24
+ if (!info || !integer(info.dev) || info.dev < 0
25
+ || !integer(info.ino) || info.ino <= 0
26
+ || !integer(info.nlink) || info.nlink <= 0) {
27
+ throw new Error('Artifact identity cannot be verified');
28
+ }
29
+ }
30
+
31
+ function rootedArtifactAccess() {
32
+ return process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY;
33
+ }
34
+
35
+ /** A root pinned as missing must be created by us, never adopted later. */
36
+ async function createArtifactRoot(store) {
37
+ let parent;
38
+ let directory;
39
+ let verified = false;
40
+ try {
41
+ let entry = store.configured;
42
+ if (rootedArtifactAccess()) {
43
+ parent = await fs.open(store.projectRoot, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
44
+ if (await fs.realpath(`/proc/self/fd/${parent.fd}`) !== store.projectRoot) {
45
+ throw new Error('Project root changed during artifact creation');
46
+ }
47
+ entry = `/proc/self/fd/${parent.fd}/.aiwg`;
48
+ }
49
+ // EEXIST is a refusal, including a symlink or directory installed since pinning.
50
+ await fs.mkdir(entry);
51
+ const created = await fs.lstat(entry, { bigint: true });
52
+ if (created.isSymbolicLink() || !created.isDirectory()) throw new Error('Artifact root changed during creation');
53
+ // Windows cannot open directory handles through fs.open; its fallback
54
+ // uses static identity checks, as do the subsequent artifact operations.
55
+ if (process.platform !== 'win32') {
56
+ directory = await fs.open(entry, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
57
+ }
58
+ const opened = directory ? await directory.stat({ bigint: true }) : await fs.stat(entry, { bigint: true });
59
+ const current = await fs.lstat(entry, { bigint: true });
60
+ if (current.isSymbolicLink() || !opened.isDirectory()
61
+ || !sameIdentity(opened, created)
62
+ || !sameIdentity(opened, current)
63
+ || await fs.realpath(store.configured) !== store.root) {
64
+ throw new Error('Artifact root changed during creation');
65
+ }
66
+ assertIdentity(opened);
67
+ store.identity = opened;
68
+ verified = true;
69
+ } finally {
70
+ const closed = await Promise.allSettled([...(directory ? [directory.close()] : []), ...(parent ? [parent.close()] : [])]);
71
+ const failure = closed.find(result => result.status === 'rejected');
72
+ if (verified && failure) throw failure.reason;
73
+ }
74
+ }
75
+
76
+ /** Pin once. Artifacts bootstrap only their default; storage opts into configured-root creation. */
77
+ async function pinArtifactRoot(configured, projectRoot, options = {}) {
78
+ try {
79
+ const root = await fs.realpath(configured);
80
+ const identity = await fs.stat(root, { bigint: true });
81
+ const store = { configured, projectRoot, root, identity, ...options };
82
+ await verifyArtifactRoot(store);
83
+ return store;
84
+ } catch (error) {
85
+ if (error.code === 'ENOENT' && options.createMissing) {
86
+ try { return await pinMissingRoot(configured, options); }
87
+ catch (missingError) { return { configured, projectRoot, root: configured, ...options, error: missingError }; }
88
+ }
89
+ if (error.code === 'ENOENT' && projectRoot && configured === path.join(projectRoot, '.aiwg')) {
90
+ const existing = await fs.lstat(configured, { bigint: true }).catch(missing => {
91
+ if (missing.code !== 'ENOENT') throw missing;
92
+ });
93
+ if (!existing) return { configured, projectRoot, root: configured, identity: null };
94
+ // A symlink (or other unexpected node) present where the default root
95
+ // should be created must not become the containment root later.
96
+ return {
97
+ configured,
98
+ projectRoot,
99
+ root: configured,
100
+ error: new Error('Default artifact root is not a creatable directory'),
101
+ };
102
+ }
103
+ // Keep discovery available, but never adopt a store that was unavailable
104
+ // at startup (including a dangling alias) on a later request.
105
+ return { configured, projectRoot, root: configured, error };
106
+ }
107
+ }
108
+
109
+ async function verifyArtifactRoot(store) {
110
+ if (store.error) throw store.error;
111
+ const canonical = await fs.realpath(store.configured);
112
+ const current = await fs.stat(store.configured, { bigint: true });
113
+ assertIdentity(current);
114
+ assertIdentity(store.identity);
115
+ if (canonical !== store.root || !current.isDirectory()
116
+ || !sameIdentity(current, store.identity)) {
117
+ throw new Error('Artifact root changed since it was pinned');
118
+ }
119
+ }
120
+
121
+ async function pinnedArtifactRoot(store, write) {
122
+ if (store.error) throw store.error;
123
+ if (!store.identity) {
124
+ if (!write) throw new Error('Default artifact root has not been created');
125
+ // Serialize first-write initialization so concurrent requests share one
126
+ // identity. Creation still rejects symlinks and uses a project descriptor.
127
+ store.initialization ||= store.creationAnchor ? initializeMissingRoot(store) : createArtifactRoot(store);
128
+ await store.initialization;
129
+ }
130
+ // All members of a storage cohort observe the same completed creation proof,
131
+ // including verification/close failures after identity has been recorded.
132
+ if (store.creationAnchor && store.initialization) await store.initialization;
133
+ await verifyArtifactRoot(store);
134
+ return store.root;
135
+ }
136
+
137
+ export function validateContainedPath(artifactPath) {
138
+ if (typeof artifactPath !== 'string' || !artifactPath || path.isAbsolute(artifactPath) || path.win32.isAbsolute(artifactPath)
139
+ || /^[A-Za-z]:/.test(artifactPath)
140
+ || (process.platform === 'win32' && artifactPath.split(/[\\/]/).some(part => part !== '.'
141
+ && (/[<>:"|?*\x00-\x1f]/.test(part) || /[. ]$/.test(part)
142
+ || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))))
143
+ || artifactPath.split(/[\\/]/).includes('..')) {
144
+ throw new Error('Artifact path must be relative and contain no .. segments');
145
+ }
146
+ }
147
+
148
+ /** Resolve existing aliases before creating anything; dangling symlinks fail closed. */
149
+ async function resolveArtifactPath(store, artifactPath, { write = false } = {}) {
150
+ validateContainedPath(artifactPath);
151
+ if (write && store.protectLocationPointer !== false && artifactPath.split(/[\\/]/).some(part => /^\.aiwg-location[. ]*(?::.*)?$/i.test(part))) {
152
+ throw new Error('Refusing to write an artifact location pointer');
153
+ }
154
+ const root = await pinnedArtifactRoot(store, write);
155
+ const target = path.resolve(root, artifactPath);
156
+ assertArtifactContained(root, target);
157
+ if (!rootedArtifactAccess()) {
158
+ let componentPath = root;
159
+ for (const component of path.relative(root, target).split(path.sep)) {
160
+ componentPath = path.join(componentPath, component);
161
+ const info = await fs.lstat(componentPath, { bigint: true }).catch(error => {
162
+ if (error.code !== 'ENOENT') throw error;
163
+ });
164
+ if (info?.isSymbolicLink()) throw new Error('Refusing artifact symlink component');
165
+ }
166
+ }
167
+ let ancestor = path.dirname(target);
168
+ const missing = [];
169
+ while (true) {
170
+ try {
171
+ // lstat distinguishes a missing directory from a dangling symlink.
172
+ await fs.lstat(ancestor, { bigint: true });
173
+ break;
174
+ } catch (error) {
175
+ if (error.code !== 'ENOENT') throw error;
176
+ missing.unshift(path.basename(ancestor));
177
+ ancestor = path.dirname(ancestor);
178
+ }
179
+ }
180
+ const realAncestor = await fs.realpath(ancestor).catch(error => {
181
+ error.containmentRefusal = true;
182
+ throw error;
183
+ });
184
+ if (realAncestor !== root) assertArtifactContained(root, realAncestor);
185
+ const fullPath = path.resolve(realAncestor, ...missing, path.basename(target));
186
+ assertArtifactContained(root, fullPath);
187
+ let info;
188
+ try {
189
+ info = await fs.lstat(fullPath, { bigint: true });
190
+ } catch (error) {
191
+ if (error.code !== 'ENOENT') throw error;
192
+ }
193
+ if (info) {
194
+ if (write && info.isSymbolicLink()) throw new Error('Refusing to write through an artifact symlink');
195
+ const realTarget = await fs.realpath(fullPath).catch(error => {
196
+ error.containmentRefusal = true;
197
+ throw error;
198
+ });
199
+ assertArtifactContained(root, realTarget);
200
+ if (!write) return { root, fullPath: realTarget };
201
+ }
202
+ return { root, fullPath };
203
+ }
204
+
205
+ /**
206
+ * Linux procfs descriptor paths anchor each lookup to an opened directory.
207
+ * Other platforms use static containment checks. Writes use a fresh inode;
208
+ * failed verification closes handles without unsafe pathname cleanup.
209
+ * Concurrent same-user relocation after verification is not a sandbox boundary.
210
+ */
211
+ async function openArtifact(store, artifactPath, { write = false, append = false, remove = false, listing = false } = {}) {
212
+ const mutate = write || append || remove;
213
+ const create = write || append;
214
+ const { root, fullPath } = await resolveArtifactPath(store, artifactPath, { write: mutate });
215
+ if (!rootedArtifactAccess()) return openStaticArtifact(store, root, fullPath, { write, append, remove, listing });
216
+ const directories = [];
217
+ let file;
218
+ let transferred = false;
219
+ const descriptorPath = handle => `/proc/self/fd/${handle.fd}`;
220
+ async function verify(handle, { rootHandle = false, directory = false, entry } = {}) {
221
+ const actual = await fs.realpath(descriptorPath(handle));
222
+ if (rootHandle) {
223
+ if (actual !== root) throw new Error('Artifact root changed during access');
224
+ } else {
225
+ assertArtifactContained(root, actual);
226
+ }
227
+ const [opened, current] = await Promise.all([handle.stat({ bigint: true }), fs.lstat(entry || actual, { bigint: true })]);
228
+ if (rootHandle) {
229
+ await verifyArtifactRoot(store);
230
+ if (!sameIdentity(opened, store.identity)) {
231
+ throw new Error('Artifact root changed since it was pinned');
232
+ }
233
+ }
234
+ assertIdentity(opened);
235
+ assertIdentity(current);
236
+ if (current.isSymbolicLink() || !sameIdentity(opened, current)
237
+ || (directory ? !opened.isDirectory() : !opened.isFile())) {
238
+ throw new Error('Artifact changed during access or is not a regular file/directory');
239
+ }
240
+ if (!directory && (opened.nlink > 1 || current.nlink > 1)) {
241
+ throw new Error('Refusing multiply-linked artifact file');
242
+ }
243
+ return actual;
244
+ }
245
+ async function close() {
246
+ const closed = await Promise.allSettled([...(file ? [file.close()] : []), ...directories.map(handle => handle.close())]);
247
+ const failure = closed.find(result => result.status === 'rejected');
248
+ if (failure) throw failure.reason;
249
+ }
250
+ try {
251
+ let parent = await fs.open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
252
+ directories.push(parent);
253
+ await verify(parent, { rootHandle: true, directory: true });
254
+ const components = path.relative(root, fullPath).split(path.sep);
255
+ const name = components.pop();
256
+ for (const component of components) {
257
+ const entry = path.join(descriptorPath(parent), component);
258
+ let child;
259
+ try {
260
+ child = await fs.open(entry, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
261
+ } catch (error) {
262
+ if (!create || error.code !== 'ENOENT') throw error;
263
+ await verify(parent, { rootHandle: directories.length === 1, directory: true });
264
+ try {
265
+ await fs.mkdir(entry);
266
+ } catch (mkdirError) {
267
+ if (mkdirError.code !== 'EEXIST') throw mkdirError;
268
+ }
269
+ child = await fs.open(entry, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
270
+ }
271
+ directories.push(child);
272
+ await verify(child, { directory: true });
273
+ parent = child;
274
+ }
275
+ const parentOptions = { rootHandle: directories.length === 1, directory: true };
276
+ await verify(parent, parentOptions);
277
+ const destination = path.join(descriptorPath(parent), name);
278
+ async function verifyDestination() {
279
+ const info = await fs.lstat(destination, { bigint: true }).catch(error => {
280
+ if (error.code !== 'ENOENT') throw error;
281
+ });
282
+ if (info && (info.isSymbolicLink() || !info.isFile())) {
283
+ throw new Error('Refusing to replace an artifact symlink or nonregular file');
284
+ }
285
+ if (info) {
286
+ assertIdentity(info);
287
+ if ((store.rejectLinkedWrites || append || remove) && info.nlink > 1) {
288
+ throw new Error('Refusing multiply-linked artifact file');
289
+ }
290
+ }
291
+ return info;
292
+ }
293
+ if (listing) {
294
+ transferred = true;
295
+ return { close, async entries() {
296
+ await verify(directories[0], { rootHandle: true, directory: true });
297
+ await verify(parent, parentOptions);
298
+ const entries = await directoryEntries(descriptorPath(parent));
299
+ await verify(parent, parentOptions);
300
+ return entries;
301
+ } };
302
+ }
303
+ const targetInfo = mutate ? await verifyDestination() : undefined;
304
+ if (remove) {
305
+ if (!targetInfo) throw Object.assign(new Error('Artifact file not found'), { code: 'ENOENT' });
306
+ // Deletion requires directory permission, not read permission on the leaf.
307
+ transferred = true;
308
+ return { close, async unlink() {
309
+ await verify(directories[0], { rootHandle: true, directory: true });
310
+ await verify(parent, parentOptions);
311
+ const current = await verifyDestination();
312
+ if (!current || !sameIdentity(current, targetInfo)) throw new Error('Artifact changed during access');
313
+ await fs.unlink(destination);
314
+ } };
315
+ }
316
+ const finalMode = targetInfo ? Number(targetInfo.mode) & 0o7777 : 0o666 & ~process.umask();
317
+ const entry = write ? path.join(descriptorPath(parent), `.aiwg-write-${randomUUID()}.tmp`) : destination;
318
+ file = await fs.open(entry,
319
+ (write ? constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL
320
+ : append ? constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT
321
+ : constants.O_RDONLY)
322
+ | constants.O_NOFOLLOW | constants.O_NONBLOCK, append ? 0o666 : 0o600);
323
+ await verify(file, { entry });
324
+ transferred = true;
325
+ return {
326
+ handle: file,
327
+ fullPath,
328
+ close,
329
+ async appendContent(content) {
330
+ await verify(directories[0], { rootHandle: true, directory: true });
331
+ await verify(parent, parentOptions);
332
+ await verify(file, { entry });
333
+ await file.writeFile(content, 'utf8');
334
+ },
335
+ async commit() {
336
+ await verify(directories[0], { rootHandle: true, directory: true });
337
+ await verify(file, { entry });
338
+ await verify(parent, parentOptions);
339
+ await verifyDestination();
340
+ await file.chmod(finalMode);
341
+ await fs.rename(entry, destination);
342
+ },
343
+ };
344
+ } finally {
345
+ if (!transferred) await Promise.allSettled([close()]);
346
+ }
347
+ }
348
+
349
+ /** Static checks for platforms without Linux descriptor paths. */
350
+ async function openStaticArtifact(store, root, fullPath, { write, append, remove, listing }) {
351
+ const create = write || append;
352
+ // Inspect the lexical path as well as the resolved path: contained aliases
353
+ // must not hide symlink components on this fallback.
354
+ async function check(target, { allowMissing = false } = {}) {
355
+ const relative = path.relative(root, target);
356
+ assertArtifactContained(root, target);
357
+ let current = root;
358
+ for (const component of relative.split(path.sep)) {
359
+ current = path.join(current, component);
360
+ const info = await fs.lstat(current, { bigint: true }).catch(error => {
361
+ if (!allowMissing || error.code !== 'ENOENT') throw error;
362
+ });
363
+ if (!info) continue;
364
+ if (info.isSymbolicLink()) throw new Error('Refusing artifact symlink component');
365
+ assertIdentity(info);
366
+ assertArtifactContained(root, await fs.realpath(current));
367
+ }
368
+ }
369
+ if (listing) {
370
+ const directory = path.dirname(fullPath);
371
+ if (directory !== root) await check(directory);
372
+ const identity = await fs.lstat(directory, { bigint: true });
373
+ assertIdentity(identity);
374
+ if (!identity.isDirectory()) throw new Error('Artifact must be a directory');
375
+ async function verifyDirectory() {
376
+ await verifyArtifactRoot(store);
377
+ if (directory !== root) await check(directory);
378
+ const current = await fs.lstat(directory, { bigint: true });
379
+ assertIdentity(current);
380
+ if (!sameIdentity(current, identity)) throw new Error('Artifact changed during access');
381
+ }
382
+ return { close: async () => {}, async entries() {
383
+ await verifyDirectory();
384
+ const entries = await directoryEntries(directory);
385
+ await verifyDirectory();
386
+ return entries;
387
+ } };
388
+ }
389
+ await check(fullPath, { allowMissing: create });
390
+ if (create) await fs.mkdir(path.dirname(fullPath), { recursive: true });
391
+ const target = await fs.lstat(fullPath, { bigint: true }).catch(error => {
392
+ if (!create || error.code !== 'ENOENT') throw error;
393
+ });
394
+ if (target && (!target.isFile() || target.isSymbolicLink())) throw new Error('Artifact must be a regular file');
395
+ if (target) assertIdentity(target);
396
+ if (target && (!write || store.rejectLinkedWrites) && target.nlink > 1) throw new Error('Refusing multiply-linked artifact file');
397
+ if (remove) {
398
+ return { close: async () => {}, async unlink() {
399
+ await verifyArtifactRoot(store);
400
+ await check(fullPath);
401
+ const current = await fs.lstat(fullPath, { bigint: true });
402
+ assertIdentity(current);
403
+ if (!current.isFile() || current.nlink > 1 || !sameIdentity(current, target)) {
404
+ throw new Error('Refusing nonregular or multiply-linked artifact file');
405
+ }
406
+ await fs.unlink(fullPath);
407
+ } };
408
+ }
409
+ const entry = write ? path.join(path.dirname(fullPath), `.aiwg-write-${randomUUID()}.tmp`) : fullPath;
410
+ const finalMode = target ? Number(target.mode) & 0o7777 : 0o666 & ~process.umask();
411
+ const handle = await fs.open(entry, (write ? constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL
412
+ : append ? constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT
413
+ : constants.O_RDONLY) | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0), append ? 0o666 : 0o600);
414
+ try {
415
+ const opened = await handle.stat({ bigint: true });
416
+ assertIdentity(opened);
417
+ const current = await fs.lstat(entry, { bigint: true });
418
+ assertIdentity(current);
419
+ await verifyArtifactRoot(store);
420
+ if (target && !write && !sameIdentity(opened, target)) throw new Error('Artifact changed during access');
421
+ if (!sameIdentity(opened, current) || current.isSymbolicLink()) throw new Error('Artifact changed during access');
422
+ if (!opened.isFile() || opened.nlink > 1) throw new Error('Refusing nonregular or multiply-linked artifact file');
423
+ async function verifyFile() {
424
+ await verifyArtifactRoot(store);
425
+ await check(fullPath);
426
+ const current = await fs.lstat(entry, { bigint: true });
427
+ const actual = await handle.stat({ bigint: true });
428
+ assertIdentity(current); assertIdentity(actual);
429
+ if (!current.isFile() || current.nlink > 1 || actual.nlink > 1
430
+ || !sameIdentity(current, actual)
431
+ || !sameIdentity(actual, opened)) {
432
+ throw new Error('Refusing nonregular or multiply-linked artifact file');
433
+ }
434
+ }
435
+ return {
436
+ handle, fullPath, close: () => handle.close(),
437
+ async appendContent(content) { await verifyFile(); await handle.writeFile(content, 'utf8'); },
438
+ async commit() {
439
+ await verifyArtifactRoot(store);
440
+ await check(fullPath, { allowMissing: true });
441
+ const destination = await fs.lstat(fullPath, { bigint: true }).catch(error => { if (error.code !== 'ENOENT') throw error; });
442
+ if (destination) {
443
+ assertIdentity(destination);
444
+ if (!destination.isFile()) throw new Error('Artifact must be a regular file');
445
+ if (store.rejectLinkedWrites && destination.nlink > 1) throw new Error('Refusing multiply-linked artifact file');
446
+ }
447
+ const temporary = await fs.lstat(entry, { bigint: true });
448
+ const actual = await handle.stat({ bigint: true });
449
+ assertIdentity(temporary); assertIdentity(actual);
450
+ if (temporary.isSymbolicLink() || actual.nlink > 1 || temporary.nlink > 1
451
+ || !sameIdentity(actual, temporary)
452
+ || !sameIdentity(actual, opened)) throw new Error('Artifact changed during access');
453
+ await handle.chmod(finalMode);
454
+ await fs.rename(entry, fullPath);
455
+ },
456
+ };
457
+ } catch (error) {
458
+ await handle.close().catch(() => {});
459
+ throw error;
460
+ }
461
+ }
462
+
463
+ // Only missing storage roots participate. Weak references do not keep unused
464
+ // adapters alive, and entries disappear when creation settles. Existing roots
465
+ // always take the normal fresh-pin path; this is not a process-wide trust cache.
466
+ const missingStorageRoots = new Map();
467
+ const forgottenStorageRoots = new FinalizationRegistry(({ key, reference }) => {
468
+ if (missingStorageRoots.get(key) === reference) missingStorageRoots.delete(key);
469
+ });
470
+
471
+ function missingStorageRootKey(configured, options) {
472
+ return JSON.stringify([configured, options.protectLocationPointer, options.rejectLinkedWrites]);
473
+ }
474
+
475
+ async function initializeMissingRoot(store) {
476
+ try {
477
+ await createMissingRoot(store);
478
+ } finally {
479
+ store.creationSettled = true;
480
+ if (missingStorageRoots.get(store.creationKey) === store.creationReference) {
481
+ missingStorageRoots.delete(store.creationKey);
482
+ }
483
+ forgottenStorageRoots.unregister(store.creationReference);
484
+ }
485
+ }
486
+
487
+ /** Pin the existing ancestor of a configured, not-yet-created storage root. */
488
+ async function pinMissingRoot(configured, options) {
489
+ const key = missingStorageRootKey(configured, options);
490
+ const pending = missingStorageRoots.get(key)?.deref();
491
+ let ancestor = configured;
492
+ const components = [];
493
+ while (true) {
494
+ const info = await fs.lstat(ancestor, { bigint: true }).catch(error => { if (error.code !== 'ENOENT') throw error; });
495
+ if (info) break; // Includes dangling aliases: realpath below must refuse them.
496
+ components.unshift(path.basename(ancestor));
497
+ ancestor = path.dirname(ancestor);
498
+ }
499
+ if (!components.length) {
500
+ // Creation may have completed while this request was pinning an observed
501
+ // missing root. Only a cohort's own completed mkdir/identity proof permits
502
+ // joining it; an independently installed root still refuses.
503
+ const shared = pending || missingStorageRoots.get(key)?.deref();
504
+ if (!shared?.initialization) throw new Error('Default artifact root is not a creatable directory');
505
+ await shared.initialization;
506
+ await verifyArtifactRoot(shared);
507
+ return shared;
508
+ }
509
+ const root = await fs.realpath(ancestor);
510
+ const identity = await fs.stat(ancestor, { bigint: true });
511
+ assertIdentity(identity);
512
+ if (!identity.isDirectory()) throw new Error('Artifact root must be a directory');
513
+ const shared = pending || missingStorageRoots.get(key)?.deref();
514
+ if (shared && shared.root === path.join(root, ...components)) {
515
+ if (!shared.creationSettled && shared.creationAnchor.configured === ancestor && shared.creationAnchor.root === root
516
+ && sameIdentity(shared.creationAnchor.identity, identity)) {
517
+ return shared;
518
+ }
519
+ if (shared.initialization) {
520
+ // A partially created ancestor can shorten this pin's missing path.
521
+ // Wait for the exclusive final mkdir proof and retain this pin's own
522
+ // observed ancestor identity before joining the established boundary.
523
+ await shared.initialization;
524
+ const current = await fs.stat(ancestor, { bigint: true });
525
+ assertIdentity(current);
526
+ if (await fs.realpath(ancestor) !== root || !sameIdentity(current, identity)) {
527
+ throw new Error('Artifact root changed since it was pinned');
528
+ }
529
+ await verifyArtifactRoot(shared);
530
+ return shared;
531
+ }
532
+ }
533
+ const store = {
534
+ configured, root: path.join(root, ...components), identity: null, ...options,
535
+ creationAnchor: { configured: ancestor, root, identity }, components,
536
+ creationKey: key,
537
+ };
538
+ const reference = new WeakRef(store);
539
+ store.creationReference = reference;
540
+ missingStorageRoots.set(key, reference);
541
+ forgottenStorageRoots.register(store, { key, reference }, reference);
542
+ return store;
543
+ }
544
+
545
+ /** Create through the pinned ancestor; never adopt an installed final root. */
546
+ async function createMissingRoot(store) {
547
+ const anchor = store.creationAnchor;
548
+ const handles = [];
549
+ let parentPath = anchor.root;
550
+ async function verifyAnchor() {
551
+ const current = await fs.stat(anchor.configured, { bigint: true });
552
+ assertIdentity(current);
553
+ if (await fs.realpath(anchor.configured) !== anchor.root || !current.isDirectory()
554
+ || !sameIdentity(current, anchor.identity)) {
555
+ throw new Error('Artifact root changed since it was pinned');
556
+ }
557
+ }
558
+ async function verifyDirectory(handle, expected, entry) {
559
+ const opened = handle ? await handle.stat({ bigint: true }) : await fs.lstat(entry, { bigint: true });
560
+ const current = await fs.lstat(entry, { bigint: true });
561
+ assertIdentity(opened); assertIdentity(current);
562
+ if (!opened.isDirectory() || current.isSymbolicLink()
563
+ || !sameIdentity(opened, current)
564
+ || await fs.realpath(handle ? `/proc/self/fd/${handle.fd}` : entry) !== expected) {
565
+ throw new Error('Artifact root changed during creation');
566
+ }
567
+ return opened;
568
+ }
569
+ try {
570
+ await verifyAnchor();
571
+ let parent;
572
+ if (rootedArtifactAccess()) {
573
+ parent = await fs.open(anchor.root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
574
+ handles.push(parent);
575
+ const opened = await verifyDirectory(parent, anchor.root, anchor.root);
576
+ if (!sameIdentity(opened, anchor.identity)) throw new Error('Artifact root changed since it was pinned');
577
+ }
578
+ for (const [index, component] of store.components.entries()) {
579
+ await verifyAnchor();
580
+ if (parent) await verifyDirectory(parent, parentPath, parentPath);
581
+ const entry = path.join(parent ? `/proc/self/fd/${parent.fd}` : parentPath, component);
582
+ const expected = path.join(parentPath, component);
583
+ try { await fs.mkdir(entry); }
584
+ catch (error) {
585
+ // Other subsystem adapters may have created shared parents. The final
586
+ // missing root is exclusive; pre-existing parents still need checks.
587
+ if (error.code !== 'EEXIST' || index === store.components.length - 1) throw error;
588
+ }
589
+ const created = await fs.lstat(entry, { bigint: true });
590
+ assertIdentity(created);
591
+ if (created.isSymbolicLink() || !created.isDirectory()) throw new Error('Artifact root changed during creation');
592
+ const child = rootedArtifactAccess()
593
+ ? await fs.open(entry, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW) : undefined;
594
+ if (child) handles.push(child);
595
+ const opened = await verifyDirectory(child, expected, entry);
596
+ if (!sameIdentity(opened, created)) throw new Error('Artifact root changed during creation');
597
+ parent = child; parentPath = expected;
598
+ if (index === store.components.length - 1) store.identity = opened;
599
+ }
600
+ await verifyArtifactRoot(store);
601
+ } finally {
602
+ const closed = await Promise.allSettled(handles.map(handle => handle.close()));
603
+ const failure = closed.find(result => result.status === 'rejected');
604
+ if (store.identity && failure) throw failure.reason;
605
+ }
606
+ }
607
+
608
+ /** Child metadata is read through the same verified directory used by listing. */
609
+ async function directoryEntries(directory) {
610
+ const results = [];
611
+ for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
612
+ // Never emit or descend through a symlink, even a contained alias.
613
+ if (!entry.isDirectory() && !entry.isFile()) continue;
614
+ const info = await fs.lstat(path.join(directory, entry.name));
615
+ assertIdentity(info);
616
+ if (info.isSymbolicLink()) throw new Error('Artifact changed during listing');
617
+ if (entry.isDirectory() !== info.isDirectory() || entry.isFile() !== info.isFile()) throw new Error('Artifact changed during listing');
618
+ if (info.isFile() && info.nlink > 1) continue;
619
+ results.push({ name: entry.name, directory: info.isDirectory(), size: info.size, modifiedAt: info.mtime });
620
+ }
621
+ return results;
622
+ }
623
+
624
+ /** Missing storage roots retain null/no-op/empty semantics without being adopted. */
625
+ async function absentRoot(store) {
626
+ if (store.error) throw store.error;
627
+ if (store.identity) { await verifyArtifactRoot(store); return false; }
628
+ return true;
629
+ }
630
+
631
+ async function withAccess(store, relative, options, use, missing) {
632
+ validateContainedPath(relative);
633
+ if (!options.write && !options.append && await absentRoot(store)) return missing;
634
+ let access;
635
+ try {
636
+ access = await openArtifact(store, relative, options);
637
+ return await use(access);
638
+ } catch (error) {
639
+ if (!options.write && !options.append && error.code === 'ENOENT' && !error.containmentRefusal) {
640
+ // Root removal/replacement is never converted to an ordinary missing leaf.
641
+ await verifyArtifactRoot(store);
642
+ return missing;
643
+ }
644
+ throw error;
645
+ } finally { if (access) await access.close(); }
646
+ }
647
+
648
+ export async function readContainedFile(store, relative) {
649
+ return withAccess(store, relative, {}, access => access.handle.readFile('utf8'), null);
650
+ }
651
+ export async function writeContainedFile(store, relative, content) {
652
+ return withAccess(store, relative, { write: true }, async access => {
653
+ await access.handle.writeFile(content, 'utf8');
654
+ await access.commit();
655
+ });
656
+ }
657
+ export async function appendContainedFile(store, relative, content) {
658
+ return withAccess(store, relative, { append: true }, access => access.appendContent(content));
659
+ }
660
+ export async function deleteContainedFile(store, relative) {
661
+ return withAccess(store, relative, { remove: true }, access => access.unlink());
662
+ }
663
+ export async function listContainedFiles(store, prefix = '') {
664
+ if (typeof prefix !== 'string') throw new Error('Artifact list prefix must be a string');
665
+ if (prefix) validateContainedPath(prefix);
666
+ if (await absentRoot(store)) return [];
667
+ const results = [];
668
+ async function walk(relative = '') {
669
+ const access = await openArtifact(store, `${relative ? `${relative}/` : ''}.aiwg-list`, { listing: true });
670
+ try {
671
+ for (const entry of await access.entries()) {
672
+ const rel = relative ? `${relative}/${entry.name}` : entry.name;
673
+ if (entry.directory) {
674
+ if (!prefix || rel.startsWith(prefix) || prefix.startsWith(`${rel}/`)) await walk(rel);
675
+ } else if (!prefix || rel.startsWith(prefix)) results.push({ path: rel, size: entry.size, modifiedAt: entry.modifiedAt });
676
+ }
677
+ } finally { await access.close(); }
678
+ }
679
+ await walk();
680
+ return results;
681
+ }
682
+
683
+ export { pinArtifactRoot as pinContainedRoot, openArtifact as openContainedFile };