codex-orchestrator 2.0.2 → 2.0.3

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 (211) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +12 -9
  3. package/dist/src/index.d.ts +10 -0
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +5 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/v2/acceptance-proof.d.ts +3 -0
  8. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  9. package/dist/src/v2/acceptance-proof.js +2 -8
  10. package/dist/src/v2/acceptance-proof.js.map +1 -1
  11. package/dist/src/v2/adapters/gh-issue-adapter.d.ts +5 -3
  12. package/dist/src/v2/adapters/gh-issue-adapter.d.ts.map +1 -1
  13. package/dist/src/v2/adapters/gh-issue-adapter.js +63 -7
  14. package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
  15. package/dist/src/v2/adapters/issues.d.ts +16 -2
  16. package/dist/src/v2/adapters/issues.d.ts.map +1 -1
  17. package/dist/src/v2/adapters/issues.js +15 -5
  18. package/dist/src/v2/adapters/issues.js.map +1 -1
  19. package/dist/src/v2/adapters/mission-coordinator-lock.d.ts +1 -0
  20. package/dist/src/v2/adapters/mission-coordinator-lock.d.ts.map +1 -1
  21. package/dist/src/v2/adapters/mission-coordinator-lock.js +5 -1
  22. package/dist/src/v2/adapters/mission-coordinator-lock.js.map +1 -1
  23. package/dist/src/v2/candidate-cli.d.ts +4 -0
  24. package/dist/src/v2/candidate-cli.d.ts.map +1 -1
  25. package/dist/src/v2/candidate-cli.js +26 -11
  26. package/dist/src/v2/candidate-cli.js.map +1 -1
  27. package/dist/src/v2/cli-contract.d.ts +1 -1
  28. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  29. package/dist/src/v2/cli-contract.js +10 -0
  30. package/dist/src/v2/cli-contract.js.map +1 -1
  31. package/dist/src/v2/code-review-report.d.ts +66 -0
  32. package/dist/src/v2/code-review-report.d.ts.map +1 -0
  33. package/dist/src/v2/code-review-report.js +259 -0
  34. package/dist/src/v2/code-review-report.js.map +1 -0
  35. package/dist/src/v2/codex-process.d.ts +8 -1
  36. package/dist/src/v2/codex-process.d.ts.map +1 -1
  37. package/dist/src/v2/codex-process.js +11 -0
  38. package/dist/src/v2/codex-process.js.map +1 -1
  39. package/dist/src/v2/config.d.ts +2 -1
  40. package/dist/src/v2/config.d.ts.map +1 -1
  41. package/dist/src/v2/config.js +8 -3
  42. package/dist/src/v2/config.js.map +1 -1
  43. package/dist/src/v2/contained-report-operation.d.ts +100 -0
  44. package/dist/src/v2/contained-report-operation.d.ts.map +1 -0
  45. package/dist/src/v2/contained-report-operation.js +200 -0
  46. package/dist/src/v2/contained-report-operation.js.map +1 -0
  47. package/dist/src/v2/containment.d.ts +6 -0
  48. package/dist/src/v2/containment.d.ts.map +1 -1
  49. package/dist/src/v2/containment.js +40 -1
  50. package/dist/src/v2/containment.js.map +1 -1
  51. package/dist/src/v2/direct-delivery.d.ts +101 -0
  52. package/dist/src/v2/direct-delivery.d.ts.map +1 -0
  53. package/dist/src/v2/direct-delivery.js +547 -0
  54. package/dist/src/v2/direct-delivery.js.map +1 -0
  55. package/dist/src/v2/immutable-workflow-publisher.d.ts +40 -0
  56. package/dist/src/v2/immutable-workflow-publisher.d.ts.map +1 -0
  57. package/dist/src/v2/immutable-workflow-publisher.js +218 -0
  58. package/dist/src/v2/immutable-workflow-publisher.js.map +1 -0
  59. package/dist/src/v2/implementation-reviewer.d.ts +81 -0
  60. package/dist/src/v2/implementation-reviewer.d.ts.map +1 -0
  61. package/dist/src/v2/implementation-reviewer.js +157 -0
  62. package/dist/src/v2/implementation-reviewer.js.map +1 -0
  63. package/dist/src/v2/owner-control-lock.d.ts +41 -0
  64. package/dist/src/v2/owner-control-lock.d.ts.map +1 -0
  65. package/dist/src/v2/owner-control-lock.js +174 -0
  66. package/dist/src/v2/owner-control-lock.js.map +1 -0
  67. package/dist/src/v2/route-continuations.d.ts +32 -0
  68. package/dist/src/v2/route-continuations.d.ts.map +1 -0
  69. package/dist/src/v2/route-continuations.js +2 -0
  70. package/dist/src/v2/route-continuations.js.map +1 -0
  71. package/dist/src/v2/route-coordinator.d.ts +77 -0
  72. package/dist/src/v2/route-coordinator.d.ts.map +1 -0
  73. package/dist/src/v2/route-coordinator.js +370 -0
  74. package/dist/src/v2/route-coordinator.js.map +1 -0
  75. package/dist/src/v2/route-decision.d.ts +129 -0
  76. package/dist/src/v2/route-decision.d.ts.map +1 -0
  77. package/dist/src/v2/route-decision.js +400 -0
  78. package/dist/src/v2/route-decision.js.map +1 -0
  79. package/dist/src/v2/run-issue.d.ts +63 -2
  80. package/dist/src/v2/run-issue.d.ts.map +1 -1
  81. package/dist/src/v2/run-issue.js +906 -91
  82. package/dist/src/v2/run-issue.js.map +1 -1
  83. package/dist/src/v2/run-store.d.ts +25 -1
  84. package/dist/src/v2/run-store.d.ts.map +1 -1
  85. package/dist/src/v2/run-store.js +143 -3
  86. package/dist/src/v2/run-store.js.map +1 -1
  87. package/dist/src/v2/runtime-assets.d.ts +15 -13
  88. package/dist/src/v2/runtime-assets.d.ts.map +1 -1
  89. package/dist/src/v2/runtime-assets.js +263 -416
  90. package/dist/src/v2/runtime-assets.js.map +1 -1
  91. package/dist/src/v2/runtime.d.ts +14 -6
  92. package/dist/src/v2/runtime.d.ts.map +1 -1
  93. package/dist/src/v2/runtime.js +478 -56
  94. package/dist/src/v2/runtime.js.map +1 -1
  95. package/dist/src/v2/setup-cli.d.ts.map +1 -1
  96. package/dist/src/v2/setup-cli.js +1 -0
  97. package/dist/src/v2/setup-cli.js.map +1 -1
  98. package/dist/src/v2/setup-runtime.d.ts.map +1 -1
  99. package/dist/src/v2/setup-runtime.js +20 -72
  100. package/dist/src/v2/setup-runtime.js.map +1 -1
  101. package/dist/src/v2/setup.d.ts +4 -1
  102. package/dist/src/v2/setup.d.ts.map +1 -1
  103. package/dist/src/v2/setup.js +104 -1
  104. package/dist/src/v2/setup.js.map +1 -1
  105. package/dist/src/v2/spec-coordinator.d.ts +85 -0
  106. package/dist/src/v2/spec-coordinator.d.ts.map +1 -0
  107. package/dist/src/v2/spec-coordinator.js +88 -0
  108. package/dist/src/v2/spec-coordinator.js.map +1 -0
  109. package/dist/src/v2/spec-delivery.d.ts +143 -0
  110. package/dist/src/v2/spec-delivery.d.ts.map +1 -0
  111. package/dist/src/v2/spec-delivery.js +401 -0
  112. package/dist/src/v2/spec-delivery.js.map +1 -0
  113. package/dist/src/v2/triage-route.d.ts +68 -0
  114. package/dist/src/v2/triage-route.d.ts.map +1 -0
  115. package/dist/src/v2/triage-route.js +223 -0
  116. package/dist/src/v2/triage-route.js.map +1 -0
  117. package/dist/src/v2/waiting-human-coordinator.d.ts +49 -0
  118. package/dist/src/v2/waiting-human-coordinator.d.ts.map +1 -0
  119. package/dist/src/v2/waiting-human-coordinator.js +509 -0
  120. package/dist/src/v2/waiting-human-coordinator.js.map +1 -0
  121. package/dist/src/v2/waiting-human.d.ts +143 -0
  122. package/dist/src/v2/waiting-human.d.ts.map +1 -0
  123. package/dist/src/v2/waiting-human.js +408 -0
  124. package/dist/src/v2/waiting-human.js.map +1 -0
  125. package/dist/src/v2/workflow-assets.d.ts +90 -0
  126. package/dist/src/v2/workflow-assets.d.ts.map +1 -0
  127. package/dist/src/v2/workflow-assets.js +554 -0
  128. package/dist/src/v2/workflow-assets.js.map +1 -0
  129. package/docs/deep-dive.md +15 -8
  130. package/internal-workflow/docs/agents/artifact-review-loop.md +267 -0
  131. package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
  132. package/internal-workflow/docs/agents/coding-skill-routing.md +203 -0
  133. package/internal-workflow/docs/agents/confidence-rubric.md +65 -0
  134. package/internal-workflow/docs/agents/contract-test-ledger.md +60 -0
  135. package/internal-workflow/docs/agents/implementation-review-loop.md +302 -0
  136. package/internal-workflow/docs/agents/review-gates.md +49 -0
  137. package/internal-workflow/docs/agents/review-protocol.md +170 -0
  138. package/internal-workflow/docs/agents/tool-usage.md +88 -0
  139. package/internal-workflow/manifest.json +1 -0
  140. package/internal-workflow/operations/acceptance-proof/SKILL.md +3 -0
  141. package/internal-workflow/operations/ambiguity-review/SKILL.md +3 -0
  142. package/internal-workflow/operations/cleanup-review/SKILL.md +3 -0
  143. package/internal-workflow/operations/code-review/SKILL.md +3 -0
  144. package/internal-workflow/operations/implementation/SKILL.md +3 -0
  145. package/internal-workflow/operations/spec-author/SKILL.md +3 -0
  146. package/internal-workflow/operations/spec-implementation/SKILL.md +3 -0
  147. package/internal-workflow/operations/spec-review/SKILL.md +3 -0
  148. package/internal-workflow/operations/triage/SKILL.md +3 -0
  149. package/internal-workflow/profiles/analyst_deep.toml +9 -0
  150. package/internal-workflow/profiles/implementer_deep.toml +9 -0
  151. package/internal-workflow/profiles/implementer_standard.toml +9 -0
  152. package/internal-workflow/profiles/proof_agent.toml +8 -0
  153. package/internal-workflow/profiles/researcher_standard.toml +9 -0
  154. package/internal-workflow/profiles/reviewer_deep.toml +9 -0
  155. package/internal-workflow/profiles/reviewer_fast.toml +9 -0
  156. package/internal-workflow/profiles/reviewer_standard.toml +9 -0
  157. package/internal-workflow/schemas/ambiguity-review-v1.json +1 -0
  158. package/internal-workflow/schemas/code-review-v1.json +1 -0
  159. package/internal-workflow/schemas/implementation-report-v1.json +1 -0
  160. package/internal-workflow/schemas/proof-report-v1.json +1 -0
  161. package/internal-workflow/schemas/spec-author-v1.json +1 -0
  162. package/internal-workflow/schemas/spec-review-v1.json +30 -0
  163. package/internal-workflow/schemas/triage-route-v1.json +1 -0
  164. package/internal-workflow/skills/acceptance-proof/agents/openai.yaml +6 -0
  165. package/internal-workflow/skills/agent-auto/agents/openai.yaml +6 -0
  166. package/internal-workflow/skills/cleanup-review/SKILL.md +84 -0
  167. package/internal-workflow/skills/cleanup-review/agents/openai.yaml +6 -0
  168. package/internal-workflow/skills/code-review/SKILL.md +257 -0
  169. package/internal-workflow/skills/code-review/agents/openai.yaml +4 -0
  170. package/internal-workflow/skills/code-review/references/bug-classes.md +56 -0
  171. package/internal-workflow/skills/code-review/references/framework-lenses.md +34 -0
  172. package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -0
  173. package/internal-workflow/skills/codebase-design/DEEPENING.md +35 -0
  174. package/internal-workflow/skills/codebase-design/DESIGN-IT-TWICE.md +50 -0
  175. package/internal-workflow/skills/codebase-design/SKILL.md +82 -0
  176. package/internal-workflow/skills/codebase-design/agents/openai.yaml +6 -0
  177. package/internal-workflow/skills/diagnosing-bugs/SKILL.md +138 -0
  178. package/internal-workflow/skills/diagnosing-bugs/agents/openai.yaml +6 -0
  179. package/internal-workflow/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  180. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +93 -0
  181. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +6 -0
  182. package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +31 -0
  183. package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +146 -0
  184. package/internal-workflow/skills/implementation-spec-review/SKILL.md +211 -0
  185. package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
  186. package/internal-workflow/skills/research/SKILL.md +107 -0
  187. package/internal-workflow/skills/research/agents/openai.yaml +6 -0
  188. package/internal-workflow/skills/small-task-implementer/SKILL.md +97 -0
  189. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
  190. package/internal-workflow/skills/spec-implementer/SKILL.md +197 -0
  191. package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
  192. package/internal-workflow/skills/tdd/SKILL.md +59 -0
  193. package/internal-workflow/skills/tdd/agents/openai.yaml +6 -0
  194. package/internal-workflow/skills/tdd/interface-design.md +31 -0
  195. package/internal-workflow/skills/tdd/mocking.md +59 -0
  196. package/internal-workflow/skills/tdd/refactoring.md +10 -0
  197. package/internal-workflow/skills/tdd/tests.md +77 -0
  198. package/internal-workflow/skills/triage/AGENT-BRIEF.md +192 -0
  199. package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +101 -0
  200. package/internal-workflow/skills/triage/SKILL.md +134 -0
  201. package/internal-workflow/skills/triage/agents/openai.yaml +6 -0
  202. package/internal-workflow/skills/ui-evidence-proof/SKILL.md +123 -0
  203. package/internal-workflow/skills/ui-evidence-proof/agents/openai.yaml +6 -0
  204. package/package.json +6 -3
  205. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/SKILL.md +0 -0
  206. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/android.md +0 -0
  207. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/browser.md +0 -0
  208. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/ios.md +0 -0
  209. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/android-lease.mjs +0 -0
  210. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/ios-lease.mjs +0 -0
  211. /package/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +0 -0
