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.
- package/CHANGELOG.md +44 -426
- package/README.md +135 -34
- package/dist/src/index.d.ts +11 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/v2/acceptance-proof.d.ts +3 -0
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +2 -8
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.d.ts +5 -3
- package/dist/src/v2/adapters/gh-issue-adapter.d.ts.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.js +67 -12
- package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
- package/dist/src/v2/adapters/issues.d.ts +16 -2
- package/dist/src/v2/adapters/issues.d.ts.map +1 -1
- package/dist/src/v2/adapters/issues.js +15 -5
- package/dist/src/v2/adapters/issues.js.map +1 -1
- package/dist/src/v2/adapters/mission-coordinator-lock.d.ts +1 -0
- package/dist/src/v2/adapters/mission-coordinator-lock.d.ts.map +1 -1
- package/dist/src/v2/adapters/mission-coordinator-lock.js +5 -1
- package/dist/src/v2/adapters/mission-coordinator-lock.js.map +1 -1
- package/dist/src/v2/cli-contract.d.ts +3 -3
- package/dist/src/v2/cli-contract.d.ts.map +1 -1
- package/dist/src/v2/cli-contract.js +9 -1
- package/dist/src/v2/cli-contract.js.map +1 -1
- package/dist/src/v2/cli.d.ts +24 -0
- package/dist/src/v2/cli.d.ts.map +1 -0
- package/dist/src/v2/{candidate-cli.js → cli.js} +30 -26
- package/dist/src/v2/cli.js.map +1 -0
- package/dist/src/v2/code-review-report.d.ts +66 -0
- package/dist/src/v2/code-review-report.d.ts.map +1 -0
- package/dist/src/v2/code-review-report.js +259 -0
- package/dist/src/v2/code-review-report.js.map +1 -0
- package/dist/src/v2/codex-process.d.ts +8 -1
- package/dist/src/v2/codex-process.d.ts.map +1 -1
- package/dist/src/v2/codex-process.js +22 -0
- package/dist/src/v2/codex-process.js.map +1 -1
- package/dist/src/v2/config.d.ts +4 -3
- package/dist/src/v2/config.d.ts.map +1 -1
- package/dist/src/v2/config.js +8 -3
- package/dist/src/v2/config.js.map +1 -1
- package/dist/src/v2/contained-report-operation.d.ts +100 -0
- package/dist/src/v2/contained-report-operation.d.ts.map +1 -0
- package/dist/src/v2/contained-report-operation.js +200 -0
- package/dist/src/v2/contained-report-operation.js.map +1 -0
- package/dist/src/v2/containment.d.ts +10 -0
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +49 -1
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +96 -0
- package/dist/src/v2/direct-delivery.d.ts.map +1 -0
- package/dist/src/v2/direct-delivery.js +482 -0
- package/dist/src/v2/direct-delivery.js.map +1 -0
- package/dist/src/v2/immutable-workflow-publisher.d.ts +40 -0
- package/dist/src/v2/immutable-workflow-publisher.d.ts.map +1 -0
- package/dist/src/v2/immutable-workflow-publisher.js +218 -0
- package/dist/src/v2/immutable-workflow-publisher.js.map +1 -0
- package/dist/src/v2/implementation-reviewer.d.ts +81 -0
- package/dist/src/v2/implementation-reviewer.d.ts.map +1 -0
- package/dist/src/v2/implementation-reviewer.js +157 -0
- package/dist/src/v2/implementation-reviewer.js.map +1 -0
- package/dist/src/v2/owner-control-lock.d.ts +41 -0
- package/dist/src/v2/owner-control-lock.d.ts.map +1 -0
- package/dist/src/v2/owner-control-lock.js +174 -0
- package/dist/src/v2/owner-control-lock.js.map +1 -0
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +55 -29
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/route-continuations.d.ts +32 -0
- package/dist/src/v2/route-continuations.d.ts.map +1 -0
- package/dist/src/v2/route-continuations.js +2 -0
- package/dist/src/v2/route-continuations.js.map +1 -0
- package/dist/src/v2/route-coordinator.d.ts +77 -0
- package/dist/src/v2/route-coordinator.d.ts.map +1 -0
- package/dist/src/v2/route-coordinator.js +370 -0
- package/dist/src/v2/route-coordinator.js.map +1 -0
- package/dist/src/v2/route-decision.d.ts +129 -0
- package/dist/src/v2/route-decision.d.ts.map +1 -0
- package/dist/src/v2/route-decision.js +400 -0
- package/dist/src/v2/route-decision.js.map +1 -0
- package/dist/src/v2/run-issue.d.ts +64 -6
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +962 -92
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +25 -1
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +129 -4
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime-assets.d.ts +15 -13
- package/dist/src/v2/runtime-assets.d.ts.map +1 -1
- package/dist/src/v2/runtime-assets.js +263 -416
- package/dist/src/v2/runtime-assets.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +17 -9
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +564 -64
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/setup-cli.d.ts.map +1 -1
- package/dist/src/v2/setup-cli.js +4 -10
- package/dist/src/v2/setup-cli.js.map +1 -1
- package/dist/src/v2/setup-runtime.d.ts.map +1 -1
- package/dist/src/v2/setup-runtime.js +19 -131
- package/dist/src/v2/setup-runtime.js.map +1 -1
- package/dist/src/v2/setup-store.d.ts +0 -5
- package/dist/src/v2/setup-store.d.ts.map +1 -1
- package/dist/src/v2/setup-store.js +3 -106
- package/dist/src/v2/setup-store.js.map +1 -1
- package/dist/src/v2/setup.d.ts +6 -43
- package/dist/src/v2/setup.d.ts.map +1 -1
- package/dist/src/v2/setup.js +13 -192
- package/dist/src/v2/setup.js.map +1 -1
- package/dist/src/v2/spec-coordinator.d.ts +85 -0
- package/dist/src/v2/spec-coordinator.d.ts.map +1 -0
- package/dist/src/v2/spec-coordinator.js +88 -0
- package/dist/src/v2/spec-coordinator.js.map +1 -0
- package/dist/src/v2/spec-delivery.d.ts +143 -0
- package/dist/src/v2/spec-delivery.d.ts.map +1 -0
- package/dist/src/v2/spec-delivery.js +401 -0
- package/dist/src/v2/spec-delivery.js.map +1 -0
- package/dist/src/v2/triage-route.d.ts +68 -0
- package/dist/src/v2/triage-route.d.ts.map +1 -0
- package/dist/src/v2/triage-route.js +223 -0
- package/dist/src/v2/triage-route.js.map +1 -0
- package/dist/src/v2/waiting-human-coordinator.d.ts +49 -0
- package/dist/src/v2/waiting-human-coordinator.d.ts.map +1 -0
- package/dist/src/v2/waiting-human-coordinator.js +509 -0
- package/dist/src/v2/waiting-human-coordinator.js.map +1 -0
- package/dist/src/v2/waiting-human.d.ts +143 -0
- package/dist/src/v2/waiting-human.d.ts.map +1 -0
- package/dist/src/v2/waiting-human.js +408 -0
- package/dist/src/v2/waiting-human.js.map +1 -0
- package/dist/src/v2/workflow-assets.d.ts +98 -0
- package/dist/src/v2/workflow-assets.d.ts.map +1 -0
- package/dist/src/v2/workflow-assets.js +646 -0
- package/dist/src/v2/workflow-assets.js.map +1 -0
- package/docs/deep-dive.md +275 -52
- package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
- package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
- package/internal-workflow/docs/agents/coding-skill-routing.md +123 -0
- package/internal-workflow/docs/agents/confidence-rubric.md +65 -0
- package/internal-workflow/docs/agents/contract-test-ledger.md +60 -0
- package/internal-workflow/docs/agents/review-gates.md +42 -0
- package/internal-workflow/docs/agents/review-protocol.md +98 -0
- package/internal-workflow/docs/agents/tool-usage.md +88 -0
- package/internal-workflow/evals/coding-skill-evals.json +66 -0
- package/internal-workflow/manifest.json +1 -0
- package/internal-workflow/operations/acceptance-proof/SKILL.md +9 -0
- package/internal-workflow/operations/ambiguity-review/SKILL.md +5 -0
- package/internal-workflow/operations/code-review/SKILL.md +23 -0
- package/internal-workflow/operations/implementation/SKILL.md +24 -0
- package/internal-workflow/operations/spec-author/SKILL.md +12 -0
- package/internal-workflow/operations/spec-review/SKILL.md +12 -0
- package/internal-workflow/operations/triage/SKILL.md +12 -0
- package/internal-workflow/profiles/analyst_deep.toml +9 -0
- package/internal-workflow/profiles/implementer_standard.toml +9 -0
- package/internal-workflow/profiles/proof_agent.toml +8 -0
- package/internal-workflow/profiles/reviewer_deep.toml +9 -0
- package/internal-workflow/profiles/reviewer_standard.toml +9 -0
- package/internal-workflow/schemas/ambiguity-review-v1.json +1 -0
- package/internal-workflow/schemas/code-review-v1.json +1 -0
- package/internal-workflow/schemas/implementation-report-v1.json +1 -0
- package/internal-workflow/schemas/proof-report-v1.json +1 -0
- package/internal-workflow/schemas/spec-author-v1.json +1 -0
- package/internal-workflow/schemas/spec-review-v1.json +30 -0
- package/internal-workflow/schemas/triage-route-v1.json +1 -0
- package/internal-workflow/skills/acceptance-proof/agents/openai.yaml +6 -0
- package/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +6 -1
- package/internal-workflow/skills/agent-auto/agents/openai.yaml +6 -0
- package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
- package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
- package/internal-workflow/skills/code-review/SKILL.md +279 -0
- package/internal-workflow/skills/code-review/agents/openai.yaml +4 -0
- package/internal-workflow/skills/code-review/references/bug-classes.md +56 -0
- package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
- package/internal-workflow/skills/code-review/references/framework-lenses.md +34 -0
- package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -0
- package/internal-workflow/skills/diagnosing-bugs/SKILL.md +138 -0
- package/internal-workflow/skills/diagnosing-bugs/agents/openai.yaml +6 -0
- package/internal-workflow/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/internal-workflow/skills/implementation-spec-maker/SKILL.md +102 -0
- package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +6 -0
- package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +31 -0
- package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +146 -0
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +115 -0
- package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
- package/internal-workflow/skills/small-task-implementer/SKILL.md +104 -0
- package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
- package/internal-workflow/skills/spec-implementer/SKILL.md +126 -0
- package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
- package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
- package/internal-workflow/skills/spec-implementer/references/review-loop.md +94 -0
- package/internal-workflow/skills/tdd/SKILL.md +72 -0
- package/internal-workflow/skills/tdd/agents/openai.yaml +6 -0
- package/internal-workflow/skills/tdd/interface-design.md +31 -0
- package/internal-workflow/skills/tdd/mocking.md +59 -0
- package/internal-workflow/skills/tdd/refactoring.md +10 -0
- package/internal-workflow/skills/tdd/tests.md +77 -0
- package/internal-workflow/skills/triage/AGENT-BRIEF.md +192 -0
- package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/internal-workflow/skills/triage/SKILL.md +134 -0
- package/internal-workflow/skills/triage/agents/openai.yaml +6 -0
- package/package.json +14 -8
- package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
- package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
- package/dist/src/v2/adapters/target-activity-fence.js +0 -249
- package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
- package/dist/src/v2/candidate-cli.d.ts +0 -22
- package/dist/src/v2/candidate-cli.d.ts.map +0 -1
- package/dist/src/v2/candidate-cli.js.map +0 -1
- package/dist/src/v2/legacy-cutover.d.ts +0 -52
- package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
- package/dist/src/v2/legacy-cutover.js +0 -87
- package/dist/src/v2/legacy-cutover.js.map +0 -1
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/SKILL.md +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/android.md +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/browser.md +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/ios.md +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/android-lease.mjs +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/ios-lease.mjs +0 -0
package/dist/src/v2/runtime.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { lstat, mkdir, open, readFile, readdir, readlink, realpath } from 'node:fs/promises';
|
|
4
|
+
import { lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
|
|
5
5
|
import { homedir, hostname } from 'node:os';
|
|
6
|
-
import { dirname, isAbsolute, join, posix, resolve } from 'node:path';
|
|
6
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
|
|
7
7
|
import { writeDurableAtomicFile } from './adapters/durable-atomic-file.js';
|
|
8
8
|
import { GitWorktreeManager } from './adapters/worktree.js';
|
|
9
9
|
import { defaultProcessExecutor } from './adapters/command.js';
|
|
10
10
|
import { AcceptanceProof, ProofQuiescenceError } from './acceptance-proof.js';
|
|
11
|
-
import { acquireExclusiveJsonFileLock } from './atomic-store.js';
|
|
12
11
|
import { createCheckedChangeCapabilities } from './checked-change.js';
|
|
12
|
+
import { InjectedContainedReportOperation } from './contained-report-operation.js';
|
|
13
|
+
import { ContainedImplementationReviewer } from './implementation-reviewer.js';
|
|
13
14
|
import { parseAgentAutoConfig } from './config.js';
|
|
14
|
-
import {
|
|
15
|
+
import { WaitingHumanCoordinator } from './waiting-human-coordinator.js';
|
|
16
|
+
import { assertContainmentCertificateMatchesRuntime, canonicalJson, containmentArgvPolicySha256, containmentCertificatePath, parseJsonWithoutDuplicateKeys, readContainmentCertificate, sha256, } from './containment.js';
|
|
15
17
|
import { FileProofRecordWriter } from './proof-store.js';
|
|
18
|
+
import { acquireOwnerControlLock, OwnerControlLockBlockedError } from './owner-control-lock.js';
|
|
16
19
|
import { decodeAgentReportForValidation } from './report-envelope.js';
|
|
17
20
|
import { CodexProcess, ProcessQuiescenceError } from './codex-process.js';
|
|
18
21
|
import { FileAndroidLeaseVerifier, FileIosLeaseVerifier } from './mobile-lease.js';
|
|
19
22
|
import { publishRuntimeAssetSnapshot } from './runtime-assets.js';
|
|
20
|
-
import {
|
|
23
|
+
import { RouteCoordinator } from './route-coordinator.js';
|
|
24
|
+
import { SpecCoordinator } from './spec-coordinator.js';
|
|
25
|
+
import { createSpecRevision } from './spec-delivery.js';
|
|
26
|
+
import { validateCodeReviewDefects } from './code-review-report.js';
|
|
27
|
+
import { hashRouteDecision, validateRouteReceipt } from './route-decision.js';
|
|
28
|
+
import { OwnerLockContentionError, OwnerLockSafetyError, RunIssue } from './run-issue.js';
|
|
21
29
|
import { FileRunRecordWriter } from './run-store.js';
|
|
30
|
+
import { parseWorkflowExecutionProfile, verifyWorkflowGeneration, } from './workflow-assets.js';
|
|
22
31
|
export class LocalGitRunIssueAdapter {
|
|
23
32
|
executor;
|
|
24
33
|
worktrees;
|
|
@@ -101,6 +110,14 @@ export class LocalGitRunIssueAdapter {
|
|
|
101
110
|
listChangedFiles(worktreePath) {
|
|
102
111
|
return this.worktrees.listChangedFiles(worktreePath);
|
|
103
112
|
}
|
|
113
|
+
async fingerprintChangedFiles(worktreePath, changedFiles) {
|
|
114
|
+
const root = await realpath(worktreePath);
|
|
115
|
+
const entries = [];
|
|
116
|
+
for (const path of [...changedFiles].sort()) {
|
|
117
|
+
entries.push({ path, fingerprint: await fingerprintRepositoryPath(root, path) });
|
|
118
|
+
}
|
|
119
|
+
return sha256(canonicalJson(entries));
|
|
120
|
+
}
|
|
104
121
|
async stageAll(worktreePath) {
|
|
105
122
|
await this.git(['-C', worktreePath, 'add', '--all']);
|
|
106
123
|
}
|
|
@@ -167,13 +184,13 @@ export class ContainedImplementationAgent {
|
|
|
167
184
|
const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
|
|
168
185
|
const attemptId = (this.dependencies.createAttemptId ?? randomUUID)();
|
|
169
186
|
const attempt = await prepareContainedAttempt({
|
|
170
|
-
packageRoot: this.dependencies.packageRoot,
|
|
171
187
|
orchestratorHome: this.dependencies.orchestratorHome,
|
|
172
188
|
canonicalRepository,
|
|
173
189
|
runId: input.runId,
|
|
174
190
|
attemptId,
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
operationId: 'implementation',
|
|
192
|
+
workflowGeneration: input.workflowGeneration,
|
|
193
|
+
bootId: this.dependencies.bootId,
|
|
177
194
|
});
|
|
178
195
|
const baseline = await this.dependencies.git.snapshot(input.worktreePath);
|
|
179
196
|
try {
|
|
@@ -188,7 +205,9 @@ export class ContainedImplementationAgent {
|
|
|
188
205
|
parentCodexHome: this.dependencies.parentCodexHome,
|
|
189
206
|
parentEnv: process.env,
|
|
190
207
|
prompt: [
|
|
191
|
-
`
|
|
208
|
+
`Package profile instructions: ${attempt.profile.developerInstructions}`,
|
|
209
|
+
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
210
|
+
`The operation's immutable workflow root is ${attempt.workflowRoot}.`,
|
|
192
211
|
`Implement issue #${input.issue.number}: ${input.issue.title}`,
|
|
193
212
|
`Implementation cycle: ${input.cycle}.`,
|
|
194
213
|
`Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
|
|
@@ -198,6 +217,8 @@ export class ContainedImplementationAgent {
|
|
|
198
217
|
].join('\n'),
|
|
199
218
|
timeoutMs: config.codex.timeoutMs,
|
|
200
219
|
idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
220
|
+
operationPolicy: attempt.policy,
|
|
221
|
+
executionProfile: attempt.profile,
|
|
201
222
|
}, input.signal);
|
|
202
223
|
if (result.kind === 'cancelled')
|
|
203
224
|
return { kind: 'cancelled' };
|
|
@@ -208,6 +229,7 @@ export class ContainedImplementationAgent {
|
|
|
208
229
|
return { kind: 'internal-error' };
|
|
209
230
|
return {
|
|
210
231
|
kind: 'completed',
|
|
232
|
+
attemptId,
|
|
211
233
|
report: decodeAgentReportForValidation(result.report.bytes),
|
|
212
234
|
};
|
|
213
235
|
}
|
|
@@ -233,20 +255,22 @@ export class ContainedProofAgent {
|
|
|
233
255
|
this.dependencies = dependencies;
|
|
234
256
|
}
|
|
235
257
|
async run(input) {
|
|
258
|
+
if (!input.workflowGeneration)
|
|
259
|
+
throw new Error('proof workflow generation is required');
|
|
236
260
|
const config = this.dependencies.config();
|
|
237
261
|
const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
|
|
238
262
|
const worktreePath = resolve(this.dependencies.targetRoot, config.runner.workspaceRoot, `issue-${input.issue.number}`);
|
|
239
263
|
const attempt = await prepareContainedAttempt({
|
|
240
|
-
packageRoot: this.dependencies.packageRoot,
|
|
241
264
|
orchestratorHome: this.dependencies.orchestratorHome,
|
|
242
265
|
canonicalRepository,
|
|
243
266
|
runId: input.runId,
|
|
244
267
|
attemptId: (this.dependencies.createAttemptId ?? randomUUID)(),
|
|
245
|
-
|
|
246
|
-
|
|
268
|
+
operationId: 'acceptance-proof',
|
|
269
|
+
workflowGeneration: input.workflowGeneration,
|
|
270
|
+
bootId: this.dependencies.bootId,
|
|
247
271
|
});
|
|
248
272
|
const artifactRoot = resolve(worktreePath, config.proof.artifactDir);
|
|
249
|
-
const snapshotRoot = dirname(attempt.
|
|
273
|
+
const snapshotRoot = dirname(attempt.sourceSkillPath ?? attempt.operationPath);
|
|
250
274
|
const androidLeaseRoot = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(canonicalRepository), 'leases');
|
|
251
275
|
const androidLeaseArtifact = join(artifactRoot, input.proofId, 'android-lease.json');
|
|
252
276
|
const iosLeaseRoot = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(canonicalRepository), 'leases');
|
|
@@ -265,7 +289,9 @@ export class ContainedProofAgent {
|
|
|
265
289
|
parentCodexHome: this.dependencies.parentCodexHome,
|
|
266
290
|
parentEnv: process.env,
|
|
267
291
|
prompt: [
|
|
268
|
-
`
|
|
292
|
+
`Package profile instructions: ${attempt.profile.developerInstructions}`,
|
|
293
|
+
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
294
|
+
`The operation's immutable workflow root is ${attempt.workflowRoot}.`,
|
|
269
295
|
`Independently prove issue #${input.issue.number}.`,
|
|
270
296
|
`Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
|
|
271
297
|
`Checked change digest: ${input.checkedChangeSha256}.`,
|
|
@@ -296,6 +322,8 @@ export class ContainedProofAgent {
|
|
|
296
322
|
].join('\n'),
|
|
297
323
|
timeoutMs: config.codex.timeoutMs,
|
|
298
324
|
idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
325
|
+
operationPolicy: attempt.policy,
|
|
326
|
+
executionProfile: attempt.profile,
|
|
299
327
|
}, input.signal);
|
|
300
328
|
if (result.kind === 'cancelled')
|
|
301
329
|
return { kind: 'cancelled' };
|
|
@@ -339,11 +367,10 @@ export function createV2Runtime(input) {
|
|
|
339
367
|
const iosXcrunPath = resolve(input.iosXcrunPath ?? '/usr/bin/xcrun');
|
|
340
368
|
const containedDependencies = () => ({
|
|
341
369
|
config: () => requireConfig(currentConfig),
|
|
342
|
-
packageRoot: requireRuntimeString(input.packageRoot, 'packageRoot'),
|
|
343
370
|
orchestratorHome,
|
|
344
371
|
parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
|
|
345
372
|
safePath: requireRuntimeString(input.safePath, 'safePath'),
|
|
346
|
-
|
|
373
|
+
bootId: input.bootId,
|
|
347
374
|
process: containedProcess,
|
|
348
375
|
createAttemptId: input.createAttemptId,
|
|
349
376
|
});
|
|
@@ -359,6 +386,292 @@ export function createV2Runtime(input) {
|
|
|
359
386
|
iosXcrunPath,
|
|
360
387
|
processExecutor: commandExecutor,
|
|
361
388
|
});
|
|
389
|
+
const reportOperation = new InjectedContainedReportOperation({
|
|
390
|
+
prepare: async ({ operation, attemptId, runId, workflowGeneration }) => ({
|
|
391
|
+
operation,
|
|
392
|
+
generationHash: workflowGeneration.generationHash,
|
|
393
|
+
...await prepareContainedAttempt({
|
|
394
|
+
orchestratorHome,
|
|
395
|
+
canonicalRepository: requireCanonicalRepository(currentConfig),
|
|
396
|
+
runId,
|
|
397
|
+
attemptId,
|
|
398
|
+
operationId: operation,
|
|
399
|
+
workflowGeneration,
|
|
400
|
+
bootId: input.bootId,
|
|
401
|
+
}),
|
|
402
|
+
}),
|
|
403
|
+
snapshot: (worktreePath) => git.snapshot(worktreePath),
|
|
404
|
+
launch: async ({ attempt, worktreePath, promptFacts, signal, onLaunched }) => {
|
|
405
|
+
const config = requireConfig(currentConfig);
|
|
406
|
+
if (!attempt.schemaPath || !attempt.reportPath || !attempt.toolHome || !attempt.tmpDir
|
|
407
|
+
|| !attempt.profile || !attempt.operationPath || !attempt.workflowRoot) {
|
|
408
|
+
return { status: 'blocked', kind: 'safety', code: 'report-operation-attempt-incomplete' };
|
|
409
|
+
}
|
|
410
|
+
let readView;
|
|
411
|
+
try {
|
|
412
|
+
readView = await materializeReportReadView({
|
|
413
|
+
worktreePath,
|
|
414
|
+
destination: join(dirname(attempt.tmpDir), 'read-view'),
|
|
415
|
+
deniedPaths: config.deny.readPaths,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return { status: 'blocked', kind: 'safety', code: 'report-operation-read-view-failed' };
|
|
420
|
+
}
|
|
421
|
+
let result;
|
|
422
|
+
let cleanupAfterSafeHalt = false;
|
|
423
|
+
try {
|
|
424
|
+
result = await containedProcess.run({
|
|
425
|
+
codexPath: config.codex.command,
|
|
426
|
+
cwd: readView,
|
|
427
|
+
schemaPath: attempt.schemaPath,
|
|
428
|
+
reportPath: attempt.reportPath,
|
|
429
|
+
toolHome: attempt.toolHome,
|
|
430
|
+
tmpDir: attempt.tmpDir,
|
|
431
|
+
safePath: requireRuntimeString(input.safePath, 'safePath'),
|
|
432
|
+
parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
|
|
433
|
+
parentEnv: process.env,
|
|
434
|
+
prompt: [
|
|
435
|
+
`Package profile instructions: ${attempt.profile.developerInstructions}`,
|
|
436
|
+
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
437
|
+
`The operation's immutable workflow root is ${attempt.workflowRoot}.`,
|
|
438
|
+
`Runner-provided facts: ${canonicalJson(promptFacts)}`,
|
|
439
|
+
'This is a read-only, report-only operation. Do not edit files or external state, use network or MCP tools, or request additional authority.',
|
|
440
|
+
'Do not read .env or any .env* file. The runner has removed repository credential paths from this read view.',
|
|
441
|
+
].join('\n'),
|
|
442
|
+
timeoutMs: config.codex.timeoutMs,
|
|
443
|
+
idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
444
|
+
operationPolicy: attempt.policy,
|
|
445
|
+
executionProfile: attempt.profile,
|
|
446
|
+
...(onLaunched ? { onSpawned: onLaunched } : {}),
|
|
447
|
+
}, signal);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (error instanceof ProcessQuiescenceError) {
|
|
451
|
+
cleanupAfterSafeHalt = true;
|
|
452
|
+
return {
|
|
453
|
+
status: 'safe-halt',
|
|
454
|
+
pid: error.pid,
|
|
455
|
+
processGroupId: error.processGroupId,
|
|
456
|
+
startedAt: now(),
|
|
457
|
+
waitForAbsence: async () => {
|
|
458
|
+
await waitForProcessGroupAbsent(error.processGroupId);
|
|
459
|
+
await rm(readView, { recursive: true, force: true });
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
if (!cleanupAfterSafeHalt)
|
|
467
|
+
await rm(readView, { recursive: true, force: true });
|
|
468
|
+
}
|
|
469
|
+
if (result.kind === 'cancelled')
|
|
470
|
+
return { status: 'cancelled' };
|
|
471
|
+
if (result.kind === 'launch-gate-failed') {
|
|
472
|
+
return { status: 'blocked', kind: 'safety', code: 'review-operation-launch-persistence-failed' };
|
|
473
|
+
}
|
|
474
|
+
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
|
|
475
|
+
return { status: 'retryable', code: `report-operation-${result.kind}` };
|
|
476
|
+
}
|
|
477
|
+
if (result.kind !== 'completed' || result.report.kind !== 'available') {
|
|
478
|
+
return { status: 'blocked', kind: 'external', code: 'report-operation-report-unavailable' };
|
|
479
|
+
}
|
|
480
|
+
return { status: 'completed', reportBytes: result.report.bytes };
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
const implementationReviewer = new ContainedImplementationReviewer({
|
|
484
|
+
operation: reportOperation,
|
|
485
|
+
createAttemptId: input.createAttemptId ?? randomUUID,
|
|
486
|
+
});
|
|
487
|
+
const createAttemptId = input.createAttemptId ?? randomUUID;
|
|
488
|
+
const specOperation = {
|
|
489
|
+
author: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
|
|
490
|
+
const attemptId = createAttemptId();
|
|
491
|
+
const sessionId = state.authorSessionId ?? randomUUID();
|
|
492
|
+
let attempt;
|
|
493
|
+
try {
|
|
494
|
+
attempt = await prepareContainedAttempt({
|
|
495
|
+
orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
|
|
496
|
+
attemptId, operationId: 'spec-author', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
|
|
497
|
+
});
|
|
498
|
+
const revisionPath = join(dirname(attempt.reportPath), `revision-${state.revisions.length + 1}.md`);
|
|
499
|
+
await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath, revisionPath });
|
|
500
|
+
const config = requireConfig(currentConfig);
|
|
501
|
+
const result = await containedProcess.run({
|
|
502
|
+
codexPath: config.codex.command, cwd: dirname(attempt.reportPath), schemaPath: attempt.schemaPath,
|
|
503
|
+
reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
|
|
504
|
+
safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
|
|
505
|
+
parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
506
|
+
operationPolicy: attempt.policy, executionProfile: attempt.profile,
|
|
507
|
+
onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
|
|
508
|
+
prompt: [
|
|
509
|
+
`Package profile instructions: ${attempt.profile.developerInstructions}`,
|
|
510
|
+
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
511
|
+
`The immutable workflow root is ${attempt.workflowRoot}.`,
|
|
512
|
+
`Author mode: ${mode}. Issue authority: ${canonicalJson(context.issue)}.`,
|
|
513
|
+
`Frozen criteria: ${canonicalJson(context.frozenCriteria)}.`,
|
|
514
|
+
`Prior revisions and review state: ${canonicalJson({ revisions: state.revisions, review: state.review })}.`,
|
|
515
|
+
`Write the complete new immutable revision only to ${revisionPath}. Return that exact absolute path and its SHA-256 in the report.`,
|
|
516
|
+
'Do not modify the product worktree, prior revisions, external state, or any .env file.',
|
|
517
|
+
].join('\n'),
|
|
518
|
+
}, signal);
|
|
519
|
+
if (result.kind === 'cancelled')
|
|
520
|
+
return { status: 'cancelled' };
|
|
521
|
+
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
|
|
522
|
+
return { status: 'retryable', code: `spec-author-${result.kind}` };
|
|
523
|
+
if (result.kind !== 'completed' || result.report.kind !== 'available')
|
|
524
|
+
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
525
|
+
const report = decodeAgentReportForValidation(result.report.bytes);
|
|
526
|
+
if (report.status !== 'ready' || report.specPath !== revisionPath || report.specSha256 === null)
|
|
527
|
+
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
528
|
+
const content = await readRegularFile(revisionPath);
|
|
529
|
+
if (report.specSha256 !== sha256(content))
|
|
530
|
+
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
531
|
+
const previous = state.revisions.at(-1) ?? null;
|
|
532
|
+
return { status: 'completed', value: createSpecRevision({
|
|
533
|
+
revision: state.revisions.length + 1, path: revisionPath, content: content.toString('utf8'),
|
|
534
|
+
evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
|
|
535
|
+
author: { attemptId, sessionId }, previousRevision: previous,
|
|
536
|
+
}) };
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (error instanceof ProcessQuiescenceError) {
|
|
540
|
+
try {
|
|
541
|
+
await waitForProcessGroupAbsent(error.processGroupId);
|
|
542
|
+
return { status: 'retryable', code: 'spec-author-process-quiescence' };
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return { status: 'blocked', kind: 'safety', code: 'spec-author-process-absence-unconfirmed' };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
review: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
|
|
552
|
+
const attemptId = createAttemptId();
|
|
553
|
+
const sessionId = state.review.reviewer?.sessionId ?? randomUUID();
|
|
554
|
+
try {
|
|
555
|
+
const attempt = await prepareContainedAttempt({
|
|
556
|
+
orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
|
|
557
|
+
attemptId, operationId: 'spec-review', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
|
|
558
|
+
});
|
|
559
|
+
await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath });
|
|
560
|
+
const config = requireConfig(currentConfig);
|
|
561
|
+
const result = await containedProcess.run({
|
|
562
|
+
codexPath: config.codex.command, cwd: context.worktreePath, schemaPath: attempt.schemaPath,
|
|
563
|
+
reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
|
|
564
|
+
safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
|
|
565
|
+
parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
566
|
+
operationPolicy: attempt.policy, executionProfile: attempt.profile,
|
|
567
|
+
onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
|
|
568
|
+
prompt: [
|
|
569
|
+
`Package profile instructions: ${attempt.profile.developerInstructions}`,
|
|
570
|
+
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
571
|
+
`Reviewer session ID: ${sessionId}. Review mode: ${mode}.`,
|
|
572
|
+
`Issue authority and frozen criteria: ${canonicalJson({ issue: context.issue, frozenCriteria: context.frozenCriteria })}.`,
|
|
573
|
+
`Immutable spec delivery state: ${canonicalJson(state)}.`,
|
|
574
|
+
'Return only the package spec-review report. Do not edit files or external state.',
|
|
575
|
+
].join('\n'),
|
|
576
|
+
}, signal);
|
|
577
|
+
if (result.kind === 'cancelled')
|
|
578
|
+
return { status: 'cancelled' };
|
|
579
|
+
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
|
|
580
|
+
return { status: 'retryable', code: `spec-review-${result.kind}` };
|
|
581
|
+
if (result.kind !== 'completed' || result.report.kind !== 'available')
|
|
582
|
+
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
583
|
+
const raw = decodeAgentReportForValidation(result.report.bytes);
|
|
584
|
+
if (raw.mode !== mode || raw.reviewerSessionId !== sessionId || !Array.isArray(raw.coverage) || !Array.isArray(raw.defects)
|
|
585
|
+
|| !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts) || !Array.isArray(raw.acceptedRisks)
|
|
586
|
+
|| typeof raw.coverageInvalidated !== 'boolean'
|
|
587
|
+
|| !['approved', 'needs-work', 'rejected'].includes(raw.verdict))
|
|
588
|
+
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
589
|
+
const target = state.revisions.at(-1);
|
|
590
|
+
const defects = validateCodeReviewDefects(raw.defects, target.revision);
|
|
591
|
+
const report = {
|
|
592
|
+
version: 1, targetRevision: target.revision, targetSha256: target.revisionSha256, mode,
|
|
593
|
+
verdict: raw.verdict, reviewer: { attemptId, sessionId },
|
|
594
|
+
coverage: raw.coverage, defects,
|
|
595
|
+
affectedDefectIds: raw.affectedDefectIds,
|
|
596
|
+
affectedContracts: raw.affectedContracts,
|
|
597
|
+
closureRequestSha256: mode === 'closure' ? state.review.closureRequestSha256 : null,
|
|
598
|
+
acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
|
|
599
|
+
};
|
|
600
|
+
return { status: 'completed', value: report, reportSha256: sha256(result.report.bytes) };
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
if (error instanceof ProcessQuiescenceError) {
|
|
604
|
+
try {
|
|
605
|
+
await waitForProcessGroupAbsent(error.processGroupId);
|
|
606
|
+
return { status: 'retryable', code: 'spec-review-process-quiescence' };
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return { status: 'blocked', kind: 'safety', code: 'spec-review-process-absence-unconfirmed' };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
recover: async ({ context, state, signal }) => {
|
|
616
|
+
const invocation = state.invocation;
|
|
617
|
+
if (!invocation || signal.aborted)
|
|
618
|
+
return signal.aborted ? { status: 'cancelled' } : { status: 'blocked', kind: 'safety', code: 'spec-recovery-invocation-missing' };
|
|
619
|
+
if (invocation.status === 'launched') {
|
|
620
|
+
try {
|
|
621
|
+
await waitForProcessGroupAbsent(invocation.processGroupId);
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return { status: 'blocked', kind: 'safety', code: 'spec-process-absence-unconfirmed' };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (!invocation.reportPath)
|
|
628
|
+
return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
|
|
629
|
+
let reportBytes;
|
|
630
|
+
try {
|
|
631
|
+
reportBytes = await readRegularFile(invocation.reportPath);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
|
|
635
|
+
}
|
|
636
|
+
if (invocation.purpose === 'author') {
|
|
637
|
+
if (!invocation.revisionPath)
|
|
638
|
+
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
639
|
+
try {
|
|
640
|
+
const raw = decodeAgentReportForValidation(reportBytes);
|
|
641
|
+
const content = await readRegularFile(invocation.revisionPath);
|
|
642
|
+
if (raw.status !== 'ready' || raw.specPath !== invocation.revisionPath || raw.specSha256 !== sha256(content))
|
|
643
|
+
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
644
|
+
return { status: 'completed', value: createSpecRevision({
|
|
645
|
+
revision: state.revisions.length + 1, path: invocation.revisionPath, content: content.toString('utf8'),
|
|
646
|
+
evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
|
|
647
|
+
author: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, previousRevision: state.revisions.at(-1) ?? null,
|
|
648
|
+
}) };
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const raw = decodeAgentReportForValidation(reportBytes);
|
|
656
|
+
if (raw.mode !== invocation.mode || raw.reviewerSessionId !== invocation.sessionId || !Array.isArray(raw.coverage)
|
|
657
|
+
|| !Array.isArray(raw.defects) || !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts)
|
|
658
|
+
|| typeof raw.coverageInvalidated !== 'boolean' || !['approved', 'needs-work', 'rejected'].includes(raw.verdict)) {
|
|
659
|
+
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
660
|
+
}
|
|
661
|
+
const defects = validateCodeReviewDefects(raw.defects, invocation.targetRevision);
|
|
662
|
+
return { status: 'completed', reportSha256: sha256(reportBytes), value: {
|
|
663
|
+
version: 1, targetRevision: invocation.targetRevision, targetSha256: invocation.targetSha256,
|
|
664
|
+
mode: invocation.mode, verdict: raw.verdict,
|
|
665
|
+
reviewer: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, coverage: raw.coverage, defects,
|
|
666
|
+
affectedDefectIds: raw.affectedDefectIds, affectedContracts: raw.affectedContracts,
|
|
667
|
+
closureRequestSha256: invocation.closureRequestSha256, acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
|
|
668
|
+
} };
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
};
|
|
362
675
|
const readConfig = async (requestedRoot) => {
|
|
363
676
|
if (resolve(requestedRoot) !== targetRoot)
|
|
364
677
|
throw new Error('runtime target root mismatch');
|
|
@@ -431,14 +744,13 @@ export function createV2Runtime(input) {
|
|
|
431
744
|
readConfig,
|
|
432
745
|
validateContainment: async (config) => {
|
|
433
746
|
const certificate = await readContainmentCertificate(containmentCertificatePath(orchestratorHome));
|
|
434
|
-
if (certificate.packageVersion !== input.packageVersion)
|
|
435
|
-
throw new Error('containment package version mismatch');
|
|
436
|
-
if (certificate.argvPolicySha256 !== containmentArgvPolicySha256())
|
|
437
|
-
throw new Error('containment argv policy mismatch');
|
|
438
747
|
const version = await commandExecutor(config.codex.command, ['--version']);
|
|
439
|
-
if (version.exitCode !== 0
|
|
440
|
-
throw new Error('
|
|
441
|
-
|
|
748
|
+
if (version.exitCode !== 0)
|
|
749
|
+
throw new Error('Codex version is unavailable');
|
|
750
|
+
assertContainmentCertificateMatchesRuntime(certificate, {
|
|
751
|
+
codexVersion: version.stdout.trim(),
|
|
752
|
+
argvPolicySha256: containmentArgvPolicySha256(),
|
|
753
|
+
});
|
|
442
754
|
},
|
|
443
755
|
ownerLock: {
|
|
444
756
|
acquire: async ({ canonicalRepository }) => acquireOwnerLock({
|
|
@@ -475,7 +787,7 @@ export function createV2Runtime(input) {
|
|
|
475
787
|
removeLabels: current.filter((label) => !labels.includes(label)),
|
|
476
788
|
});
|
|
477
789
|
},
|
|
478
|
-
postComment: (issueNumber, body) => input.issues.postComment(issueNumber, body),
|
|
790
|
+
postComment: async (issueNumber, body) => { await input.issues.postComment(issueNumber, body); },
|
|
479
791
|
},
|
|
480
792
|
pullRequests: {
|
|
481
793
|
findOpen: async ({ headBranch, baseBranch }) => {
|
|
@@ -489,7 +801,51 @@ export function createV2Runtime(input) {
|
|
|
489
801
|
createDraft: async ({ title, body, headBranch, baseBranch }) => input.pullRequests.createDraftPullRequest({ title, body, headBranch, baseBranch }),
|
|
490
802
|
},
|
|
491
803
|
git,
|
|
804
|
+
routeCoordinator: {
|
|
805
|
+
run: ({ state, ...routeInput }) => new RouteCoordinator({
|
|
806
|
+
state,
|
|
807
|
+
operation: reportOperation,
|
|
808
|
+
createAttemptId: input.createAttemptId ?? randomUUID,
|
|
809
|
+
now,
|
|
810
|
+
createReceipt: ({ artifact, triage, review, decidedAt }) => {
|
|
811
|
+
if (artifact.status === 'blocked')
|
|
812
|
+
throw new Error('blocked triage cannot create a route receipt');
|
|
813
|
+
const receipt = {
|
|
814
|
+
version: 1,
|
|
815
|
+
route: artifact.status,
|
|
816
|
+
triage,
|
|
817
|
+
review,
|
|
818
|
+
artifact,
|
|
819
|
+
decisionSha256: '',
|
|
820
|
+
decidedAt,
|
|
821
|
+
assumptions: [...artifact.assumptions],
|
|
822
|
+
};
|
|
823
|
+
receipt.decisionSha256 = hashRouteDecision(receipt);
|
|
824
|
+
return validateRouteReceipt(receipt, triage.generationHash);
|
|
825
|
+
},
|
|
826
|
+
}).run(routeInput),
|
|
827
|
+
},
|
|
828
|
+
routeContinuations: {
|
|
829
|
+
direct: async () => ({ status: 'completed' }),
|
|
830
|
+
specRequired: (context, state, signal) => new SpecCoordinator({ state, operation: specOperation }).run(context, signal),
|
|
831
|
+
awaitingUser: async (context, state, signal) => {
|
|
832
|
+
const config = requireConfig(currentConfig);
|
|
833
|
+
return new WaitingHumanCoordinator({
|
|
834
|
+
issues: input.issues,
|
|
835
|
+
labels: {
|
|
836
|
+
auto: config.github.labels.auto.name,
|
|
837
|
+
running: config.github.labels.running.name,
|
|
838
|
+
blocked: config.github.labels.blocked.name,
|
|
839
|
+
review: config.github.labels.review.name,
|
|
840
|
+
waitingHuman: config.github.labels.waitingHuman.name,
|
|
841
|
+
},
|
|
842
|
+
now,
|
|
843
|
+
}).run(context, state, signal);
|
|
844
|
+
},
|
|
845
|
+
},
|
|
492
846
|
implementationAgent,
|
|
847
|
+
implementationReviewer,
|
|
848
|
+
waitForReviewProcessAbsence: waitForProcessGroupAbsent,
|
|
493
849
|
checks: {
|
|
494
850
|
run: async ({ command, cwd, signal }) => runShellCheck(command, cwd, signal),
|
|
495
851
|
},
|
|
@@ -503,9 +859,11 @@ export function createV2Runtime(input) {
|
|
|
503
859
|
return { id: `evidence:${runId}:${code}`, path: relativePath };
|
|
504
860
|
},
|
|
505
861
|
packageVersion: input.packageVersion,
|
|
506
|
-
|
|
862
|
+
createWorkflowGeneration: input.createWorkflowGeneration,
|
|
863
|
+
verifyWorkflowGeneration,
|
|
507
864
|
createRunId: input.createRunId ?? randomUUID,
|
|
508
865
|
createProofId: input.createProofId ?? randomUUID,
|
|
866
|
+
createReviewSessionId: input.createAttemptId ?? randomUUID,
|
|
509
867
|
now,
|
|
510
868
|
signal: controller.signal,
|
|
511
869
|
});
|
|
@@ -522,48 +880,27 @@ export function createV2Runtime(input) {
|
|
|
522
880
|
};
|
|
523
881
|
}
|
|
524
882
|
async function acquireOwnerLock(input) {
|
|
525
|
-
const token = randomUUID();
|
|
526
|
-
const host = hostname();
|
|
527
|
-
const record = {
|
|
528
|
-
version: 1,
|
|
529
|
-
token,
|
|
530
|
-
canonicalRepository: input.canonicalRepository,
|
|
531
|
-
host,
|
|
532
|
-
bootId: input.bootId,
|
|
533
|
-
pid: process.pid,
|
|
534
|
-
acquiredAt: input.now(),
|
|
535
|
-
};
|
|
536
|
-
const repoKey = sha256(input.canonicalRepository);
|
|
537
883
|
try {
|
|
538
|
-
return await
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
884
|
+
return await acquireOwnerControlLock({
|
|
885
|
+
orchestratorHome: input.orchestratorHome,
|
|
886
|
+
canonicalRepository: input.canonicalRepository,
|
|
887
|
+
bootId: input.bootId,
|
|
888
|
+
host: hostname(),
|
|
889
|
+
pid: process.pid,
|
|
890
|
+
now: input.now,
|
|
891
|
+
createToken: randomUUID,
|
|
892
|
+
processAlive: input.processAlive,
|
|
545
893
|
});
|
|
546
894
|
}
|
|
547
895
|
catch (error) {
|
|
548
|
-
if (error instanceof
|
|
896
|
+
if (error instanceof OwnerControlLockBlockedError) {
|
|
897
|
+
if (error.kind === 'live-contention')
|
|
898
|
+
throw new OwnerLockContentionError(error.message);
|
|
549
899
|
throw new OwnerLockSafetyError(error.message);
|
|
900
|
+
}
|
|
550
901
|
throw error;
|
|
551
902
|
}
|
|
552
903
|
}
|
|
553
|
-
function parseOwnerLock(value) {
|
|
554
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
555
|
-
throw new Error('owner lock is invalid');
|
|
556
|
-
const keys = Object.keys(value).sort();
|
|
557
|
-
const expected = ['version', 'token', 'canonicalRepository', 'host', 'bootId', 'pid', 'acquiredAt'].sort();
|
|
558
|
-
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index]))
|
|
559
|
-
throw new Error('owner lock keys are invalid');
|
|
560
|
-
const record = value;
|
|
561
|
-
if (record.version !== 1 || !record.token || !record.canonicalRepository || !record.host || !record.bootId
|
|
562
|
-
|| !Number.isSafeInteger(record.pid) || record.pid <= 0 || Number.isNaN(Date.parse(record.acquiredAt))) {
|
|
563
|
-
throw new Error('owner lock fields are invalid');
|
|
564
|
-
}
|
|
565
|
-
return record;
|
|
566
|
-
}
|
|
567
904
|
async function readRegularFile(path) {
|
|
568
905
|
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
569
906
|
try {
|
|
@@ -647,6 +984,10 @@ function requireConfig(config) {
|
|
|
647
984
|
throw new Error('runtime config is unavailable');
|
|
648
985
|
return config;
|
|
649
986
|
}
|
|
987
|
+
function requireCanonicalRepository(config) {
|
|
988
|
+
const value = requireConfig(config);
|
|
989
|
+
return `${value.github.owner.toLowerCase()}/${value.github.repo.toLowerCase()}`;
|
|
990
|
+
}
|
|
650
991
|
function requireRuntimeString(value, field) {
|
|
651
992
|
if (!value)
|
|
652
993
|
throw new Error(`${field} is required for contained Codex agents`);
|
|
@@ -721,13 +1062,28 @@ async function prepareContainedAttempt(input) {
|
|
|
721
1062
|
const runtimeRoot = join(resolve(input.orchestratorHome), 'v2', sha256(input.canonicalRepository));
|
|
722
1063
|
const attemptRelativePath = `runs/${input.runId}/attempts/${input.attemptId}`;
|
|
723
1064
|
const snapshot = await publishRuntimeAssetSnapshot({
|
|
724
|
-
|
|
1065
|
+
workflowGeneration: input.workflowGeneration,
|
|
725
1066
|
runtimeRoot,
|
|
726
1067
|
snapshotRelativePath: `${attemptRelativePath}/snapshot`,
|
|
727
|
-
|
|
1068
|
+
operation: input.operationId,
|
|
1069
|
+
bootId: input.bootId,
|
|
728
1070
|
});
|
|
729
|
-
|
|
730
|
-
|
|
1071
|
+
const reportOnly = input.operationId === 'triage' || input.operationId === 'ambiguity-review'
|
|
1072
|
+
|| input.operationId === 'code-review' || input.operationId === 'spec-review';
|
|
1073
|
+
if ((reportOnly
|
|
1074
|
+
? snapshot.policy.sandboxMode !== 'read-only'
|
|
1075
|
+
|| snapshot.policy.worktreeAccess !== 'read-only'
|
|
1076
|
+
|| snapshot.policy.runnerPostcondition !== 'report-only'
|
|
1077
|
+
|| snapshot.policy.writableRootClasses.length !== 0
|
|
1078
|
+
: snapshot.policy.sandboxMode !== 'workspace-write')
|
|
1079
|
+
|| snapshot.policy.approvalCeiling !== 'never'
|
|
1080
|
+
|| snapshot.policy.network !== 'deny'
|
|
1081
|
+
|| snapshot.policy.networkHosts.length !== 0
|
|
1082
|
+
|| snapshot.policy.mcpTools.length !== 0
|
|
1083
|
+
|| snapshot.policy.externalWrite !== false) {
|
|
1084
|
+
throw new Error(`workflow operation policy exceeds containment: ${input.operationId}`);
|
|
1085
|
+
}
|
|
1086
|
+
const profile = parseWorkflowExecutionProfile(await readFile(snapshot.profilePath, 'utf8'), snapshot.policy);
|
|
731
1087
|
const attemptRoot = join(runtimeRoot, attemptRelativePath);
|
|
732
1088
|
const toolHome = join(attemptRoot, 'tool-home');
|
|
733
1089
|
const tmpDir = join(attemptRoot, 'tmp');
|
|
@@ -740,11 +1096,15 @@ async function prepareContainedAttempt(input) {
|
|
|
740
1096
|
throw error;
|
|
741
1097
|
});
|
|
742
1098
|
return {
|
|
743
|
-
|
|
1099
|
+
workflowRoot: snapshot.snapshotRoot,
|
|
1100
|
+
operationPath: snapshot.operationPath,
|
|
1101
|
+
sourceSkillPath: snapshot.sourceSkillPath,
|
|
744
1102
|
schemaPath: snapshot.schemaPath,
|
|
745
1103
|
reportPath: join(attemptRoot, 'report.json'),
|
|
746
1104
|
toolHome,
|
|
747
1105
|
tmpDir,
|
|
1106
|
+
policy: structuredClone(snapshot.policy),
|
|
1107
|
+
profile,
|
|
748
1108
|
};
|
|
749
1109
|
}
|
|
750
1110
|
async function artifactInventory(root, logicalRoot) {
|
|
@@ -832,6 +1192,146 @@ async function waitForProcessGroupAbsent(processGroupId) {
|
|
|
832
1192
|
await new Promise((resolveWait) => setTimeout(resolveWait, 1_000));
|
|
833
1193
|
}
|
|
834
1194
|
}
|
|
1195
|
+
export async function materializeReportReadView(input) {
|
|
1196
|
+
await assertDisjointReadViewPaths(input.worktreePath, input.destination);
|
|
1197
|
+
const deniedPaths = normalizeReadViewDeniedPaths(input.deniedPaths);
|
|
1198
|
+
await rm(input.destination, { recursive: true, force: true });
|
|
1199
|
+
await mkdir(input.destination, { recursive: true, mode: 0o700 });
|
|
1200
|
+
await materializeCurrentReadView(input.worktreePath, input.destination, deniedPaths);
|
|
1201
|
+
await scrubReportReadView(input.destination);
|
|
1202
|
+
return input.destination;
|
|
1203
|
+
}
|
|
1204
|
+
async function assertDisjointReadViewPaths(worktreePath, destination) {
|
|
1205
|
+
const worktree = await realpath(resolve(worktreePath));
|
|
1206
|
+
const readView = await canonicalProspectivePath(destination);
|
|
1207
|
+
if (isPathWithin(worktree, readView) || isPathWithin(readView, worktree)) {
|
|
1208
|
+
throw new Error('report read-view destination must not overlap its source worktree');
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async function canonicalProspectivePath(path) {
|
|
1212
|
+
let current = resolve(path);
|
|
1213
|
+
const suffix = [];
|
|
1214
|
+
while (true) {
|
|
1215
|
+
try {
|
|
1216
|
+
return resolve(await realpath(current), ...suffix);
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
1220
|
+
throw error;
|
|
1221
|
+
const parent = dirname(current);
|
|
1222
|
+
if (parent === current)
|
|
1223
|
+
throw error;
|
|
1224
|
+
suffix.unshift(basename(current));
|
|
1225
|
+
current = parent;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function isPathWithin(parent, child) {
|
|
1230
|
+
const contained = relative(parent, child);
|
|
1231
|
+
return contained === '' || (contained !== '..' && !contained.startsWith('../') && !isAbsolute(contained));
|
|
1232
|
+
}
|
|
1233
|
+
function normalizeReadViewDeniedPaths(paths) {
|
|
1234
|
+
return paths.flatMap((path) => {
|
|
1235
|
+
if (isAbsolute(path))
|
|
1236
|
+
return [];
|
|
1237
|
+
if (path.length === 0 || posix.normalize(path) !== path
|
|
1238
|
+
|| path.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
1239
|
+
throw new Error('report read-view denied path escapes destination');
|
|
1240
|
+
}
|
|
1241
|
+
return [path];
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
async function materializeCurrentReadView(worktreePath, destination, deniedPaths) {
|
|
1245
|
+
const paths = new Set(await readGitPathList(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']));
|
|
1246
|
+
for (const path of [...paths].sort()) {
|
|
1247
|
+
if (isExcludedReadViewPath(path, deniedPaths))
|
|
1248
|
+
continue;
|
|
1249
|
+
const target = resolve(destination, path);
|
|
1250
|
+
const contained = relative(destination, target);
|
|
1251
|
+
if (contained === '' || contained === '..' || contained.startsWith('../') || isAbsolute(contained)) {
|
|
1252
|
+
throw new Error('report read-view path escapes destination');
|
|
1253
|
+
}
|
|
1254
|
+
const source = resolve(worktreePath, path);
|
|
1255
|
+
let metadata;
|
|
1256
|
+
try {
|
|
1257
|
+
metadata = await lstat(source);
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
1261
|
+
throw error;
|
|
1262
|
+
await rm(target, { recursive: true, force: true });
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
1266
|
+
await rm(target, { recursive: true, force: true });
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
await copyRegularFileNoFollow(source, target, metadata.mode);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function isExcludedReadViewPath(path, deniedPaths) {
|
|
1273
|
+
if (path.split('/').some((segment) => segment.startsWith('.env')))
|
|
1274
|
+
return true;
|
|
1275
|
+
return deniedPaths.some((deniedPath) => path === deniedPath || path.startsWith(`${deniedPath}/`));
|
|
1276
|
+
}
|
|
1277
|
+
async function readGitPathList(worktreePath, args) {
|
|
1278
|
+
return new Promise((resolvePaths, reject) => {
|
|
1279
|
+
const child = spawn('git', ['-C', worktreePath, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1280
|
+
const stdout = [];
|
|
1281
|
+
const stderr = [];
|
|
1282
|
+
child.once('error', reject);
|
|
1283
|
+
child.stdout.on('data', (chunk) => { stdout.push(chunk); });
|
|
1284
|
+
child.stderr.on('data', (chunk) => { stderr.push(chunk); });
|
|
1285
|
+
child.once('close', (code) => {
|
|
1286
|
+
if (code !== 0) {
|
|
1287
|
+
reject(new Error(`report read-view inventory failed: ${Buffer.concat(stderr).toString('utf8').slice(0, 8 * 1024)}`));
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
resolvePaths(Buffer.concat(stdout).toString('utf8').split('\0').filter((path) => path.length > 0));
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
async function copyRegularFileNoFollow(sourcePath, targetPath, mode) {
|
|
1295
|
+
const source = await open(sourcePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1296
|
+
let target;
|
|
1297
|
+
try {
|
|
1298
|
+
const metadata = await source.stat();
|
|
1299
|
+
if (!metadata.isFile())
|
|
1300
|
+
throw new Error('report read-view source must be a regular file');
|
|
1301
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
1302
|
+
await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
1303
|
+
target = await open(targetPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode & 0o777);
|
|
1304
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
1305
|
+
while (true) {
|
|
1306
|
+
const { bytesRead } = await source.read(buffer, 0, buffer.length, null);
|
|
1307
|
+
if (bytesRead === 0)
|
|
1308
|
+
break;
|
|
1309
|
+
let offset = 0;
|
|
1310
|
+
while (offset < bytesRead) {
|
|
1311
|
+
const { bytesWritten } = await target.write(buffer, offset, bytesRead - offset, null);
|
|
1312
|
+
offset += bytesWritten;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
finally {
|
|
1317
|
+
await target?.close();
|
|
1318
|
+
await source.close();
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
async function scrubReportReadView(directory) {
|
|
1322
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
1323
|
+
const path = join(directory, entry.name);
|
|
1324
|
+
if (entry.name.startsWith('.env') || entry.isSymbolicLink()) {
|
|
1325
|
+
await rm(path, { recursive: true, force: true });
|
|
1326
|
+
}
|
|
1327
|
+
else if (entry.isDirectory()) {
|
|
1328
|
+
await scrubReportReadView(path);
|
|
1329
|
+
}
|
|
1330
|
+
else if (!entry.isFile()) {
|
|
1331
|
+
await rm(path, { recursive: true, force: true });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
835
1335
|
function isErrorCode(error, code) {
|
|
836
1336
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
837
1337
|
}
|