@fitlab-ai/agent-infra 0.9.5 → 0.9.7
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/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { extractSection, findSectionHeading, parseTable } from './sections.js';
|
|
4
|
+
const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'];
|
|
5
|
+
const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'];
|
|
6
|
+
const RECEIPT_EVENTS = new Set([
|
|
7
|
+
'review-analysis.completed',
|
|
8
|
+
'review-plan.completed',
|
|
9
|
+
'review-code.completed',
|
|
10
|
+
'code.completed'
|
|
11
|
+
]);
|
|
12
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
13
|
+
const ARTIFACT_NAME_RE = /^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r([2-9]|[1-9]\d+))?\.md$/;
|
|
14
|
+
const RECEIPT_SHAPES = {
|
|
15
|
+
'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
|
|
16
|
+
'review-plan.completed': { output: 'review-plan', input: 'plan' },
|
|
17
|
+
'review-code.completed': { output: 'review-code', input: 'code' },
|
|
18
|
+
'code.completed': { output: 'code', input: 'plan' }
|
|
19
|
+
};
|
|
20
|
+
class ArtifactReceiptError extends Error {
|
|
21
|
+
code = 'ARTIFACT_RECEIPT_INVALID';
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'ArtifactReceiptError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function sha256Bytes(value) {
|
|
28
|
+
return createHash('sha256').update(value).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
function sha256File(filePath) {
|
|
31
|
+
return sha256Bytes(fs.readFileSync(filePath));
|
|
32
|
+
}
|
|
33
|
+
function parseReceiptArtifactName(name) {
|
|
34
|
+
const match = ARTIFACT_NAME_RE.exec(name);
|
|
35
|
+
if (!match)
|
|
36
|
+
return null;
|
|
37
|
+
const roundText = match[2];
|
|
38
|
+
if (roundText !== undefined) {
|
|
39
|
+
const round = Number(roundText);
|
|
40
|
+
if (!Number.isSafeInteger(round) || String(round) !== roundText)
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return { family: match[1] };
|
|
44
|
+
}
|
|
45
|
+
function validateReceiptShape(event, output, input) {
|
|
46
|
+
const outputIdentity = parseReceiptArtifactName(output);
|
|
47
|
+
const inputIdentity = parseReceiptArtifactName(input);
|
|
48
|
+
if (!outputIdentity || !inputIdentity)
|
|
49
|
+
throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
|
|
50
|
+
const shape = RECEIPT_SHAPES[event];
|
|
51
|
+
if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
|
|
52
|
+
throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function parseArtifactReceipts(content) {
|
|
56
|
+
const section = extractSection(content, [...RECEIPT_SECTION_ALIASES]);
|
|
57
|
+
if (!section)
|
|
58
|
+
return { present: false, rows: [] };
|
|
59
|
+
let table;
|
|
60
|
+
try {
|
|
61
|
+
table = parseTable(content, {
|
|
62
|
+
sectionAliases: [...RECEIPT_SECTION_ALIASES],
|
|
63
|
+
columns: [...RECEIPT_COLUMNS],
|
|
64
|
+
keyColumn: 'output'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new ArtifactReceiptError(error instanceof Error ? error.message : String(error));
|
|
69
|
+
}
|
|
70
|
+
if (!table)
|
|
71
|
+
throw new ArtifactReceiptError('receipt section has no receipt table');
|
|
72
|
+
const rows = table.rows.map((row) => {
|
|
73
|
+
const event = row.values.event ?? '';
|
|
74
|
+
const output = row.values.output ?? '';
|
|
75
|
+
const input = row.values.input ?? '';
|
|
76
|
+
const inputSha256 = row.values.input_sha256 ?? '';
|
|
77
|
+
const completedAt = row.values.completed_at ?? '';
|
|
78
|
+
if (!RECEIPT_EVENTS.has(event))
|
|
79
|
+
throw new ArtifactReceiptError(`unknown receipt event '${event}'`);
|
|
80
|
+
if (!output || !input)
|
|
81
|
+
throw new ArtifactReceiptError('receipt output and input are required');
|
|
82
|
+
validateReceiptShape(event, output, input);
|
|
83
|
+
if (!SHA256_RE.test(inputSha256))
|
|
84
|
+
throw new ArtifactReceiptError(`receipt digest for '${output}' is invalid`);
|
|
85
|
+
if (!completedAt || Number.isNaN(Date.parse(completedAt.replace(' ', 'T')))) {
|
|
86
|
+
throw new ArtifactReceiptError(`receipt completion time for '${output}' is invalid`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
event: event,
|
|
90
|
+
output,
|
|
91
|
+
input,
|
|
92
|
+
inputSha256,
|
|
93
|
+
completedAt
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
return { present: true, rows };
|
|
97
|
+
}
|
|
98
|
+
function receiptForOutput(content, output) {
|
|
99
|
+
const parsed = parseArtifactReceipts(content);
|
|
100
|
+
return parsed.rows.find((row) => row.output === output) ?? null;
|
|
101
|
+
}
|
|
102
|
+
function upsertArtifactReceipt(content, receipt) {
|
|
103
|
+
if (!RECEIPT_EVENTS.has(receipt.event))
|
|
104
|
+
throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
|
|
105
|
+
if (!receipt.output || !receipt.input)
|
|
106
|
+
throw new ArtifactReceiptError('receipt output and input are required');
|
|
107
|
+
validateReceiptShape(receipt.event, receipt.output, receipt.input);
|
|
108
|
+
if (!SHA256_RE.test(receipt.inputSha256))
|
|
109
|
+
throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
|
|
110
|
+
if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
|
|
111
|
+
throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
|
|
112
|
+
}
|
|
113
|
+
const existing = parseArtifactReceipts(content).rows;
|
|
114
|
+
const previous = existing.find((row) => row.output === receipt.output);
|
|
115
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
|
|
116
|
+
throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
|
|
117
|
+
}
|
|
118
|
+
const rows = previous ? existing : [...existing, receipt];
|
|
119
|
+
const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
|
|
120
|
+
const body = [
|
|
121
|
+
`| ${RECEIPT_COLUMNS.join(' | ')} |`,
|
|
122
|
+
`| ${RECEIPT_COLUMNS.map(() => '---').join(' | ')} |`,
|
|
123
|
+
...rows.map((row) => `| ${row.event} | ${row.output} | ${row.input} | ${row.inputSha256} | ${row.completedAt} |`)
|
|
124
|
+
].join('\n');
|
|
125
|
+
return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
|
|
126
|
+
}
|
|
127
|
+
export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, sha256Bytes, sha256File, upsertArtifactReceipt };
|
|
128
|
+
//# sourceMappingURL=artifact-receipts.js.map
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { preflightCodexLifecycleEvidence, resolveCodexSpawnedChild, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
|
|
3
|
+
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
|
|
4
|
+
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
|
|
5
|
+
import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
6
|
+
import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
|
|
7
|
+
import { resolveTaskRef } from './resolve-ref.js';
|
|
8
|
+
import { activateMatchingOrchestrationDelegation, hasActivatableOrchestrationDelegation, hasSealableOrchestrationDelegation, pauseMatchingOrchestrationDelegation, prepareOrchestrationDelegation, reconcileMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence } from './orchestration.js';
|
|
9
|
+
function coreOptions(options) {
|
|
10
|
+
return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
|
|
11
|
+
}
|
|
12
|
+
function bridgeFailure(code, message) {
|
|
13
|
+
return {
|
|
14
|
+
status: 'failed', changed: false, taskId: null, run: null, next: null,
|
|
15
|
+
error: { code, message }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function pauseBridge(code, message, options) {
|
|
19
|
+
const paused = pauseMatchingOrchestrationDelegation('codex', code, message, coreOptions(options));
|
|
20
|
+
return paused.error?.code === 'ORCHESTRATION_DELEGATION_MISSING'
|
|
21
|
+
? bridgeFailure(code, message)
|
|
22
|
+
: paused;
|
|
23
|
+
}
|
|
24
|
+
function requiredStore(options) {
|
|
25
|
+
if (!options.store)
|
|
26
|
+
throw new Error('Codex lifecycle store is required for orchestration bridge events');
|
|
27
|
+
return options.store;
|
|
28
|
+
}
|
|
29
|
+
function controllerContext(taskId, repoRoot) {
|
|
30
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
31
|
+
return contextPath
|
|
32
|
+
? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
|
|
33
|
+
: null;
|
|
34
|
+
}
|
|
35
|
+
async function prepareCodexOrchestrationDelegation(taskRef, input, options = {}) {
|
|
36
|
+
if (input.client !== 'codex')
|
|
37
|
+
return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
|
|
38
|
+
try {
|
|
39
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
40
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot });
|
|
41
|
+
if (!resolved.ok)
|
|
42
|
+
return bridgeFailure(resolved.code, resolved.message);
|
|
43
|
+
const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot);
|
|
44
|
+
if (!input.capabilityToken) {
|
|
45
|
+
return bridgeFailure('ORCHESTRATION_CODEX_CAPABILITY_REQUIRED', 'Codex prepare requires a current-session capability token');
|
|
46
|
+
}
|
|
47
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
48
|
+
const controller = controllerContext(resolved.taskId, repoRoot);
|
|
49
|
+
const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
|
|
50
|
+
const consumed = capabilityStore.consume(input.capabilityToken, {
|
|
51
|
+
taskId: resolved.taskId,
|
|
52
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
53
|
+
buildIdentity,
|
|
54
|
+
...(controller ? { controller: {
|
|
55
|
+
instanceDigest: controller.controllerInstanceDigest,
|
|
56
|
+
controlGeneration: controller.controlGeneration
|
|
57
|
+
} } : {})
|
|
58
|
+
});
|
|
59
|
+
return prepareOrchestrationDelegation(taskRef, {
|
|
60
|
+
...input,
|
|
61
|
+
lifecycleProvenance: {
|
|
62
|
+
...consumed.buildIdentity,
|
|
63
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
64
|
+
...preflight.hookProvenance,
|
|
65
|
+
capabilitySessionId: consumed.sessionId,
|
|
66
|
+
capabilityTurnId: consumed.turnId,
|
|
67
|
+
capabilityToolUseId: consumed.toolUseId,
|
|
68
|
+
controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
|
|
69
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
70
|
+
}
|
|
71
|
+
}, coreOptions(options));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return bridgeFailure(error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
|
|
75
|
+
? error.name
|
|
76
|
+
: 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED', error instanceof Error ? error.message : String(error));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function activateCodexOrchestrationDelegation(childThreadId, options = {}) {
|
|
80
|
+
try {
|
|
81
|
+
if (!hasActivatableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
|
|
82
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
83
|
+
}
|
|
84
|
+
const store = requiredStore(options);
|
|
85
|
+
const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
|
|
86
|
+
store.apply(resolved.resolution.thread);
|
|
87
|
+
for (const reroute of resolved.reroutes)
|
|
88
|
+
store.apply(reroute);
|
|
89
|
+
store.apply(resolved.resolution.settings);
|
|
90
|
+
const record = store.read(childThreadId);
|
|
91
|
+
const evidence = record.state.startEvidence;
|
|
92
|
+
if (record.state.status !== 'start-ready' || !evidence) {
|
|
93
|
+
return pauseBridge('ORCHESTRATION_CODEX_START_EVIDENCE_INVALID', 'Codex lifecycle start evidence is not ready', options);
|
|
94
|
+
}
|
|
95
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
96
|
+
const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot, {
|
|
97
|
+
sessionId: evidence.parentThreadId,
|
|
98
|
+
turnId: evidence.parentTurnId,
|
|
99
|
+
toolUseId: evidence.spawnToolUseId
|
|
100
|
+
});
|
|
101
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
102
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
103
|
+
const contextTaskId = contextPath
|
|
104
|
+
? JSON.parse(fs.readFileSync(contextPath, 'utf8')).taskId
|
|
105
|
+
: null;
|
|
106
|
+
if (contextPath && typeof contextTaskId !== 'string') {
|
|
107
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
108
|
+
}
|
|
109
|
+
const controller = contextPath
|
|
110
|
+
? verifyCodexSandboxControllerContext(contextPath, contextTaskId, { repoRoot })
|
|
111
|
+
: null;
|
|
112
|
+
return activateMatchingOrchestrationDelegation('codex', {
|
|
113
|
+
nativeAgent: evidence.nativeAgent,
|
|
114
|
+
childId: evidence.childThreadId,
|
|
115
|
+
parentId: evidence.parentThreadId,
|
|
116
|
+
spawnMode: evidence.spawnMode,
|
|
117
|
+
actualModel: evidence.actualModel.value,
|
|
118
|
+
actualReasoningEffort: evidence.actualReasoningEffort.value,
|
|
119
|
+
...(evidence.modelFallbackReason ? { modelFallbackReason: evidence.modelFallbackReason } : {}),
|
|
120
|
+
...(evidence.reasoningEffortFallbackReason
|
|
121
|
+
? { reasoningEffortFallbackReason: evidence.reasoningEffortFallbackReason }
|
|
122
|
+
: {}),
|
|
123
|
+
hostEvidence: {
|
|
124
|
+
kind: 'codex-lifecycle-v2',
|
|
125
|
+
hookDefinitionHash: evidence.hookDefinitionHash,
|
|
126
|
+
startRevision: record.revision,
|
|
127
|
+
...buildIdentity,
|
|
128
|
+
...preflight.hookProvenance,
|
|
129
|
+
capabilitySessionId: evidence.parentThreadId,
|
|
130
|
+
capabilityTurnId: evidence.parentTurnId,
|
|
131
|
+
spawnToolUseId: evidence.spawnToolUseId,
|
|
132
|
+
spawnObservedAt: record.spawnObservedAt ?? undefined,
|
|
133
|
+
controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
|
|
134
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
135
|
+
}
|
|
136
|
+
}, coreOptions(options));
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function activateCodexSpawnDelegation(spawn, options = {}) {
|
|
143
|
+
try {
|
|
144
|
+
const store = requiredStore(options);
|
|
145
|
+
const childThreadId = resolveCodexSpawnedChild(spawn.transcriptPath, spawn);
|
|
146
|
+
const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
|
|
147
|
+
store.applyToSpawn(spawn, {
|
|
148
|
+
type: 'hook-child',
|
|
149
|
+
sessionId: spawn.sessionId,
|
|
150
|
+
turnId: spawn.turnId,
|
|
151
|
+
childThreadId,
|
|
152
|
+
parentThreadId: resolved.resolution.thread.parentThreadId,
|
|
153
|
+
nativeAgent: spawn.nativeAgent,
|
|
154
|
+
source: 'parent-rollout'
|
|
155
|
+
});
|
|
156
|
+
return activateCodexOrchestrationDelegation(childThreadId, {
|
|
157
|
+
...options,
|
|
158
|
+
store,
|
|
159
|
+
resolveThread: async () => resolved
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function sealCodexOrchestrationDelegation(childThreadId, options = {}) {
|
|
167
|
+
try {
|
|
168
|
+
if (!hasSealableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
|
|
169
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
170
|
+
}
|
|
171
|
+
const store = requiredStore(options);
|
|
172
|
+
const existing = store.read(childThreadId);
|
|
173
|
+
if (!existing.consumer) {
|
|
174
|
+
store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId));
|
|
175
|
+
}
|
|
176
|
+
const record = store.read(childThreadId);
|
|
177
|
+
const evidence = record.state.stopEvidence;
|
|
178
|
+
if (record.state.status !== 'stop-ready' || !evidence || !record.state.startEvidence) {
|
|
179
|
+
return pauseBridge('ORCHESTRATION_CODEX_STOP_EVIDENCE_INVALID', 'Codex lifecycle stop evidence is not ready', options);
|
|
180
|
+
}
|
|
181
|
+
return sealMatchingOrchestrationDelegationWithHostEvidence('codex', { nativeAgent: record.state.startEvidence.nativeAgent, childId: childThreadId }, (receipt) => {
|
|
182
|
+
const consumed = store.consume(childThreadId, receipt.id, receipt.hostEvidence?.hookDefinitionHash);
|
|
183
|
+
return {
|
|
184
|
+
stopRevision: consumed.revision,
|
|
185
|
+
consumer: consumed.consumer,
|
|
186
|
+
consumedAt: consumed.consumedAt
|
|
187
|
+
};
|
|
188
|
+
}, coreOptions(options));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function sealCodexParentDelegation(parentThreadId, options = {}) {
|
|
195
|
+
try {
|
|
196
|
+
const store = requiredStore(options);
|
|
197
|
+
const candidates = store.findByParent(parentThreadId);
|
|
198
|
+
if (!candidates.length)
|
|
199
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
200
|
+
for (const consumed of candidates.filter((record) => record.consumer)) {
|
|
201
|
+
const replayed = await sealCodexOrchestrationDelegation(consumed.state.startEvidence.childThreadId, options);
|
|
202
|
+
if (replayed.error?.code !== 'ORCHESTRATION_DELEGATION_MISSING')
|
|
203
|
+
return replayed;
|
|
204
|
+
}
|
|
205
|
+
const active = candidates.find((record) => !record.consumer);
|
|
206
|
+
if (!active)
|
|
207
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
208
|
+
const start = active.state.startEvidence;
|
|
209
|
+
const child = active.state.child;
|
|
210
|
+
if (!active.state.terminal) {
|
|
211
|
+
store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(start.childThreadId));
|
|
212
|
+
}
|
|
213
|
+
if (!store.read(start.childThreadId).state.stop) {
|
|
214
|
+
store.apply({
|
|
215
|
+
type: 'hook-stop',
|
|
216
|
+
sessionId: parentThreadId,
|
|
217
|
+
turnId: child.turnId,
|
|
218
|
+
childThreadId: start.childThreadId,
|
|
219
|
+
nativeAgent: start.nativeAgent,
|
|
220
|
+
source: 'parent-rollout'
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return sealCodexOrchestrationDelegation(start.childThreadId, options);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof Error && error.message === 'CODEX_TURN_NOT_TERMINAL') {
|
|
227
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'The matching Codex child has not completed');
|
|
228
|
+
}
|
|
229
|
+
return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function reconcileCodexOrchestrationDelegation(childThreadId, options = {}) {
|
|
233
|
+
return reconcileMatchingOrchestrationDelegation('codex', childThreadId, coreOptions(options));
|
|
234
|
+
}
|
|
235
|
+
export { activateCodexOrchestrationDelegation, activateCodexSpawnDelegation, prepareCodexOrchestrationDelegation, reconcileCodexOrchestrationDelegation, sealCodexParentDelegation, sealCodexOrchestrationDelegation };
|
|
236
|
+
//# sourceMappingURL=codex-orchestration.js.map
|
|
@@ -59,6 +59,7 @@ const STAGE_ORDER = [
|
|
|
59
59
|
'review-plan',
|
|
60
60
|
'code',
|
|
61
61
|
'review-code',
|
|
62
|
+
'validation-run',
|
|
62
63
|
'task',
|
|
63
64
|
'other'
|
|
64
65
|
];
|
|
@@ -72,6 +73,8 @@ function stageOf(name) {
|
|
|
72
73
|
return 'review-plan';
|
|
73
74
|
if (stem.startsWith('review-code'))
|
|
74
75
|
return 'review-code';
|
|
76
|
+
if (stem.startsWith('validation-run'))
|
|
77
|
+
return 'validation-run';
|
|
75
78
|
if (stem.startsWith('analysis'))
|
|
76
79
|
return 'analysis';
|
|
77
80
|
if (stem.startsWith('plan'))
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { loadConfig } from '../../sandbox/config.js';
|
|
7
|
+
import { detectEngine } from '../../sandbox/engine.js';
|
|
8
|
+
import { runEngine } from '../../sandbox/shell.js';
|
|
9
|
+
import { sandboxBranchLabel, sandboxLabel } from '../../sandbox/constants.js';
|
|
10
|
+
import { fetchSandboxRows, selectSandboxContainer } from '../../sandbox/commands/list-running.js';
|
|
11
|
+
import { containerNameCandidates } from '../../sandbox/constants.js';
|
|
12
|
+
import { resolveSandboxTarget } from '../../sandbox/workspace-identity.js';
|
|
13
|
+
import { sandboxControlPaths } from '../../sandbox/workspace-view.js';
|
|
14
|
+
import { appendSandboxControlAudit, readActiveLease, readSandboxControlStatus } from '../../sandbox/control/state.js';
|
|
15
|
+
import { SANDBOX_CONTROL_FUTURE_SKEW_MS, SANDBOX_CONTROL_STATUS_STALE_MS } from '../../sandbox/control/protocol.js';
|
|
16
|
+
import { getProcessStartTime } from '../../server/process-state.js';
|
|
17
|
+
import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
|
|
18
|
+
const USAGE = `Usage: ai task validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] -- <command> [args...]`;
|
|
19
|
+
const MAX_TIMEOUT_MS = 60 * 60 * 1000;
|
|
20
|
+
const RECOVERY_GRACE_MS = 30_000;
|
|
21
|
+
export function parseValidateArgs(args) {
|
|
22
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
23
|
+
return { target: '', scope: 'snapshot', timeoutMs: 300_000, format: 'text', command: [], help: true };
|
|
24
|
+
}
|
|
25
|
+
const separator = args.indexOf('--');
|
|
26
|
+
if (separator < 0)
|
|
27
|
+
throw new Error(`${USAGE}\nA literal -- must separate validation options from the command.`);
|
|
28
|
+
const optionArgs = args.slice(0, separator);
|
|
29
|
+
const command = args.slice(separator + 1);
|
|
30
|
+
let scope = 'snapshot';
|
|
31
|
+
let timeoutMs = 300_000;
|
|
32
|
+
let format = 'text';
|
|
33
|
+
const positionals = [];
|
|
34
|
+
for (let index = 0; index < optionArgs.length; index += 1) {
|
|
35
|
+
const arg = optionArgs[index];
|
|
36
|
+
if (arg === '--scope') {
|
|
37
|
+
const value = optionArgs[++index];
|
|
38
|
+
if (value !== 'snapshot' && value !== 'inplace')
|
|
39
|
+
throw new Error('validate --scope must be snapshot or inplace');
|
|
40
|
+
scope = value;
|
|
41
|
+
}
|
|
42
|
+
else if (arg === '--timeout') {
|
|
43
|
+
const value = Number(optionArgs[++index]);
|
|
44
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TIMEOUT_MS)
|
|
45
|
+
throw new Error(`validate --timeout must be 1..${MAX_TIMEOUT_MS}`);
|
|
46
|
+
timeoutMs = value;
|
|
47
|
+
}
|
|
48
|
+
else if (arg === '--format') {
|
|
49
|
+
const value = optionArgs[++index];
|
|
50
|
+
if (value !== 'text' && value !== 'json')
|
|
51
|
+
throw new Error('validate --format must be text or json');
|
|
52
|
+
format = value;
|
|
53
|
+
}
|
|
54
|
+
else if (arg.startsWith('-')) {
|
|
55
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
positionals.push(arg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (positionals.length !== 1 || command.length === 0)
|
|
62
|
+
throw new Error(USAGE);
|
|
63
|
+
return { target: positionals[0], scope, timeoutMs, format, command, help: false };
|
|
64
|
+
}
|
|
65
|
+
function runValidationCommand(command, cwd, scope, timeoutMs, json) {
|
|
66
|
+
const [file, ...args] = command;
|
|
67
|
+
const result = spawnSync(file, args, {
|
|
68
|
+
cwd,
|
|
69
|
+
env: { ...process.env, AGENT_INFRA_VALIDATION_SCOPE: scope },
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
timeout: timeoutMs,
|
|
72
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
73
|
+
shell: process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file)
|
|
74
|
+
});
|
|
75
|
+
if (json) {
|
|
76
|
+
if (result.stdout)
|
|
77
|
+
process.stderr.write(result.stdout);
|
|
78
|
+
if (result.stderr)
|
|
79
|
+
process.stderr.write(result.stderr);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (result.stdout)
|
|
83
|
+
process.stdout.write(result.stdout);
|
|
84
|
+
if (result.stderr)
|
|
85
|
+
process.stderr.write(result.stderr);
|
|
86
|
+
}
|
|
87
|
+
return { exitCode: result.status ?? 1, signal: result.signal ?? null };
|
|
88
|
+
}
|
|
89
|
+
function git(root, args) {
|
|
90
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
91
|
+
}
|
|
92
|
+
function waitForState(statusDir, predicate, timeoutMs = 5_000) {
|
|
93
|
+
const deadline = Date.now() + timeoutMs;
|
|
94
|
+
while (Date.now() < deadline) {
|
|
95
|
+
try {
|
|
96
|
+
const status = readSandboxControlStatus(statusDir);
|
|
97
|
+
if (predicate(status))
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Atomic status publication may be between renames.
|
|
102
|
+
}
|
|
103
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
104
|
+
}
|
|
105
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
|
|
106
|
+
}
|
|
107
|
+
function snapshotValidation(repoRoot, commit, options) {
|
|
108
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
|
|
109
|
+
const worktree = path.join(tempRoot, 'worktree');
|
|
110
|
+
let cleanup = 'not-created';
|
|
111
|
+
try {
|
|
112
|
+
git(repoRoot, ['worktree', 'add', '--detach', worktree, commit]);
|
|
113
|
+
cleanup = 'pending';
|
|
114
|
+
return { ...runValidationCommand(options.command, worktree, 'snapshot', options.timeoutMs, options.format === 'json'), cleanup: () => cleanup };
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
let cleanupError = null;
|
|
118
|
+
if (fs.existsSync(worktree)) {
|
|
119
|
+
try {
|
|
120
|
+
git(repoRoot, ['worktree', 'remove', '--force', worktree]);
|
|
121
|
+
cleanup = 'completed';
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
cleanupError = error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
git(repoRoot, ['worktree', 'prune']);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
cleanupError ??= error;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
cleanupError ??= error;
|
|
138
|
+
}
|
|
139
|
+
if (cleanupError)
|
|
140
|
+
throw cleanupError;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function inplaceValidation(config, target, options) {
|
|
144
|
+
const engine = detectEngine(config);
|
|
145
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
146
|
+
const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
|
|
147
|
+
if (!row)
|
|
148
|
+
throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
|
|
149
|
+
const control = sandboxControlPaths({
|
|
150
|
+
base: config.controlBase, project: config.project,
|
|
151
|
+
container: row.name, identity: target.workspace
|
|
152
|
+
});
|
|
153
|
+
const manifest = JSON.parse(fs.readFileSync(control.manifestPath, 'utf8'));
|
|
154
|
+
const initial = readSandboxControlStatus(control.statusDir);
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (initial.generation !== manifest.generation
|
|
157
|
+
|| now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
|
|
158
|
+
|| initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
159
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
|
|
160
|
+
}
|
|
161
|
+
if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
|
|
162
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
|
|
163
|
+
}
|
|
164
|
+
const startTime = getProcessStartTime(process.pid);
|
|
165
|
+
if (!startTime)
|
|
166
|
+
throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
|
|
167
|
+
const leasePath = path.join(control.root, 'lease.json');
|
|
168
|
+
const nonce = randomUUID();
|
|
169
|
+
const issuedAt = Date.now();
|
|
170
|
+
const lease = {
|
|
171
|
+
version: 2, generation: manifest.generation, nonce,
|
|
172
|
+
owner: { pid: process.pid, startTime }, issuedAt,
|
|
173
|
+
expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
|
|
174
|
+
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
|
175
|
+
branch: target.branch, reason: 'manual-validation'
|
|
176
|
+
};
|
|
177
|
+
const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
178
|
+
const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
|
|
179
|
+
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
180
|
+
let stopped = false;
|
|
181
|
+
let restored = false;
|
|
182
|
+
try {
|
|
183
|
+
appendSandboxControlAudit(manifest, 'lease-acquire', { ownerPid: process.pid });
|
|
184
|
+
waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
185
|
+
if (row.running) {
|
|
186
|
+
runEngine(engine, 'docker', ['stop', row.name]);
|
|
187
|
+
stopped = true;
|
|
188
|
+
}
|
|
189
|
+
const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
190
|
+
const refreshed = selectSandboxContainer([...refreshedRows.running, ...refreshedRows.nonRunning], containerNameCandidates(config, target.branch));
|
|
191
|
+
if (!refreshed || refreshed.running)
|
|
192
|
+
throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
|
|
193
|
+
const activeLease = readActiveLease(manifest);
|
|
194
|
+
if (!activeLease || activeLease.nonce !== nonce)
|
|
195
|
+
throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
196
|
+
assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
|
|
197
|
+
const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
|
|
198
|
+
return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
const recoveryErrors = [];
|
|
202
|
+
try {
|
|
203
|
+
const currentBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
204
|
+
if (currentBranch !== originalBranch)
|
|
205
|
+
git(manifest.worktreeRoot, ['switch', originalBranch]);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
try {
|
|
209
|
+
git(manifest.worktreeRoot, ['checkout', '--detach', originalHead]);
|
|
210
|
+
git(manifest.worktreeRoot, ['switch', originalBranch]);
|
|
211
|
+
}
|
|
212
|
+
catch (fallbackError) {
|
|
213
|
+
recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (stopped) {
|
|
217
|
+
try {
|
|
218
|
+
runEngine(engine, 'docker', ['start', row.name]);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
226
|
+
if (currentLease.nonce !== nonce)
|
|
227
|
+
throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
231
|
+
}
|
|
232
|
+
if (recoveryErrors.length === 0) {
|
|
233
|
+
try {
|
|
234
|
+
fs.unlinkSync(leasePath);
|
|
235
|
+
appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
|
|
236
|
+
waitForState(control.statusDir, (status) => status.state === 'healthy');
|
|
237
|
+
restored = true;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
try {
|
|
241
|
+
if (!fs.existsSync(leasePath)) {
|
|
242
|
+
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
243
|
+
}
|
|
244
|
+
appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
|
|
245
|
+
}
|
|
246
|
+
catch (leaseError) {
|
|
247
|
+
recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
|
|
248
|
+
}
|
|
249
|
+
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (recoveryErrors.length > 0) {
|
|
253
|
+
throw new Error(`SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export async function validate(args) {
|
|
258
|
+
const options = parseValidateArgs(args);
|
|
259
|
+
if (options.help) {
|
|
260
|
+
process.stdout.write(`${USAGE}\n`);
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
const config = loadConfig();
|
|
264
|
+
const target = resolveSandboxTarget(options.target, config.repoRoot);
|
|
265
|
+
const commit = git(config.repoRoot, ['rev-parse', target.branch]);
|
|
266
|
+
const startedAt = new Date().toISOString();
|
|
267
|
+
const result = options.scope === 'snapshot'
|
|
268
|
+
? snapshotValidation(config.repoRoot, commit, options)
|
|
269
|
+
: inplaceValidation(config, target, options);
|
|
270
|
+
const evidence = {
|
|
271
|
+
version: 1,
|
|
272
|
+
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
|
273
|
+
branch: target.branch,
|
|
274
|
+
scope: options.scope,
|
|
275
|
+
commit,
|
|
276
|
+
command: path.basename(options.command[0]),
|
|
277
|
+
startedAt,
|
|
278
|
+
completedAt: new Date().toISOString(),
|
|
279
|
+
exitCode: result.exitCode,
|
|
280
|
+
signal: result.signal,
|
|
281
|
+
cleanup: result.cleanup()
|
|
282
|
+
};
|
|
283
|
+
if (options.format === 'json')
|
|
284
|
+
process.stdout.write(`${JSON.stringify(evidence)}\n`);
|
|
285
|
+
else
|
|
286
|
+
process.stdout.write(`Validation ${result.exitCode === 0 ? 'passed' : 'failed'} (${options.scope}, ${evidence.command}, cleanup=${evidence.cleanup}).\n`);
|
|
287
|
+
return result.exitCode;
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=validate.js.map
|