codex-orchestrator 2.0.2 → 2.0.4

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 (221) hide show
  1. package/CHANGELOG.md +44 -426
  2. package/README.md +135 -34
  3. package/dist/src/index.d.ts +11 -1
  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 +67 -12
  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/cli-contract.d.ts +3 -3
  24. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  25. package/dist/src/v2/cli-contract.js +9 -1
  26. package/dist/src/v2/cli-contract.js.map +1 -1
  27. package/dist/src/v2/cli.d.ts +24 -0
  28. package/dist/src/v2/cli.d.ts.map +1 -0
  29. package/dist/src/v2/{candidate-cli.js → cli.js} +30 -26
  30. package/dist/src/v2/cli.js.map +1 -0
  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 +22 -0
  38. package/dist/src/v2/codex-process.js.map +1 -1
  39. package/dist/src/v2/config.d.ts +4 -3
  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 +10 -0
  48. package/dist/src/v2/containment.d.ts.map +1 -1
  49. package/dist/src/v2/containment.js +49 -1
  50. package/dist/src/v2/containment.js.map +1 -1
  51. package/dist/src/v2/direct-delivery.d.ts +96 -0
  52. package/dist/src/v2/direct-delivery.d.ts.map +1 -0
  53. package/dist/src/v2/direct-delivery.js +482 -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/proof-report.d.ts.map +1 -1
  68. package/dist/src/v2/proof-report.js +55 -29
  69. package/dist/src/v2/proof-report.js.map +1 -1
  70. package/dist/src/v2/route-continuations.d.ts +32 -0
  71. package/dist/src/v2/route-continuations.d.ts.map +1 -0
  72. package/dist/src/v2/route-continuations.js +2 -0
  73. package/dist/src/v2/route-continuations.js.map +1 -0
  74. package/dist/src/v2/route-coordinator.d.ts +77 -0
  75. package/dist/src/v2/route-coordinator.d.ts.map +1 -0
  76. package/dist/src/v2/route-coordinator.js +370 -0
  77. package/dist/src/v2/route-coordinator.js.map +1 -0
  78. package/dist/src/v2/route-decision.d.ts +129 -0
  79. package/dist/src/v2/route-decision.d.ts.map +1 -0
  80. package/dist/src/v2/route-decision.js +400 -0
  81. package/dist/src/v2/route-decision.js.map +1 -0
  82. package/dist/src/v2/run-issue.d.ts +64 -6
  83. package/dist/src/v2/run-issue.d.ts.map +1 -1
  84. package/dist/src/v2/run-issue.js +962 -92
  85. package/dist/src/v2/run-issue.js.map +1 -1
  86. package/dist/src/v2/run-store.d.ts +25 -1
  87. package/dist/src/v2/run-store.d.ts.map +1 -1
  88. package/dist/src/v2/run-store.js +129 -4
  89. package/dist/src/v2/run-store.js.map +1 -1
  90. package/dist/src/v2/runtime-assets.d.ts +15 -13
  91. package/dist/src/v2/runtime-assets.d.ts.map +1 -1
  92. package/dist/src/v2/runtime-assets.js +263 -416
  93. package/dist/src/v2/runtime-assets.js.map +1 -1
  94. package/dist/src/v2/runtime.d.ts +17 -9
  95. package/dist/src/v2/runtime.d.ts.map +1 -1
  96. package/dist/src/v2/runtime.js +564 -64
  97. package/dist/src/v2/runtime.js.map +1 -1
  98. package/dist/src/v2/setup-cli.d.ts.map +1 -1
  99. package/dist/src/v2/setup-cli.js +4 -10
  100. package/dist/src/v2/setup-cli.js.map +1 -1
  101. package/dist/src/v2/setup-runtime.d.ts.map +1 -1
  102. package/dist/src/v2/setup-runtime.js +19 -131
  103. package/dist/src/v2/setup-runtime.js.map +1 -1
  104. package/dist/src/v2/setup-store.d.ts +0 -5
  105. package/dist/src/v2/setup-store.d.ts.map +1 -1
  106. package/dist/src/v2/setup-store.js +3 -106
  107. package/dist/src/v2/setup-store.js.map +1 -1
  108. package/dist/src/v2/setup.d.ts +6 -43
  109. package/dist/src/v2/setup.d.ts.map +1 -1
  110. package/dist/src/v2/setup.js +13 -192
  111. package/dist/src/v2/setup.js.map +1 -1
  112. package/dist/src/v2/spec-coordinator.d.ts +85 -0
  113. package/dist/src/v2/spec-coordinator.d.ts.map +1 -0
  114. package/dist/src/v2/spec-coordinator.js +88 -0
  115. package/dist/src/v2/spec-coordinator.js.map +1 -0
  116. package/dist/src/v2/spec-delivery.d.ts +143 -0
  117. package/dist/src/v2/spec-delivery.d.ts.map +1 -0
  118. package/dist/src/v2/spec-delivery.js +401 -0
  119. package/dist/src/v2/spec-delivery.js.map +1 -0
  120. package/dist/src/v2/triage-route.d.ts +68 -0
  121. package/dist/src/v2/triage-route.d.ts.map +1 -0
  122. package/dist/src/v2/triage-route.js +223 -0
  123. package/dist/src/v2/triage-route.js.map +1 -0
  124. package/dist/src/v2/waiting-human-coordinator.d.ts +49 -0
  125. package/dist/src/v2/waiting-human-coordinator.d.ts.map +1 -0
  126. package/dist/src/v2/waiting-human-coordinator.js +509 -0
  127. package/dist/src/v2/waiting-human-coordinator.js.map +1 -0
  128. package/dist/src/v2/waiting-human.d.ts +143 -0
  129. package/dist/src/v2/waiting-human.d.ts.map +1 -0
  130. package/dist/src/v2/waiting-human.js +408 -0
  131. package/dist/src/v2/waiting-human.js.map +1 -0
  132. package/dist/src/v2/workflow-assets.d.ts +98 -0
  133. package/dist/src/v2/workflow-assets.d.ts.map +1 -0
  134. package/dist/src/v2/workflow-assets.js +646 -0
  135. package/dist/src/v2/workflow-assets.js.map +1 -0
  136. package/docs/deep-dive.md +275 -52
  137. package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
  138. package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
  139. package/internal-workflow/docs/agents/coding-skill-routing.md +123 -0
  140. package/internal-workflow/docs/agents/confidence-rubric.md +65 -0
  141. package/internal-workflow/docs/agents/contract-test-ledger.md +60 -0
  142. package/internal-workflow/docs/agents/review-gates.md +42 -0
  143. package/internal-workflow/docs/agents/review-protocol.md +98 -0
  144. package/internal-workflow/docs/agents/tool-usage.md +88 -0
  145. package/internal-workflow/evals/coding-skill-evals.json +66 -0
  146. package/internal-workflow/manifest.json +1 -0
  147. package/internal-workflow/operations/acceptance-proof/SKILL.md +9 -0
  148. package/internal-workflow/operations/ambiguity-review/SKILL.md +5 -0
  149. package/internal-workflow/operations/code-review/SKILL.md +23 -0
  150. package/internal-workflow/operations/implementation/SKILL.md +24 -0
  151. package/internal-workflow/operations/spec-author/SKILL.md +12 -0
  152. package/internal-workflow/operations/spec-review/SKILL.md +12 -0
  153. package/internal-workflow/operations/triage/SKILL.md +12 -0
  154. package/internal-workflow/profiles/analyst_deep.toml +9 -0
  155. package/internal-workflow/profiles/implementer_standard.toml +9 -0
  156. package/internal-workflow/profiles/proof_agent.toml +8 -0
  157. package/internal-workflow/profiles/reviewer_deep.toml +9 -0
  158. package/internal-workflow/profiles/reviewer_standard.toml +9 -0
  159. package/internal-workflow/schemas/ambiguity-review-v1.json +1 -0
  160. package/internal-workflow/schemas/code-review-v1.json +1 -0
  161. package/internal-workflow/schemas/implementation-report-v1.json +1 -0
  162. package/internal-workflow/schemas/proof-report-v1.json +1 -0
  163. package/internal-workflow/schemas/spec-author-v1.json +1 -0
  164. package/internal-workflow/schemas/spec-review-v1.json +30 -0
  165. package/internal-workflow/schemas/triage-route-v1.json +1 -0
  166. package/internal-workflow/skills/acceptance-proof/agents/openai.yaml +6 -0
  167. package/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +6 -1
  168. package/internal-workflow/skills/agent-auto/agents/openai.yaml +6 -0
  169. package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
  170. package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
  171. package/internal-workflow/skills/code-review/SKILL.md +279 -0
  172. package/internal-workflow/skills/code-review/agents/openai.yaml +4 -0
  173. package/internal-workflow/skills/code-review/references/bug-classes.md +56 -0
  174. package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
  175. package/internal-workflow/skills/code-review/references/framework-lenses.md +34 -0
  176. package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -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 +102 -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 +115 -0
  185. package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
  186. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
  187. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
  188. package/internal-workflow/skills/small-task-implementer/SKILL.md +104 -0
  189. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
  190. package/internal-workflow/skills/spec-implementer/SKILL.md +126 -0
  191. package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
  192. package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
  193. package/internal-workflow/skills/spec-implementer/references/review-loop.md +94 -0
  194. package/internal-workflow/skills/tdd/SKILL.md +72 -0
  195. package/internal-workflow/skills/tdd/agents/openai.yaml +6 -0
  196. package/internal-workflow/skills/tdd/interface-design.md +31 -0
  197. package/internal-workflow/skills/tdd/mocking.md +59 -0
  198. package/internal-workflow/skills/tdd/refactoring.md +10 -0
  199. package/internal-workflow/skills/tdd/tests.md +77 -0
  200. package/internal-workflow/skills/triage/AGENT-BRIEF.md +192 -0
  201. package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +101 -0
  202. package/internal-workflow/skills/triage/SKILL.md +134 -0
  203. package/internal-workflow/skills/triage/agents/openai.yaml +6 -0
  204. package/package.json +14 -8
  205. package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
  206. package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
  207. package/dist/src/v2/adapters/target-activity-fence.js +0 -249
  208. package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
  209. package/dist/src/v2/candidate-cli.d.ts +0 -22
  210. package/dist/src/v2/candidate-cli.d.ts.map +0 -1
  211. package/dist/src/v2/candidate-cli.js.map +0 -1
  212. package/dist/src/v2/legacy-cutover.d.ts +0 -52
  213. package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
  214. package/dist/src/v2/legacy-cutover.js +0 -87
  215. package/dist/src/v2/legacy-cutover.js.map +0 -1
  216. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/SKILL.md +0 -0
  217. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/android.md +0 -0
  218. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/browser.md +0 -0
  219. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/ios.md +0 -0
  220. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/android-lease.mjs +0 -0
  221. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/ios-lease.mjs +0 -0