@@ -1,412 +1,243 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { lstat, mkdir, open, readFile, readdir, rename, rm, } from 'node:fs/promises';
3
- import { dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
4
- import { canonicalJson, sha256 } from './containment.js';
5
- import { implementationReportOutputSchema } from './implementation-report.js';
6
- import { proofReportOutputSchema } from './proof-report.js';
7
- const SNAPSHOT_ROOT_MODE = 0o700;
8
- const SNAPSHOT_FILE_MODE = 0o400;
9
- const LOCK_WAIT_MS = 5_000;
10
- const LOCK_POLL_MS = 25;
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { O_NOFOLLOW, O_RDONLY } from 'node:constants';
3
+ import { chmod, lstat, mkdir, open, readdir, realpath, stat, writeFile } from 'node:fs/promises';
4
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
5
+ import { resolveWorkflowOperation, sealedWorkflowContentSha256, sealedWorkflowMode, verifyWorkflowGeneration, } from './workflow-assets.js';
6
+ import { publishImmutableWorkflow } from './immutable-workflow-publisher.js';
7
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
11
8
  export async function publishRuntimeAssetSnapshot(input) {
12
- const expectedUid = runnerUid();
13
- const packageRoot = resolve(input.packageRoot);
14
- const runtimeRoot = resolve(input.runtimeRoot);
15
- const snapshotRelativePath = validateRelativeSnapshotPath(input.snapshotRelativePath);
16
- const snapshotRoot = resolve(runtimeRoot, ...snapshotRelativePath.split('/'));
17
- assertContained(runtimeRoot, snapshotRoot, 'snapshot root');
18
- const attemptRoot = dirname(snapshotRoot);
19
- const source = await resolveSourceAssets(packageRoot, input.skill);
20
- await input.onStep?.('after-source-resolve');
21
- await assertSourceUnchanged(source);
22
- await ensureManagedDirectoryPath(runtimeRoot, attemptRoot, expectedUid);
23
- const attemptStat = await lstat(attemptRoot);
24
- assertDirectory(attemptStat, attemptRoot);
25
- assertOwner(attemptStat.uid, expectedUid, attemptRoot);
26
- assertMode(attemptStat.mode, SNAPSHOT_ROOT_MODE, attemptRoot);
27
- const lockPath = join(attemptRoot, '.snapshot.publish.lock');
28
- const lock = await acquirePublishLock(lockPath);
29
- let tempRoot;
30
- let published = false;
31
- try {
32
- await assertSourceUnchanged(source);
33
- if (await pathExists(snapshotRoot)) {
34
- const existing = expectedSnapshot({ source, runtimeRoot, snapshotRoot, expectedUid, reused: true });
35
- await verifyRuntimeAssetSnapshot(existing);
36
- await assertSourceUnchanged(source);
37
- return existing;
38
- }
39
- tempRoot = join(attemptRoot, `.snapshot.tmp-${process.pid}-${randomUUID()}`);
40
- await mkdir(tempRoot, { mode: SNAPSHOT_ROOT_MODE });
41
- const expectedFiles = sourceSnapshotFiles(source, expectedUid);
42
- for (const [index, file] of expectedFiles.entries()) {
43
- const bytes = sourceFileBytes(source, file.relativePath);
44
- if (file.relativePath.includes('/')) {
45
- await mkdir(dirname(join(tempRoot, file.relativePath)), { recursive: true, mode: SNAPSHOT_ROOT_MODE });
46
- }
47
- const handle = await open(join(tempRoot, file.relativePath), 'wx', SNAPSHOT_FILE_MODE);
48
- try {
49
- await handle.writeFile(bytes);
50
- await handle.sync();
9
+ await verifyWorkflowGeneration(input.workflowGeneration);
10
+ const operation = await resolveWorkflowOperation(input.workflowGeneration, input.operation);
11
+ const requestedRuntimeRoot = resolve(input.runtimeRoot);
12
+ const requested = validateRelativePath(input.snapshotRelativePath);
13
+ const requestedLogicalRoot = resolve(requestedRuntimeRoot, ...requested.split('/'));
14
+ assertContained(requestedRuntimeRoot, requestedLogicalRoot);
15
+ await ensureManagedPath(requestedRuntimeRoot, dirname(requestedLogicalRoot));
16
+ const runtimeRoot = await realpath(requestedRuntimeRoot);
17
+ const logicalRoot = resolve(runtimeRoot, ...requested.split('/'));
18
+ const parent = dirname(logicalRoot);
19
+ const stem = basename(logicalRoot);
20
+ return publishImmutableWorkflow({
21
+ parent,
22
+ identity: stem,
23
+ bootId: input.bootId,
24
+ createOwner: (parentToken, processStartIdentity) => newOwner(input.bootId, parentToken, processStartIdentity),
25
+ parseOwner,
26
+ createReady: (owner, contentSha256) => ({
27
+ version: 1,
28
+ status: 'ready',
29
+ token: owner.token,
30
+ generationHash: input.workflowGeneration.generationHash,
31
+ operation: input.operation,
32
+ contentSha256,
33
+ }),
34
+ parseReady,
35
+ serializeRecord: (value) => Buffer.from(`${canonicalJson(value)}\n`),
36
+ readControl: readRegular,
37
+ writeContent: async (contentRoot, onStep) => {
38
+ for (const [index, file] of operation.files.entries()) {
39
+ const source = join(input.workflowGeneration.generationRoot, ...file.path.split('/'));
40
+ const target = join(contentRoot, ...file.path.split('/'));
41
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
42
+ await writeSynced(target, await readRegular(source), sealedWorkflowMode(file.mode));
43
+ if (index === 0)
44
+ await onStep('after-first-content-file');
51
45
  }
52
- finally {
53
- await handle.close();
46
+ for (const directory of (await listDirectories(contentRoot)).sort((left, right) => right.length - left.length)) {
47
+ await chmod(directory, 0o555);
48
+ await syncDirectory(directory);
54
49
  }
55
- await syncDirectory(dirname(join(tempRoot, file.relativePath)));
56
- if (index === 0)
57
- await input.onStep?.('after-first-file-sync');
58
- }
59
- const tempEvidence = expectedSnapshot({
60
- source,
61
- runtimeRoot,
62
- snapshotRoot: tempRoot,
63
- expectedUid,
64
- reused: false,
65
- });
66
- await verifyRuntimeAssetSnapshot(tempEvidence);
67
- await input.onStep?.('before-temp-directory-sync');
68
- await syncDirectory(tempRoot);
69
- await input.onStep?.('after-temp-directory-sync');
70
- await assertSourceUnchanged(source);
71
- await input.onStep?.('before-rename');
72
- if (await pathExists(snapshotRoot)) {
73
- const raced = expectedSnapshot({ source, runtimeRoot, snapshotRoot, expectedUid, reused: true });
74
- await verifyRuntimeAssetSnapshot(raced);
75
- await assertSourceUnchanged(source);
76
- return raced;
77
- }
78
- await rename(tempRoot, snapshotRoot);
79
- published = true;
80
- await input.onStep?.('after-rename');
81
- await syncDirectory(attemptRoot);
82
- await input.onStep?.('after-parent-sync');
83
- const snapshot = expectedSnapshot({ source, runtimeRoot, snapshotRoot, expectedUid, reused: false });
84
- await verifyRuntimeAssetSnapshot(snapshot);
85
- await assertSourceUnchanged(source);
86
- return snapshot;
87
- }
88
- finally {
89
- if (tempRoot && !published)
90
- await rm(tempRoot, { recursive: true, force: true });
91
- await releasePublishLock(lockPath, lock.token);
92
- }
50
+ return contentDigest(await evidence(contentRoot, operation.files));
51
+ },
52
+ resultFromReady: ({ ready, contentRoot, reused }) => snapshotFromReady(input, operation, runtimeRoot, ready, contentRoot, reused),
53
+ activePublisherError: 'runtime asset publisher is still active',
54
+ recoveryChainError: 'runtime asset recovery chain is invalid',
55
+ onStep: input.onStep,
56
+ });
93
57
  }
94
58
  export async function verifyRuntimeAssetSnapshot(snapshot) {
95
- const expectedUid = runnerUid();
96
- if (snapshot.ownerUid !== expectedUid)
97
- throw new Error('snapshot evidence owner does not match the runner');
98
- if (snapshot.rootMode !== SNAPSHOT_ROOT_MODE)
99
- throw new Error('snapshot evidence root mode is invalid');
100
- const runtimeRoot = resolve(snapshot.runtimeRoot);
101
- const snapshotRoot = resolve(snapshot.snapshotRoot);
102
- assertContained(runtimeRoot, snapshotRoot, 'snapshot evidence root');
103
- await assertExistingManagedPath(runtimeRoot, snapshotRoot, expectedUid);
104
- const rootStat = await lstat(snapshotRoot);
105
- assertDirectory(rootStat, snapshotRoot);
106
- assertOwner(rootStat.uid, expectedUid, snapshotRoot);
107
- assertMode(rootStat.mode, SNAPSHOT_ROOT_MODE, snapshotRoot);
108
- const expectedNames = snapshot.skill === 'acceptance-proof'
109
- ? [
110
- 'SKILL.md', 'output-schema.json', 'references/android.md', 'references/browser.md', 'references/ios.md',
111
- 'tools/android-lease.mjs', 'tools/ios-lease.mjs',
112
- ]
113
- : ['SKILL.md', 'output-schema.json'];
114
- const actualNames = await snapshotFilePaths(snapshotRoot, expectedUid);
115
- if (actualNames.length !== expectedNames.length || actualNames.some((name, index) => name !== expectedNames[index])) {
116
- throw new Error('snapshot has missing or extra entries');
117
- }
118
- if (snapshot.files.length !== expectedNames.length)
119
- throw new Error('snapshot evidence file list is invalid');
120
- const evidenceNames = snapshot.files.map((file) => file.relativePath);
121
- if (evidenceNames.some((name, index) => name !== expectedNames[index]))
122
- throw new Error('snapshot evidence file order is invalid');
123
- for (const file of snapshot.files) {
124
- if (file.ownerUid !== expectedUid)
125
- throw new Error(`snapshot evidence owner drift: ${file.relativePath}`);
126
- if (file.mode !== SNAPSHOT_FILE_MODE)
127
- throw new Error(`snapshot evidence mode drift: ${file.relativePath}`);
128
- const path = join(snapshotRoot, file.relativePath);
129
- const info = await lstat(path);
130
- if (info.isSymbolicLink())
131
- throw new Error(`snapshot contains a symbolic link: ${file.relativePath}`);
132
- if (!info.isFile())
133
- throw new Error(`snapshot entry is not a regular file: ${file.relativePath}`);
134
- assertOwner(info.uid, expectedUid, path);
135
- assertMode(info.mode, SNAPSHOT_FILE_MODE, path);
136
- const bytes = await readFile(path);
137
- if (bytes.length !== file.size)
138
- throw new Error(`snapshot size mismatch: ${file.relativePath}`);
139
- if (sha256(bytes) !== file.sha256)
140
- throw new Error(`snapshot hash mismatch: ${file.relativePath}`);
141
- }
142
- const skillPath = join(snapshotRoot, 'SKILL.md');
143
- const schemaPath = join(snapshotRoot, 'output-schema.json');
144
- if (resolve(snapshot.skillPath) !== skillPath || resolve(snapshot.schemaPath) !== schemaPath) {
145
- throw new Error('snapshot evidence paths are invalid');
59
+ if (snapshot.ownerUid !== runnerUid())
60
+ throw new Error('runtime asset owner drift');
61
+ const root = resolve(snapshot.snapshotRoot);
62
+ assertContained(resolve(snapshot.runtimeRoot), root);
63
+ if (await realpath(root) !== root)
64
+ throw new Error('runtime asset snapshot contains a symbolic link');
65
+ for (const directory of await listDirectories(root)) {
66
+ const info = await lstat(directory);
67
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== runnerUid() || (info.mode & 0o777) !== 0o555) {
68
+ throw new Error('runtime asset directory mode or owner drift');
69
+ }
146
70
  }
147
- if (snapshot.generatedSchemaSha256 !== snapshot.files.find((file) => file.relativePath === 'output-schema.json')?.sha256) {
148
- throw new Error('generated schema hash does not match snapshot evidence');
71
+ const actual = await listFiles(root);
72
+ const expected = snapshot.files.map((file) => file.path);
73
+ if (!same(actual, expected))
74
+ throw new Error('runtime asset file closure drift');
75
+ const current = await evidence(root, snapshot.files);
76
+ if (canonicalJson(current) !== canonicalJson(snapshot.files))
77
+ throw new Error('runtime asset evidence drift');
78
+ if (contentDigest(current) !== snapshot.contentSha256)
79
+ throw new Error('runtime asset content digest drift');
80
+ for (const [path, expectedPath] of [
81
+ [snapshot.operationPath, `operations/${snapshot.operation}/SKILL.md`],
82
+ [snapshot.schemaPath, snapshot.files.find((file) => resolve(root, ...file.path.split('/')) === resolve(snapshot.schemaPath))?.path],
83
+ [snapshot.profilePath, snapshot.files.find((file) => resolve(root, ...file.path.split('/')) === resolve(snapshot.profilePath))?.path],
84
+ ]) {
85
+ if (typeof path !== 'string' || typeof expectedPath !== 'string'
86
+ || resolve(path) !== resolve(root, ...expectedPath.split('/')))
87
+ throw new Error('runtime asset authority path drift');
149
88
  }
150
89
  }
151
- async function resolveSourceAssets(packageRoot, skill) {
152
- const packageJsonPath = join(packageRoot, 'package.json');
153
- const skillSourcePath = join(packageRoot, 'internal-skills', skill, 'SKILL.md');
154
- const supplementalPaths = skill === 'acceptance-proof'
155
- ? [
156
- { relativePath: 'references/android.md', sourcePath: join(packageRoot, 'internal-skills', skill, 'references', 'android.md') },
157
- { relativePath: 'references/browser.md', sourcePath: join(packageRoot, 'internal-skills', skill, 'references', 'browser.md') },
158
- { relativePath: 'references/ios.md', sourcePath: join(packageRoot, 'internal-skills', skill, 'references', 'ios.md') },
159
- { relativePath: 'tools/android-lease.mjs', sourcePath: join(packageRoot, 'internal-skills', skill, 'tools', 'android-lease.mjs') },
160
- { relativePath: 'tools/ios-lease.mjs', sourcePath: join(packageRoot, 'internal-skills', skill, 'tools', 'ios-lease.mjs') },
161
- ]
162
- : [];
163
- await assertSourcePath(packageRoot, packageJsonPath, true);
164
- await assertSourcePath(packageRoot, skillSourcePath, true);
165
- await Promise.all(supplementalPaths.map((asset) => assertSourcePath(packageRoot, asset.sourcePath, true)));
166
- const [packageJsonBytes, skillBytes, supplementalBytes] = await Promise.all([
167
- readFile(packageJsonPath),
168
- readFile(skillSourcePath),
169
- Promise.all(supplementalPaths.map((asset) => readFile(asset.sourcePath))),
170
- ]);
171
- const packageJson = JSON.parse(packageJsonBytes.toString('utf8'));
172
- if (packageJson.name !== 'codex-orchestrator' || typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
173
- throw new Error('package.json does not identify a versioned codex-orchestrator package');
174
- }
175
- const schema = skill === 'agent-auto' ? implementationReportOutputSchema() : proofReportOutputSchema();
176
- return {
177
- packageRoot,
178
- packageJsonPath,
179
- packageJsonBytes,
180
- packageVersion: packageJson.version,
181
- skill,
182
- skillSourcePath,
183
- skillBytes,
184
- schemaBytes: Buffer.from(`${canonicalJson(schema)}\n`, 'utf8'),
185
- supplementalAssets: supplementalPaths.map((asset, index) => ({ ...asset, bytes: supplementalBytes[index] })),
186
- };
187
- }
188
- async function assertSourceUnchanged(source) {
189
- await assertSourcePath(source.packageRoot, source.packageJsonPath, true);
190
- await assertSourcePath(source.packageRoot, source.skillSourcePath, true);
191
- await Promise.all(source.supplementalAssets.map((asset) => assertSourcePath(source.packageRoot, asset.sourcePath, true)));
192
- const [packageJsonBytes, skillBytes, supplementalBytes] = await Promise.all([
193
- readFile(source.packageJsonPath),
194
- readFile(source.skillSourcePath),
195
- Promise.all(source.supplementalAssets.map((asset) => readFile(asset.sourcePath))),
196
- ]);
197
- if (!packageJsonBytes.equals(source.packageJsonBytes) || !skillBytes.equals(source.skillBytes)) {
198
- throw new Error('package assets changed during resolution');
90
+ async function snapshotFromReady(input, operation, runtimeRoot, ready, contentRoot, reused) {
91
+ if (ready.generationHash !== input.workflowGeneration.generationHash || ready.operation !== input.operation) {
92
+ throw new Error('runtime asset ready identity mismatch');
199
93
  }
200
- if (supplementalBytes.some((bytes, index) => !bytes.equals(source.supplementalAssets[index].bytes))) {
201
- throw new Error('package assets changed during resolution');
202
- }
203
- }
204
- function expectedSnapshot(input) {
205
- const files = sourceSnapshotFiles(input.source, input.expectedUid);
206
- return {
207
- packageVersion: input.source.packageVersion,
208
- skill: input.source.skill,
209
- runtimeRoot: input.runtimeRoot,
210
- snapshotRoot: input.snapshotRoot,
211
- skillPath: join(input.snapshotRoot, 'SKILL.md'),
212
- schemaPath: join(input.snapshotRoot, 'output-schema.json'),
213
- rootMode: SNAPSHOT_ROOT_MODE,
214
- ownerUid: input.expectedUid,
94
+ const snapshotRoot = await realpath(contentRoot);
95
+ const files = await evidence(snapshotRoot, operation.files);
96
+ const remap = (path) => join(snapshotRoot, ...relative(operation.workflowRoot, path).split(sep));
97
+ const snapshot = {
98
+ packageVersion: input.workflowGeneration.packageVersion,
99
+ generationHash: input.workflowGeneration.generationHash,
100
+ operation: input.operation,
101
+ runtimeRoot,
102
+ snapshotRoot,
103
+ operationPath: remap(operation.entryPath),
104
+ sourceSkillPath: operation.sourceSkillPath ? remap(operation.sourceSkillPath) : undefined,
105
+ schemaPath: remap(operation.schemaPath),
106
+ profilePath: remap(operation.profilePath),
107
+ policy: structuredClone(operation.policy),
108
+ ownerUid: runnerUid(),
215
109
  files,
216
- generatedSchemaSha256: files.find((file) => file.relativePath === 'output-schema.json').sha256,
217
- reused: input.reused,
110
+ contentSha256: ready.contentSha256,
111
+ reused,
218
112
  };
113
+ await verifyRuntimeAssetSnapshot(snapshot);
114
+ return snapshot;
219
115
  }
220
- function sourceSnapshotFiles(source, ownerUid) {
221
- const files = [
222
- {
223
- relativePath: 'SKILL.md',
224
- sha256: sha256(source.skillBytes),
225
- size: source.skillBytes.length,
226
- mode: SNAPSHOT_FILE_MODE,
227
- ownerUid,
228
- },
229
- {
230
- relativePath: 'output-schema.json',
231
- sha256: sha256(source.schemaBytes),
232
- size: source.schemaBytes.length,
233
- mode: SNAPSHOT_FILE_MODE,
234
- ownerUid,
235
- },
236
- ];
237
- for (const asset of source.supplementalAssets) {
238
- files.push({
239
- relativePath: asset.relativePath,
240
- sha256: sha256(asset.bytes),
241
- size: asset.bytes.length,
242
- mode: SNAPSHOT_FILE_MODE,
243
- ownerUid,
244
- });
116
+ async function evidence(root, records) {
117
+ const output = [];
118
+ for (const record of records) {
119
+ const path = join(root, ...record.path.split('/'));
120
+ const { bytes, mode, uid } = await readRegularEvidence(path);
121
+ const sealedMode = sealedWorkflowMode(record.mode);
122
+ if ((mode & 0o777) !== sealedMode || uid !== runnerUid()) {
123
+ throw new Error(`runtime asset mode or owner drift: ${record.path}`);
124
+ }
125
+ if (bytes.length !== record.size || sha(bytes) !== record.sha256)
126
+ throw new Error(`runtime asset hash drift: ${record.path}`);
127
+ output.push({ ...record, sealedMode, ownerUid: uid });
245
128
  }
246
- return files;
129
+ return output;
247
130
  }
248
- function sourceFileBytes(source, relativePath) {
249
- if (relativePath === 'SKILL.md')
250
- return source.skillBytes;
251
- if (relativePath === 'output-schema.json')
252
- return source.schemaBytes;
253
- const supplemental = source.supplementalAssets.find((asset) => asset.relativePath === relativePath);
254
- if (supplemental)
255
- return supplemental.bytes;
256
- throw new Error(`unknown runtime asset: ${relativePath}`);
131
+ function contentDigest(files) {
132
+ return sealedWorkflowContentSha256(files.map(({ path, sealedMode, size, sha256 }) => ({ path, sealedMode, size, sha256 })));
257
133
  }
258
- async function snapshotFilePaths(snapshotRoot, expectedUid) {
259
- const paths = [];
260
- const visit = async (directory, relativeDirectory) => {
261
- for (const entry of await readdir(directory, { withFileTypes: true })) {
262
- const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
263
- const path = join(directory, entry.name);
264
- const info = await lstat(path);
265
- if (entry.isSymbolicLink() || info.isSymbolicLink())
266
- throw new Error(`snapshot contains a symbolic link: ${relativePath}`);
267
- assertOwner(info.uid, expectedUid, path);
268
- if (entry.isDirectory()) {
269
- assertMode(info.mode, SNAPSHOT_ROOT_MODE, path);
270
- await visit(path, relativePath);
271
- }
272
- else if (entry.isFile()) {
273
- paths.push(relativePath);
274
- }
275
- else {
276
- throw new Error(`snapshot contains a special entry: ${relativePath}`);
277
- }
278
- }
279
- };
280
- await visit(snapshotRoot, '');
281
- return paths.sort((left, right) => expectedAssetOrder(left) - expectedAssetOrder(right) || left.localeCompare(right));
134
+ function newOwner(bootId, parentToken, processStartIdentity) {
135
+ if (!bootId)
136
+ throw new Error('runtime asset boot identity is invalid');
137
+ return { version: 1, status: 'building', bootId, pid: process.pid, token: randomUUID(), parentToken, processStartIdentity };
282
138
  }
283
- function expectedAssetOrder(path) {
284
- if (path === 'SKILL.md')
285
- return 0;
286
- if (path === 'output-schema.json')
287
- return 1;
288
- return 2;
139
+ function parseOwner(value) {
140
+ exact(value, ['version', 'status', 'bootId', 'pid', 'token', 'parentToken', 'processStartIdentity']);
141
+ if (value.version !== 1 || value.status !== 'building' || typeof value.bootId !== 'string' || !value.bootId
142
+ || !Number.isSafeInteger(value.pid) || Number(value.pid) <= 0 || typeof value.token !== 'string' || !UUID.test(value.token)
143
+ || !(value.parentToken === null || (typeof value.parentToken === 'string' && UUID.test(value.parentToken))))
144
+ throw new Error('runtime asset owner is invalid');
145
+ if (typeof value.processStartIdentity !== 'string' || !value.processStartIdentity)
146
+ throw new Error('runtime asset owner is invalid');
147
+ return value;
289
148
  }
290
- async function ensureManagedDirectoryPath(runtimeRoot, target, expectedUid) {
291
- await ensureManagedRuntimeRoot(runtimeRoot, expectedUid);
292
- assertContained(runtimeRoot, target, 'managed directory');
293
- const relativeTarget = relative(runtimeRoot, target);
294
- let current = runtimeRoot;
295
- for (const segment of relativeTarget.split(sep).filter(Boolean)) {
149
+ function parseReady(value) {
150
+ exact(value, ['version', 'status', 'token', 'generationHash', 'operation', 'contentSha256']);
151
+ if (value.version !== 1 || value.status !== 'ready' || typeof value.token !== 'string' || !UUID.test(value.token)
152
+ || typeof value.generationHash !== 'string' || !/^[a-f0-9]{64}$/u.test(value.generationHash)
153
+ || typeof value.operation !== 'string' || !value.operation || typeof value.contentSha256 !== 'string'
154
+ || !/^[a-f0-9]{64}$/u.test(value.contentSha256))
155
+ throw new Error('runtime asset ready record is invalid');
156
+ return value;
157
+ }
158
+ async function ensureManagedPath(root, target) {
159
+ await createManagedRoot(root);
160
+ const rootInfo = await lstat(root);
161
+ if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || rootInfo.uid !== runnerUid() || (rootInfo.mode & 0o777) !== 0o700) {
162
+ throw new Error('runtime asset root is unsafe');
163
+ }
164
+ let current = root;
165
+ for (const segment of relative(root, target).split(sep).filter(Boolean)) {
296
166
  current = join(current, segment);
167
+ await mkdir(current, { mode: 0o700 }).catch((error) => { if (!isCode(error, 'EEXIST'))
168
+ throw error; });
169
+ const info = await lstat(current);
170
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== runnerUid() || (info.mode & 0o777) !== 0o700)
171
+ throw new Error('runtime asset managed path is unsafe');
172
+ }
173
+ }
174
+ async function createManagedRoot(path) {
175
+ const chain = [];
176
+ let current = resolve(path);
177
+ while (dirname(current) !== current) {
178
+ chain.push(current);
179
+ current = dirname(current);
180
+ }
181
+ for (const segment of chain.reverse()) {
182
+ let info;
297
183
  try {
298
- const info = await lstat(current);
299
- if (info.isSymbolicLink())
300
- throw new Error(`managed runtime path contains a symbolic link: ${current}`);
301
- assertDirectory(info, current);
302
- assertOwner(info.uid, expectedUid, current);
184
+ info = await lstat(segment);
303
185
  }
304
186
  catch (error) {
305
- if (error.code !== 'ENOENT')
187
+ if (!isCode(error, 'ENOENT'))
306
188
  throw error;
307
189
  try {
308
- await mkdir(current, { mode: SNAPSHOT_ROOT_MODE });
190
+ await mkdir(segment, { mode: 0o700 });
309
191
  }
310
192
  catch (mkdirError) {
311
- if (mkdirError.code !== 'EEXIST')
193
+ if (!isCode(mkdirError, 'EEXIST'))
312
194
  throw mkdirError;
313
195
  }
314
- const created = await lstat(current);
315
- if (created.isSymbolicLink())
316
- throw new Error(`managed runtime path contains a symbolic link: ${current}`);
317
- assertDirectory(created, current);
318
- assertOwner(created.uid, expectedUid, current);
319
- assertMode(created.mode, SNAPSHOT_ROOT_MODE, current);
196
+ info = await lstat(segment);
320
197
  }
321
- }
322
- }
323
- async function ensureManagedRuntimeRoot(runtimeRoot, expectedUid) {
324
- try {
325
- await mkdir(runtimeRoot, { mode: SNAPSHOT_ROOT_MODE });
326
- }
327
- catch (error) {
328
- if (error.code !== 'EEXIST')
329
- throw error;
330
- }
331
- const info = await lstat(runtimeRoot);
332
- if (info.isSymbolicLink())
333
- throw new Error(`managed runtime path contains a symbolic link: ${runtimeRoot}`);
334
- assertDirectory(info, runtimeRoot);
335
- assertOwner(info.uid, expectedUid, runtimeRoot);
336
- assertMode(info.mode, SNAPSHOT_ROOT_MODE, runtimeRoot);
337
- }
338
- async function assertExistingManagedPath(runtimeRoot, target, expectedUid) {
339
- assertContained(runtimeRoot, target, 'managed snapshot');
340
- const paths = [runtimeRoot];
341
- let current = runtimeRoot;
342
- for (const segment of relative(runtimeRoot, target).split(sep).filter(Boolean)) {
343
- current = join(current, segment);
344
- paths.push(current);
345
- }
346
- for (const path of paths) {
347
- const info = await lstat(path);
348
- if (info.isSymbolicLink())
349
- throw new Error(`managed runtime path contains a symbolic link: ${path}`);
350
- if (path !== target)
351
- assertDirectory(info, path);
352
- assertOwner(info.uid, expectedUid, path);
353
- }
354
- }
355
- async function assertSourcePath(root, target, finalMustBeFile) {
356
- assertContained(root, target, 'package asset');
357
- const paths = [root];
358
- let current = root;
359
- for (const segment of relative(root, target).split(sep).filter(Boolean)) {
360
- current = join(current, segment);
361
- paths.push(current);
362
- }
363
- for (const [index, path] of paths.entries()) {
364
- const info = await lstat(path);
365
- if (info.isSymbolicLink())
366
- throw new Error(`package asset path contains a symbolic link: ${path}`);
367
- const isFinal = index === paths.length - 1;
368
- if (isFinal && finalMustBeFile) {
369
- if (!info.isFile())
370
- throw new Error(`package asset is not a regular file: ${path}`);
198
+ if (info.isSymbolicLink()) {
199
+ if (info.uid === runnerUid() || !(await stat(segment)).isDirectory())
200
+ throw new Error('runtime asset root is unsafe');
371
201
  }
372
- else {
373
- assertDirectory(info, path);
202
+ else if (!info.isDirectory()) {
203
+ throw new Error('runtime asset root is unsafe');
374
204
  }
375
205
  }
376
206
  }
377
- async function acquirePublishLock(path) {
378
- const token = randomUUID();
379
- const deadline = Date.now() + LOCK_WAIT_MS;
380
- while (true) {
381
- try {
382
- const handle = await open(path, 'wx', 0o600);
383
- try {
384
- await handle.writeFile(`${token}\n`);
385
- await handle.sync();
386
- }
387
- finally {
388
- await handle.close();
389
- }
390
- return { token };
391
- }
392
- catch (error) {
393
- if (error.code !== 'EEXIST')
394
- throw error;
395
- if (Date.now() >= deadline)
396
- throw new Error('timed out waiting for runtime asset snapshot publisher');
397
- await new Promise((resolveWait) => setTimeout(resolveWait, LOCK_POLL_MS));
207
+ async function listFiles(root) {
208
+ const output = [];
209
+ const visit = async (directory) => {
210
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
211
+ const path = join(directory, entry.name);
212
+ if (entry.isSymbolicLink())
213
+ throw new Error('runtime asset symlink rejected');
214
+ if (entry.isDirectory())
215
+ await visit(path);
216
+ else if (entry.isFile())
217
+ output.push(relative(root, path).split(sep).join('/'));
218
+ else
219
+ throw new Error('runtime asset special entry rejected');
398
220
  }
399
- }
221
+ };
222
+ await visit(root);
223
+ return output.sort(compareUtf8);
400
224
  }
401
- async function releasePublishLock(path, token) {
225
+ async function listDirectories(root) {
226
+ const output = [root];
227
+ for (const entry of await readdir(root, { withFileTypes: true }))
228
+ if (entry.isDirectory())
229
+ output.push(...await listDirectories(join(root, entry.name)));
230
+ return output;
231
+ }
232
+ async function writeSynced(path, bytes, mode) {
233
+ await writeFile(path, bytes, { mode });
234
+ await chmod(path, mode);
235
+ const handle = await open(path, 'r');
402
236
  try {
403
- const current = await readFile(path, 'utf8');
404
- if (current === `${token}\n`)
405
- await rm(path);
237
+ await handle.sync();
406
238
  }
407
- catch (error) {
408
- if (error.code !== 'ENOENT')
409
- throw error;
239
+ finally {
240
+ await handle.close();
410
241
  }
411
242
  }
412
243
  async function syncDirectory(path) {
@@ -418,56 +249,72 @@ async function syncDirectory(path) {
418
249
  await handle.close();
419
250
  }
420
251
  }
421
- function validateRelativeSnapshotPath(value) {
422
- if (value.length === 0 || isAbsolute(value) || value.includes('\\') || posix.normalize(value) !== value) {
423
- throw new Error('snapshotRelativePath must be a normalized relative POSIX path');
252
+ async function readRegular(path) {
253
+ return (await readRegularEvidence(path)).bytes;
254
+ }
255
+ async function readRegularEvidence(path) {
256
+ let handle;
257
+ try {
258
+ handle = await open(path, O_RDONLY | O_NOFOLLOW);
424
259
  }
425
- const segments = value.split('/');
426
- if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
427
- throw new Error('snapshotRelativePath contains an unsafe segment');
260
+ catch (error) {
261
+ throw new Error('runtime asset control path is invalid', { cause: error });
428
262
  }
429
- if (segments.at(-1) !== 'snapshot')
430
- throw new Error('snapshotRelativePath must end in snapshot');
431
- return value;
432
- }
433
- function assertContained(root, target, field) {
434
- const relation = relative(resolve(root), resolve(target));
435
- if (relation === '' && resolve(root) === resolve(target))
436
- return;
437
- if (relation.startsWith(`..${sep}`) || relation === '..' || isAbsolute(relation)) {
438
- throw new Error(`${field} escapes its trusted root`);
263
+ try {
264
+ const before = await handle.stat({ bigint: true });
265
+ if (!before.isFile())
266
+ throw new Error('runtime asset control path is invalid');
267
+ const bytes = await handle.readFile();
268
+ const after = await handle.stat({ bigint: true });
269
+ if (!after.isFile() || before.dev !== after.dev || before.ino !== after.ino
270
+ || before.size !== after.size || before.mtimeNs !== after.mtimeNs
271
+ || BigInt(bytes.length) !== after.size)
272
+ throw new Error('runtime asset control path changed while reading');
273
+ return { bytes, mode: Number(after.mode), uid: Number(after.uid) };
274
+ }
275
+ finally {
276
+ await handle.close();
439
277
  }
440
278
  }
441
- function assertDirectory(info, path) {
442
- if (info.isSymbolicLink())
443
- throw new Error(`path is a symbolic link: ${path}`);
444
- if (!info.isDirectory())
445
- throw new Error(`path is not a directory: ${path}`);
279
+ function validateRelativePath(value) {
280
+ if (!value || isAbsolute(value) || value.includes('\\') || posix.normalize(value) !== value
281
+ || value.split('/').some((part) => !part || part === '.' || part === '..') || basename(value) !== 'snapshot') {
282
+ throw new Error('snapshotRelativePath is invalid');
283
+ }
284
+ return value;
446
285
  }
447
- function assertOwner(actualUid, expectedUid, path) {
448
- if (actualUid !== expectedUid)
449
- throw new Error(`path owner drift: ${path}`);
286
+ function assertContained(root, target) {
287
+ const rel = relative(resolve(root), resolve(target));
288
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
289
+ throw new Error('runtime asset path escapes root');
450
290
  }
451
- function assertMode(rawMode, expectedMode, path) {
452
- const mode = rawMode & 0o777;
453
- if (mode !== expectedMode)
454
- throw new Error(`path mode drift at ${path}: expected ${expectedMode.toString(8)}, got ${mode.toString(8)}`);
291
+ function exact(value, keys) {
292
+ if (!isRecord(value) || !same(Object.keys(value).sort(compareUtf8), [...keys].sort(compareUtf8)))
293
+ throw new Error('runtime asset record shape is invalid');
455
294
  }
456
- async function pathExists(path) {
457
- try {
458
- await lstat(path);
459
- return true;
460
- }
461
- catch (error) {
462
- if (error.code === 'ENOENT')
463
- return false;
464
- throw error;
465
- }
295
+ function canonicalJson(value) {
296
+ if (value === null || ['boolean', 'string', 'number'].includes(typeof value))
297
+ return JSON.stringify(value);
298
+ if (Array.isArray(value))
299
+ return `[${value.map(canonicalJson).join(',')}]`;
300
+ if (isRecord(value))
301
+ return `{${Object.keys(value).sort(compareUtf8).map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
302
+ throw new Error('runtime asset value is not canonicalizable');
466
303
  }
467
- function runnerUid() {
468
- const uid = process.getuid?.();
469
- if (uid === undefined)
470
- throw new Error('runtime asset snapshots require POSIX ownership checks');
471
- return uid;
304
+ function runnerUid() { const uid = process.getuid?.(); if (uid === undefined)
305
+ throw new Error('POSIX ownership is required'); return uid; }
306
+ function sha(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
307
+ function compareUtf8(left, right) { return Buffer.compare(Buffer.from(left), Buffer.from(right)); }
308
+ function same(left, right) { return left.length === right.length && left.every((value, index) => value === right[index]); }
309
+ function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
310
+ function isCode(error, code) { return typeof error === 'object' && error !== null && error.code === code; }
311
+ async function exists(path) { try {
312
+ await lstat(path);
313
+ return true;
472
314
  }
315
+ catch (error) {
316
+ if (isCode(error, 'ENOENT'))
317
+ return false;
318
+ throw error;
319
+ } }
473
320
  //# sourceMappingURL=runtime-assets.js.map