@@ -0,0 +1,646 @@
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 { dirname, join, relative, resolve, sep } from 'node:path';
5
+ import { publishImmutableWorkflow } from './immutable-workflow-publisher.js';
6
+ const GENERATION_MAGIC = 'codex-orchestrator-workflow-generation-v2\0';
7
+ const SOURCE_MAGIC = 'codex-orchestrator-workflow-source-v2\0';
8
+ const CONTENT_MAGIC = 'codex-orchestrator-sealed-content-v1\0';
9
+ const SHA256 = /^[a-f0-9]{64}$/u;
10
+ 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
+ const EXPECTED_OPERATION_BINDINGS = {
12
+ 'acceptance-proof': { sourceSkill: 'acceptance-proof', dependencySkills: [], outputSchema: 'schemas/proof-report-v1.json', profile: 'proof_agent' },
13
+ 'ambiguity-review': { sourceSkill: null, dependencySkills: [], outputSchema: 'schemas/ambiguity-review-v1.json', profile: 'reviewer_deep' },
14
+ 'code-review': { sourceSkill: 'code-review', dependencySkills: [], outputSchema: 'schemas/code-review-v1.json', profile: 'reviewer_standard' },
15
+ implementation: {
16
+ sourceSkill: 'agent-auto', dependencySkills: ['code-debugger', 'diagnosing-bugs', 'small-task-implementer', 'tdd'],
17
+ outputSchema: 'schemas/implementation-report-v1.json', profile: 'implementer_standard',
18
+ },
19
+ 'spec-author': { sourceSkill: 'implementation-spec-maker', dependencySkills: [], outputSchema: 'schemas/spec-author-v1.json', profile: 'implementer_standard' },
20
+ 'spec-review': { sourceSkill: 'implementation-spec-review', dependencySkills: [], outputSchema: 'schemas/spec-review-v1.json', profile: 'reviewer_deep' },
21
+ triage: { sourceSkill: 'triage', dependencySkills: [], outputSchema: 'schemas/triage-route-v1.json', profile: 'analyst_deep' },
22
+ };
23
+ export function parseWorkflowExecutionProfile(text, policy) {
24
+ const scalar = (key) => text.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"\\s*$`, 'mu'))?.[1];
25
+ const instructions = text.match(/^developer_instructions\s*=\s*"""([\s\S]*?)"""\s*$/mu)?.[1]?.trim();
26
+ const name = scalar('name');
27
+ const model = scalar('model');
28
+ const reasoningEffort = scalar('model_reasoning_effort');
29
+ const sandboxMode = scalar('sandbox_mode');
30
+ if (!name || !model || !/^[a-z0-9._-]+$/u.test(model)
31
+ || !['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(reasoningEffort ?? '')
32
+ || !['read-only', 'workspace-write'].includes(sandboxMode ?? '') || !instructions) {
33
+ throw new Error('workflow execution profile is invalid');
34
+ }
35
+ if (sandboxMode !== policy.sandboxMode)
36
+ throw new Error('workflow execution profile sandbox mismatch');
37
+ return {
38
+ name,
39
+ model,
40
+ reasoningEffort: reasoningEffort,
41
+ sandboxMode: sandboxMode,
42
+ developerInstructions: instructions,
43
+ };
44
+ }
45
+ export function sealedWorkflowMode(sourceMode) {
46
+ if (sourceMode === 0o644)
47
+ return 0o444;
48
+ if (sourceMode === 0o755)
49
+ return 0o555;
50
+ throw new Error('workflow source mode cannot be sealed');
51
+ }
52
+ export function sealedWorkflowContentSha256(files) {
53
+ return sha(Buffer.from(`${CONTENT_MAGIC}${canonicalJson({ files })}`, 'utf8'));
54
+ }
55
+ export async function resolveWorkflowOperation(receipt, operationId) {
56
+ await verifyWorkflowGeneration(receipt);
57
+ const manifest = parseManifest(JSON.parse((await readRegular(join(receipt.generationRoot, 'manifest.json'))).toString('utf8')));
58
+ const operation = manifest.operations[operationId];
59
+ if (!operation || operation.id !== operationId)
60
+ throw new Error(`workflow operation is unavailable: ${operationId}`);
61
+ const profile = manifest.profiles[operation.profile];
62
+ if (!profile)
63
+ throw new Error(`workflow operation profile is unavailable: ${operationId}`);
64
+ const required = [operation.entry, operation.outputSchema, profile];
65
+ if (!required.every((path) => operation.files.includes(path)))
66
+ throw new Error(`workflow operation closure is invalid: ${operationId}`);
67
+ const sourceSkillPath = operation.sourceSkill ? manifest.skills[operation.sourceSkill]?.entry : undefined;
68
+ if (operation.sourceSkill && (!sourceSkillPath || !operation.files.includes(sourceSkillPath))) {
69
+ throw new Error(`workflow operation source skill is unavailable: ${operationId}`);
70
+ }
71
+ return {
72
+ id: operation.id,
73
+ workflowRoot: receipt.generationRoot,
74
+ entryPath: join(receipt.generationRoot, ...operation.entry.split('/')),
75
+ schemaPath: join(receipt.generationRoot, ...operation.outputSchema.split('/')),
76
+ profilePath: join(receipt.generationRoot, ...profile.split('/')),
77
+ sourceSkillPath: sourceSkillPath ? join(receipt.generationRoot, ...sourceSkillPath.split('/')) : undefined,
78
+ policy: structuredClone(operation.policy),
79
+ files: operation.files.map((path) => structuredClone(manifest.files.find((file) => file.path === path))),
80
+ };
81
+ }
82
+ export async function workflowSkillHashes(receipt) {
83
+ await verifyWorkflowGeneration(receipt);
84
+ const manifest = parseManifest(JSON.parse((await readRegular(join(receipt.generationRoot, 'manifest.json'))).toString('utf8')));
85
+ return Object.fromEntries(Object.entries(manifest.skills).map(([id, skill]) => {
86
+ const file = manifest.files.find((candidate) => candidate.path === skill.entry);
87
+ if (!file)
88
+ throw new Error(`workflow skill entry is unavailable: ${id}`);
89
+ return [id, file.sha256];
90
+ }));
91
+ }
92
+ export async function loadPackageWorkflow(packageRootInput) {
93
+ const packageRoot = await realpath(resolve(packageRootInput));
94
+ const workflowRoot = join(packageRoot, 'internal-workflow');
95
+ const workflowRootInfo = await lstat(workflowRoot);
96
+ if (!workflowRootInfo.isDirectory() || workflowRootInfo.isSymbolicLink())
97
+ throw new Error('package workflow root is unsafe');
98
+ const manifestPath = join(workflowRoot, 'manifest.json');
99
+ const manifestBytes = await readRegular(manifestPath);
100
+ const manifest = parseManifest(JSON.parse(manifestBytes.toString('utf8')));
101
+ verifyManifestIdentity(manifest, manifestBytes);
102
+ const actual = await listFiles(workflowRoot);
103
+ const expected = ['manifest.json', ...manifest.files.map((file) => file.path)].sort(compareUtf8);
104
+ if (!same(actual, expected))
105
+ throw new Error('workflow file closure mismatch');
106
+ const bytesByPath = new Map();
107
+ for (const file of manifest.files) {
108
+ const path = join(workflowRoot, ...file.path.split('/'));
109
+ const { bytes, mode } = await readRegularEvidence(path);
110
+ if ((mode & 0o777) !== file.mode)
111
+ throw new Error(`workflow mode drift: ${file.path}`);
112
+ if (bytes.length !== file.size || sha(bytes) !== file.sha256)
113
+ throw new Error(`workflow hash mismatch: ${file.path}`);
114
+ bytesByPath.set(file.path, bytes);
115
+ }
116
+ verifyEvalBytes(manifest, bytesByPath);
117
+ verifyOperationEntryBindings(manifest, bytesByPath);
118
+ return { manifest, manifestBytes, bytesByPath };
119
+ }
120
+ export async function materializeWorkflowGeneration(input) {
121
+ const loaded = await loadPackageWorkflow(input.packageRoot);
122
+ const requestedRuntimeRoot = resolve(input.runtimeRoot);
123
+ await ensureManagedPath(requestedRuntimeRoot, join(requestedRuntimeRoot, 'workflow-generations'));
124
+ const runtimeRoot = await realpath(requestedRuntimeRoot);
125
+ const parent = join(runtimeRoot, 'workflow-generations');
126
+ const identity = loaded.manifest.generationHash;
127
+ return publishImmutableWorkflow({
128
+ parent,
129
+ identity,
130
+ bootId: input.bootId,
131
+ createOwner: (parentToken, processStartIdentity) => newOwner(input.bootId, parentToken, processStartIdentity),
132
+ parseOwner,
133
+ createReady: (owner, contentSha256) => ({
134
+ version: 1, status: 'ready', token: owner.token, contentSha256,
135
+ }),
136
+ parseReady,
137
+ serializeRecord: (value) => Buffer.from(`${canonicalJson(value)}\n`, 'utf8'),
138
+ readControl: readRegular,
139
+ writeContent: async (contentRoot, onStep) => {
140
+ await writeContent(contentRoot, loaded, onStep);
141
+ return sealedContentSha256(contentRoot);
142
+ },
143
+ resultFromReady: ({ ready, contentRoot }) => receiptFromReady(loaded, input.packageVersion, ready, contentRoot),
144
+ resultFromPublished: async ({ contentRoot, content: contentSha256 }) => ({
145
+ generationHash: identity,
146
+ manifestSha256: sha(loaded.manifestBytes),
147
+ packageVersion: input.packageVersion,
148
+ generationRoot: await realpath(contentRoot),
149
+ contentSha256,
150
+ }),
151
+ assertContentPathAvailable: async (contentRoot) => {
152
+ if (await exists(contentRoot))
153
+ throw new Error('workflow generation content path already exists');
154
+ },
155
+ verifyReadyCollision: (existing, proposed) => {
156
+ if (!existing.equals(proposed))
157
+ throw new Error('workflow ready receipt mismatch');
158
+ },
159
+ activePublisherError: 'workflow generation publisher is still active',
160
+ recoveryChainError: 'workflow recovery chain is invalid',
161
+ onStep: input.onStep,
162
+ });
163
+ }
164
+ export async function verifyWorkflowGeneration(receipt) {
165
+ if (!SHA256.test(receipt.generationHash) || !SHA256.test(receipt.manifestSha256) || !SHA256.test(receipt.contentSha256)) {
166
+ throw new Error('workflow generation receipt identity is invalid');
167
+ }
168
+ const root = resolve(receipt.generationRoot);
169
+ if (await realpath(root) !== root)
170
+ throw new Error('workflow generation root contains a symbolic link');
171
+ const rootInfo = await lstat(root);
172
+ if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || rootInfo.uid !== runnerUid() || (rootInfo.mode & 0o777) !== 0o555) {
173
+ throw new Error('workflow generation root mode or owner drift');
174
+ }
175
+ const manifestBytes = await readRegular(join(root, 'manifest.json'));
176
+ if (sha(manifestBytes) !== receipt.manifestSha256)
177
+ throw new Error('workflow generation manifest mismatch');
178
+ const manifest = parseManifest(JSON.parse(manifestBytes.toString('utf8')));
179
+ verifyManifestIdentity(manifest, manifestBytes);
180
+ if (manifest.generationHash !== receipt.generationHash)
181
+ throw new Error('workflow generation identity mismatch');
182
+ const actual = await listFiles(root);
183
+ const expected = ['manifest.json', ...manifest.files.map((file) => file.path)].sort(compareUtf8);
184
+ if (!same(actual, expected))
185
+ throw new Error('workflow generation file closure drift');
186
+ const bytesByPath = new Map();
187
+ for (const file of manifest.files) {
188
+ const path = join(root, ...file.path.split('/'));
189
+ const { bytes, mode: actualMode, uid } = await readRegularEvidence(path);
190
+ const mode = file.mode === 0o755 ? 0o555 : 0o444;
191
+ if (uid !== runnerUid() || (actualMode & 0o777) !== mode)
192
+ throw new Error(`workflow generation mode drift: ${file.path}`);
193
+ if (bytes.length !== file.size || sha(bytes) !== file.sha256)
194
+ throw new Error(`workflow generation hash drift: ${file.path}`);
195
+ bytesByPath.set(file.path, bytes);
196
+ }
197
+ verifyEvalBytes(manifest, bytesByPath);
198
+ verifyOperationEntryBindings(manifest, bytesByPath);
199
+ for (const directory of await listDirectories(root)) {
200
+ const info = await lstat(directory);
201
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== runnerUid() || (info.mode & 0o777) !== 0o555) {
202
+ throw new Error('workflow generation directory mode or owner drift');
203
+ }
204
+ }
205
+ if (await sealedContentSha256(root) !== receipt.contentSha256)
206
+ throw new Error('workflow generation content hash mismatch');
207
+ }
208
+ function verifyManifestIdentity(manifest, manifestBytes) {
209
+ const canonicalBytes = Buffer.from(`${canonicalJson(manifest)}\n`, 'utf8');
210
+ if (!manifestBytes.equals(canonicalBytes))
211
+ throw new Error('workflow manifest bytes are not canonical');
212
+ const sourceFingerprint = sha(Buffer.from(`${SOURCE_MAGIC}${canonicalJson({ files: manifest.files })}`, 'utf8'));
213
+ if (sourceFingerprint !== manifest.sourceFingerprint)
214
+ throw new Error('workflow source fingerprint mismatch');
215
+ const generationHash = sha(Buffer.from(`${GENERATION_MAGIC}${canonicalJson({ ...manifest, generationHash: '' })}`, 'utf8'));
216
+ if (generationHash !== manifest.generationHash)
217
+ throw new Error('workflow generation hash mismatch');
218
+ }
219
+ async function writeContent(root, loaded, onStep) {
220
+ await writeContentFile(root, 'manifest.json', loaded.manifestBytes, 0o444);
221
+ await onStep('after-first-content-file');
222
+ for (const file of loaded.manifest.files) {
223
+ await writeContentFile(root, file.path, loaded.bytesByPath.get(file.path), sealedWorkflowMode(file.mode));
224
+ }
225
+ for (const directory of (await listDirectories(root)).sort((left, right) => right.length - left.length)) {
226
+ await chmod(directory, 0o555);
227
+ await syncDirectory(directory);
228
+ }
229
+ }
230
+ async function writeContentFile(root, logical, bytes, mode) {
231
+ const path = join(root, ...logical.split('/'));
232
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
233
+ await writeSyncedFile(path, bytes, mode);
234
+ }
235
+ async function sealedContentSha256(root) {
236
+ const files = [];
237
+ for (const path of await listFiles(root)) {
238
+ const absolute = join(root, ...path.split('/'));
239
+ const { bytes, mode } = await readRegularEvidence(absolute);
240
+ files.push({ path, sealedMode: mode & 0o777, size: bytes.length, sha256: sha(bytes) });
241
+ }
242
+ return sealedWorkflowContentSha256(files);
243
+ }
244
+ async function receiptFromReady(loaded, packageVersion, ready, contentRoot) {
245
+ const receipt = {
246
+ generationHash: loaded.manifest.generationHash,
247
+ manifestSha256: sha(loaded.manifestBytes),
248
+ packageVersion,
249
+ generationRoot: await realpath(contentRoot),
250
+ contentSha256: ready.contentSha256,
251
+ };
252
+ await verifyWorkflowGeneration(receipt);
253
+ return receipt;
254
+ }
255
+ function newOwner(bootId, parentToken, processStartIdentity) {
256
+ return {
257
+ version: 1, status: 'building', bootId, pid: process.pid, token: randomUUID(), parentToken,
258
+ processStartIdentity, startedAt: new Date().toISOString(),
259
+ };
260
+ }
261
+ function parseOwner(value) {
262
+ assertExact(value, ['version', 'status', 'bootId', 'pid', 'token', 'parentToken', 'processStartIdentity', 'startedAt']);
263
+ if (value.version !== 1 || value.status !== 'building' || typeof value.bootId !== 'string' || !value.bootId
264
+ || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.token !== 'string' || !UUID.test(value.token)
265
+ || !(value.parentToken === null || (typeof value.parentToken === 'string' && UUID.test(value.parentToken)))
266
+ || typeof value.processStartIdentity !== 'string' || !value.processStartIdentity
267
+ || typeof value.startedAt !== 'string' || Number.isNaN(Date.parse(value.startedAt)))
268
+ throw new Error('workflow owner record is invalid');
269
+ return value;
270
+ }
271
+ function parseReady(value) {
272
+ assertExact(value, ['version', 'status', 'token', 'contentSha256']);
273
+ if (value.version !== 1 || value.status !== 'ready' || typeof value.token !== 'string' || !UUID.test(value.token)
274
+ || !SHA256.test(String(value.contentSha256)))
275
+ throw new Error('workflow ready receipt is invalid');
276
+ return value;
277
+ }
278
+ function parseManifest(value) {
279
+ if (!isRecord(value) || value.version !== 2)
280
+ throw new Error('workflow manifest version is invalid');
281
+ assertExact(value, ['version', 'sourceFingerprint', 'generationHash', 'files', 'skills', 'profiles', 'operations', 'evals']);
282
+ if (!SHA256.test(String(value.sourceFingerprint)) || !SHA256.test(String(value.generationHash))
283
+ || !Array.isArray(value.files) || !isRecord(value.skills) || !isRecord(value.profiles) || !isRecord(value.operations)
284
+ || !isRecord(value.evals))
285
+ throw new Error('workflow manifest is invalid');
286
+ const skills = value.skills;
287
+ const profiles = value.profiles;
288
+ const operations = value.operations;
289
+ const evals = value.evals;
290
+ let previous = '';
291
+ const physical = new Set();
292
+ for (const file of value.files) {
293
+ assertExact(file, ['path', 'mode', 'size', 'sha256']);
294
+ if (typeof file.path !== 'string' || normalizePath(file.path) !== file.path || (previous && compareUtf8(previous, file.path) >= 0)
295
+ || ![0o644, 0o755].includes(file.mode) || !Number.isSafeInteger(file.size) || file.size < 0
296
+ || !SHA256.test(String(file.sha256)))
297
+ throw new Error('workflow manifest file record is invalid');
298
+ previous = file.path;
299
+ physical.add(file.path);
300
+ }
301
+ if (Object.keys(skills).length === 0)
302
+ throw new Error('workflow skills inventory is invalid');
303
+ for (const [id, skill] of Object.entries(skills)) {
304
+ assertExact(skill, ['entry', 'metadata', 'files']);
305
+ if (typeof skill.entry !== 'string' || typeof skill.metadata !== 'string' || !Array.isArray(skill.files)) {
306
+ throw new Error(`workflow skill is invalid: ${id}`);
307
+ }
308
+ validatePathList(skill.files, physical, `workflow skill closure: ${id}`);
309
+ if (!skill.files.includes(skill.entry) || !skill.files.includes(skill.metadata)
310
+ || skill.entry !== `skills/${id}/SKILL.md` || skill.metadata !== `skills/${id}/agents/openai.yaml`) {
311
+ throw new Error(`workflow skill authority is invalid: ${id}`);
312
+ }
313
+ }
314
+ if (Object.keys(profiles).length === 0)
315
+ throw new Error('workflow profiles inventory is invalid');
316
+ for (const [id, path] of Object.entries(profiles)) {
317
+ if (typeof path !== 'string' || normalizePath(path) !== path || path !== `profiles/${id}.toml` || !physical.has(path)) {
318
+ throw new Error(`workflow profile is invalid: ${id}`);
319
+ }
320
+ }
321
+ const bindings = EXPECTED_OPERATION_BINDINGS;
322
+ assertRecordKeys(operations, Object.keys(bindings), 'workflow operations');
323
+ for (const [id, operation] of Object.entries(operations)) {
324
+ assertExact(operation, ['id', 'entry', 'sourceSkill', 'dependencySkills', 'resources', 'outputSchema', 'profile', 'policy', 'files']);
325
+ if (operation.id !== id || typeof operation.entry !== 'string' || operation.entry !== `operations/${id}/SKILL.md`
326
+ || !(operation.sourceSkill === null || typeof operation.sourceSkill === 'string')
327
+ || typeof operation.outputSchema !== 'string' || typeof operation.profile !== 'string'
328
+ || !Array.isArray(operation.files) || !isRecord(operation.policy)) {
329
+ throw new Error(`workflow operation is invalid: ${id}`);
330
+ }
331
+ if (id === 'ambiguity-review' ? operation.sourceSkill !== null : operation.sourceSkill === null) {
332
+ throw new Error(`workflow operation source skill is invalid: ${id}`);
333
+ }
334
+ const binding = bindings[id];
335
+ validateSortedStrings(operation.dependencySkills, `workflow operation dependency skills: ${id}`);
336
+ validateSortedStrings(operation.resources, `workflow operation resources: ${id}`);
337
+ const dependencySkills = operation.dependencySkills;
338
+ const resources = operation.resources;
339
+ if (!same(dependencySkills, binding.dependencySkills)) {
340
+ throw new Error(`workflow operation dependency binding is invalid: ${id}`);
341
+ }
342
+ if (operation.sourceSkill !== binding.sourceSkill || operation.outputSchema !== binding.outputSchema || operation.profile !== binding.profile) {
343
+ throw new Error(`workflow operation binding is invalid: ${id}`);
344
+ }
345
+ const sourceSkill = operation.sourceSkill === null ? undefined : skills[operation.sourceSkill];
346
+ if (operation.sourceSkill !== null && !sourceSkill)
347
+ throw new Error(`workflow operation source skill is invalid: ${id}`);
348
+ if (dependencySkills.some((skill) => skill === operation.sourceSkill || !(skill in skills))) {
349
+ throw new Error(`workflow operation dependency skill is invalid: ${id}`);
350
+ }
351
+ if (resources.some((path) => typeof path !== 'string' || !physical.has(path)))
352
+ throw new Error(`workflow operation resource is invalid: ${id}`);
353
+ if (!(operation.profile in profiles))
354
+ throw new Error(`workflow operation profile is invalid: ${id}`);
355
+ validatePathList(operation.files, physical, `workflow operation closure: ${id}`);
356
+ const required = [
357
+ operation.entry,
358
+ operation.outputSchema,
359
+ profiles[operation.profile],
360
+ ...resources,
361
+ ...(sourceSkill?.files ?? []),
362
+ ...dependencySkills.flatMap((skill) => skills[skill].files),
363
+ ].sort(compareUtf8);
364
+ if (!same(operation.files, [...new Set(required)].sort(compareUtf8)))
365
+ throw new Error(`workflow operation closure is invalid: ${id}`);
366
+ if (operation.files.some((path) => path.includes('/evals/'))) {
367
+ throw new Error(`workflow operation eval isolation is invalid: ${id}`);
368
+ }
369
+ validateOperationPolicy(id, operation.policy);
370
+ }
371
+ const evalPaths = new Set();
372
+ for (const [id, entry] of Object.entries(evals)) {
373
+ assertExact(entry, ['owner', 'path']);
374
+ if (!id || !(entry.owner === null || (typeof entry.owner === 'string' && entry.owner in skills))
375
+ || typeof entry.path !== 'string' || !physical.has(entry.path) || evalPaths.has(entry.path)) {
376
+ throw new Error(`workflow eval binding is invalid: ${id}`);
377
+ }
378
+ evalPaths.add(entry.path);
379
+ }
380
+ if (evalPaths.size === 0)
381
+ throw new Error('workflow eval inventory is empty');
382
+ return value;
383
+ }
384
+ function verifyEvalBytes(manifest, bytesByPath) {
385
+ const caseIds = new Set();
386
+ for (const [id, entry] of Object.entries(manifest.evals)) {
387
+ const bytes = bytesByPath.get(entry.path);
388
+ if (!bytes)
389
+ throw new Error(`workflow eval bytes are missing: ${id}`);
390
+ let value;
391
+ try {
392
+ value = JSON.parse(bytes.toString('utf8'));
393
+ }
394
+ catch {
395
+ throw new Error(`workflow eval JSON is invalid: ${id}`);
396
+ }
397
+ if (!isRecord(value) || value.schema_version !== 1 || !Array.isArray(value.cases) || value.cases.length === 0
398
+ || (entry.owner !== null && value.skill !== entry.owner))
399
+ throw new Error(`workflow eval contract is invalid: ${id}`);
400
+ for (const item of value.cases) {
401
+ if (!isRecord(item) || typeof item.id !== 'string' || item.id.length === 0 || caseIds.has(item.id)
402
+ || typeof item.prompt !== 'string' || item.prompt.length === 0
403
+ || !evalTextList(item.expected) || !evalTextList(item.forbidden))
404
+ throw new Error(`workflow eval case is invalid: ${id}`);
405
+ caseIds.add(item.id);
406
+ }
407
+ }
408
+ }
409
+ function verifyOperationEntryBindings(manifest, bytesByPath) {
410
+ for (const [id, operation] of Object.entries(manifest.operations)) {
411
+ const entryBytes = bytesByPath.get(operation.entry);
412
+ if (!entryBytes)
413
+ throw new Error(`workflow operation entry bytes are missing: ${id}`);
414
+ const linked = new Set();
415
+ for (const match of entryBytes.toString('utf8').matchAll(/\]\(([^)#]+)(?:#[^)]+)?\)/gu)) {
416
+ const target = match[1];
417
+ if (/^[a-z]+:/iu.test(target) || target.startsWith('/'))
418
+ continue;
419
+ linked.add(normalizePath(relative('/', resolve('/', dirname(operation.entry), target)).split(sep).join('/')));
420
+ }
421
+ const required = [
422
+ ...(operation.sourceSkill === null ? [] : [[operation.sourceSkill, manifest.skills[operation.sourceSkill].entry]]),
423
+ ...operation.dependencySkills.map((skill) => [skill, manifest.skills[skill].entry]),
424
+ ...operation.resources.map((path) => [path, path]),
425
+ ];
426
+ for (const [name, path] of required) {
427
+ if (!linked.has(path))
428
+ throw new Error(`workflow operation ${id} does not reference declared dependency ${name}`);
429
+ }
430
+ }
431
+ }
432
+ function evalTextList(value) {
433
+ return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'string' && item.length > 0);
434
+ }
435
+ function validateSortedStrings(value, field) {
436
+ if (!Array.isArray(value))
437
+ throw new Error(`${field} is invalid`);
438
+ let previous = '';
439
+ for (const item of value) {
440
+ if (typeof item !== 'string' || item.length === 0 || (previous && compareUtf8(previous, item) >= 0))
441
+ throw new Error(`${field} is invalid`);
442
+ previous = item;
443
+ }
444
+ }
445
+ function validateOperationPolicy(id, value) {
446
+ assertExact(value, [
447
+ 'sandboxMode', 'cwdClass', 'worktreeAccess', 'writableRootClasses', 'runnerPostcondition',
448
+ 'network', 'networkHosts', 'mcpTools', 'approvalCeiling', 'externalWrite',
449
+ ]);
450
+ if (value.network !== 'deny' || !Array.isArray(value.networkHosts) || value.networkHosts.length !== 0
451
+ || !Array.isArray(value.mcpTools) || value.mcpTools.length !== 0 || value.approvalCeiling !== 'never'
452
+ || value.externalWrite !== false)
453
+ throw new Error(`workflow operation authority is invalid: ${id}`);
454
+ const expected = id === 'implementation'
455
+ ? ['workspace-write', 'worktree', 'write', 'worktree', 'change-set']
456
+ : id === 'acceptance-proof'
457
+ ? ['workspace-write', 'worktree', 'write', 'worktree', 'proof-only']
458
+ : id === 'spec-author'
459
+ ? ['workspace-write', 'target-state', 'write', 'target-state', 'spec-only']
460
+ : ['read-only', 'worktree', 'read-only', '', 'report-only'];
461
+ const roots = Array.isArray(value.writableRootClasses) ? value.writableRootClasses.join(',') : '<invalid>';
462
+ const actual = [value.sandboxMode, value.cwdClass, value.worktreeAccess, roots, value.runnerPostcondition];
463
+ if (!same(actual.map(String), expected))
464
+ throw new Error(`workflow operation policy is invalid: ${id}`);
465
+ }
466
+ function validatePathList(value, physical, field) {
467
+ let previous = '';
468
+ for (const path of value) {
469
+ if (typeof path !== 'string' || normalizePath(path) !== path || (previous && compareUtf8(previous, path) >= 0) || !physical.has(path)) {
470
+ throw new Error(`${field} is invalid`);
471
+ }
472
+ previous = path;
473
+ }
474
+ if (value.length === 0)
475
+ throw new Error(`${field} is empty`);
476
+ }
477
+ function assertRecordKeys(value, expected, field) {
478
+ const actual = Object.keys(value).sort(compareUtf8);
479
+ const wanted = [...expected].sort(compareUtf8);
480
+ if (!same(actual, wanted))
481
+ throw new Error(`${field} inventory is invalid`);
482
+ }
483
+ async function listFiles(root) {
484
+ const result = [];
485
+ const visit = async (directory) => {
486
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
487
+ const path = join(directory, entry.name);
488
+ const logical = relative(root, path).split(sep).join('/');
489
+ if (entry.isSymbolicLink())
490
+ throw new Error(`workflow symlink rejected: ${logical}`);
491
+ if (entry.isDirectory())
492
+ await visit(path);
493
+ else if (entry.isFile())
494
+ result.push(logical);
495
+ else
496
+ throw new Error(`workflow special entry rejected: ${logical}`);
497
+ }
498
+ };
499
+ await visit(root);
500
+ return result.sort(compareUtf8);
501
+ }
502
+ async function listDirectories(root) {
503
+ const result = [root];
504
+ for (const entry of await readdir(root, { withFileTypes: true }))
505
+ if (entry.isDirectory())
506
+ result.push(...await listDirectories(join(root, entry.name)));
507
+ return result;
508
+ }
509
+ async function writeSyncedFile(path, bytes, mode) {
510
+ await writeFile(path, bytes, { mode });
511
+ await chmod(path, mode);
512
+ const handle = await open(path, 'r');
513
+ try {
514
+ await handle.sync();
515
+ }
516
+ finally {
517
+ await handle.close();
518
+ }
519
+ }
520
+ async function syncDirectory(path) {
521
+ const handle = await open(path, 'r');
522
+ try {
523
+ await handle.sync();
524
+ }
525
+ finally {
526
+ await handle.close();
527
+ }
528
+ }
529
+ async function readRegular(path) {
530
+ return (await readRegularEvidence(path)).bytes;
531
+ }
532
+ async function readRegularEvidence(path) {
533
+ let handle;
534
+ try {
535
+ handle = await open(path, O_RDONLY | O_NOFOLLOW);
536
+ }
537
+ catch (error) {
538
+ throw new Error(`workflow path is not a regular file: ${path}`, { cause: error });
539
+ }
540
+ try {
541
+ const before = await handle.stat({ bigint: true });
542
+ if (!before.isFile())
543
+ throw new Error(`workflow path is not a regular file: ${path}`);
544
+ const bytes = await handle.readFile();
545
+ const after = await handle.stat({ bigint: true });
546
+ if (!after.isFile() || before.dev !== after.dev || before.ino !== after.ino
547
+ || before.size !== after.size || before.mtimeNs !== after.mtimeNs
548
+ || BigInt(bytes.length) !== after.size)
549
+ throw new Error(`workflow path changed while reading: ${path}`);
550
+ return { bytes, mode: Number(after.mode), uid: Number(after.uid) };
551
+ }
552
+ finally {
553
+ await handle.close();
554
+ }
555
+ }
556
+ async function ensureManagedPath(root, target) {
557
+ await createManagedRoot(root, 'workflow runtime root is unsafe');
558
+ const rootInfo = await lstat(root);
559
+ if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || rootInfo.uid !== runnerUid())
560
+ throw new Error('workflow runtime root is unsafe');
561
+ let current = root;
562
+ for (const segment of relative(root, target).split(sep).filter(Boolean)) {
563
+ current = join(current, segment);
564
+ await mkdir(current, { mode: 0o700 }).catch((error) => { if (!isCode(error, 'EEXIST'))
565
+ throw error; });
566
+ const info = await lstat(current);
567
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== runnerUid() || (info.mode & 0o777) !== 0o700) {
568
+ throw new Error('workflow managed path is unsafe');
569
+ }
570
+ }
571
+ }
572
+ async function createManagedRoot(path, message) {
573
+ const chain = [];
574
+ let current = resolve(path);
575
+ while (dirname(current) !== current) {
576
+ chain.push(current);
577
+ current = dirname(current);
578
+ }
579
+ for (const segment of chain.reverse()) {
580
+ let info;
581
+ try {
582
+ info = await lstat(segment);
583
+ }
584
+ catch (error) {
585
+ if (!isCode(error, 'ENOENT'))
586
+ throw error;
587
+ try {
588
+ await mkdir(segment, { mode: 0o700 });
589
+ }
590
+ catch (mkdirError) {
591
+ if (!isCode(mkdirError, 'EEXIST'))
592
+ throw mkdirError;
593
+ }
594
+ info = await lstat(segment);
595
+ }
596
+ if (info.isSymbolicLink()) {
597
+ if (info.uid === runnerUid() || !(await stat(segment)).isDirectory())
598
+ throw new Error(message);
599
+ }
600
+ else if (!info.isDirectory()) {
601
+ throw new Error(message);
602
+ }
603
+ }
604
+ }
605
+ async function exists(path) {
606
+ try {
607
+ await lstat(path);
608
+ return true;
609
+ }
610
+ catch (error) {
611
+ if (isCode(error, 'ENOENT'))
612
+ return false;
613
+ throw error;
614
+ }
615
+ }
616
+ function assertExact(value, keys) {
617
+ if (!isRecord(value))
618
+ throw new Error('workflow object is invalid');
619
+ const actual = Object.keys(value).sort(compareUtf8);
620
+ const expected = [...keys].sort(compareUtf8);
621
+ if (!same(actual, expected))
622
+ throw new Error('workflow object has unknown or missing keys');
623
+ }
624
+ function normalizePath(value) {
625
+ const path = value.normalize('NFC').replaceAll('\\', '/');
626
+ if (!path || path.startsWith('/') || path.split('/').some((part) => !part || part === '.' || part === '..'))
627
+ throw new Error('workflow path is invalid');
628
+ return path;
629
+ }
630
+ function canonicalJson(value) {
631
+ if (value === null || typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number')
632
+ return JSON.stringify(value);
633
+ if (Array.isArray(value))
634
+ return `[${value.map(canonicalJson).join(',')}]`;
635
+ if (isRecord(value))
636
+ return `{${Object.keys(value).sort(compareUtf8).map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
637
+ throw new Error('unsupported canonical workflow value');
638
+ }
639
+ function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
640
+ function same(left, right) { return left.length === right.length && left.every((value, index) => value === right[index]); }
641
+ function compareUtf8(left, right) { return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')); }
642
+ function sha(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
643
+ function isCode(error, code) { return typeof error === 'object' && error !== null && error.code === code; }
644
+ function runnerUid() { const uid = process.getuid?.(); if (uid === undefined)
645
+ throw new Error('POSIX ownership is required'); return uid; }
646
+ //# sourceMappingURL=workflow-assets.js.map