@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,319 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
preflightCodexLifecycleEvidence,
|
|
5
|
+
resolveCodexSpawnedChild,
|
|
6
|
+
resolveCodexTerminal,
|
|
7
|
+
resolveCodexThread
|
|
8
|
+
} from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
|
|
9
|
+
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
10
|
+
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
11
|
+
import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
12
|
+
import type { LifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
13
|
+
import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
|
|
14
|
+
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
15
|
+
import { resolveTaskRef } from './resolve-ref.ts';
|
|
16
|
+
import {
|
|
17
|
+
activateMatchingOrchestrationDelegation,
|
|
18
|
+
hasActivatableOrchestrationDelegation,
|
|
19
|
+
hasSealableOrchestrationDelegation,
|
|
20
|
+
pauseMatchingOrchestrationDelegation,
|
|
21
|
+
prepareOrchestrationDelegation,
|
|
22
|
+
reconcileMatchingOrchestrationDelegation,
|
|
23
|
+
sealMatchingOrchestrationDelegationWithHostEvidence
|
|
24
|
+
} from './orchestration.ts';
|
|
25
|
+
import type { OrchestrationOptions, OrchestrationResult } from './orchestration.ts';
|
|
26
|
+
|
|
27
|
+
type LifecycleStore = ReturnType<typeof createCodexLifecycleStore>;
|
|
28
|
+
type CapabilityStore = ReturnType<typeof createCodexCapabilityStore>;
|
|
29
|
+
type CodexBridgeOptions = Readonly<{
|
|
30
|
+
repoRoot?: string;
|
|
31
|
+
store?: LifecycleStore;
|
|
32
|
+
preflight?: typeof preflightCodexLifecycleEvidence;
|
|
33
|
+
resolveThread?: typeof resolveCodexThread;
|
|
34
|
+
resolveTerminal?: typeof resolveCodexTerminal;
|
|
35
|
+
capabilityStore?: CapabilityStore;
|
|
36
|
+
buildIdentity?: LifecycleBuildIdentity;
|
|
37
|
+
orchestrationOptions?: OrchestrationOptions;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
type CodexSpawnIdentity = Readonly<{
|
|
41
|
+
sessionId: string;
|
|
42
|
+
turnId: string;
|
|
43
|
+
toolUseId: string;
|
|
44
|
+
transcriptPath: string;
|
|
45
|
+
nativeAgent: string;
|
|
46
|
+
taskName: string;
|
|
47
|
+
requestedModel?: string;
|
|
48
|
+
requestedReasoningEffort?: string;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
function coreOptions(options: CodexBridgeOptions): OrchestrationOptions {
|
|
52
|
+
return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function bridgeFailure(code: string, message: string): OrchestrationResult {
|
|
56
|
+
return {
|
|
57
|
+
status: 'failed', changed: false, taskId: null, run: null, next: null,
|
|
58
|
+
error: { code, message }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pauseBridge(code: string, message: string, options: CodexBridgeOptions): OrchestrationResult {
|
|
63
|
+
const paused = pauseMatchingOrchestrationDelegation('codex', code, message, coreOptions(options));
|
|
64
|
+
return paused.error?.code === 'ORCHESTRATION_DELEGATION_MISSING'
|
|
65
|
+
? bridgeFailure(code, message)
|
|
66
|
+
: paused;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requiredStore(options: CodexBridgeOptions): LifecycleStore {
|
|
70
|
+
if (!options.store) throw new Error('Codex lifecycle store is required for orchestration bridge events');
|
|
71
|
+
return options.store;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function controllerContext(taskId: string, repoRoot: string) {
|
|
75
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
76
|
+
return contextPath
|
|
77
|
+
? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function prepareCodexOrchestrationDelegation(
|
|
82
|
+
taskRef: string,
|
|
83
|
+
input: Readonly<{
|
|
84
|
+
client: AgentClientId;
|
|
85
|
+
requestedModel?: string;
|
|
86
|
+
requestedReasoningEffort?: string;
|
|
87
|
+
capabilityToken?: string;
|
|
88
|
+
}>,
|
|
89
|
+
options: CodexBridgeOptions = {}
|
|
90
|
+
): Promise<OrchestrationResult> {
|
|
91
|
+
if (input.client !== 'codex') return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
|
|
92
|
+
try {
|
|
93
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
94
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot });
|
|
95
|
+
if (!resolved.ok) return bridgeFailure(resolved.code, resolved.message);
|
|
96
|
+
const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot);
|
|
97
|
+
if (!input.capabilityToken) {
|
|
98
|
+
return bridgeFailure(
|
|
99
|
+
'ORCHESTRATION_CODEX_CAPABILITY_REQUIRED',
|
|
100
|
+
'Codex prepare requires a current-session capability token'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
104
|
+
const controller = controllerContext(resolved.taskId, repoRoot);
|
|
105
|
+
const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
|
|
106
|
+
const consumed = capabilityStore.consume(input.capabilityToken, {
|
|
107
|
+
taskId: resolved.taskId,
|
|
108
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
109
|
+
buildIdentity,
|
|
110
|
+
...(controller ? { controller: {
|
|
111
|
+
instanceDigest: controller.controllerInstanceDigest,
|
|
112
|
+
controlGeneration: controller.controlGeneration
|
|
113
|
+
} } : {})
|
|
114
|
+
});
|
|
115
|
+
return prepareOrchestrationDelegation(taskRef, {
|
|
116
|
+
...input,
|
|
117
|
+
lifecycleProvenance: {
|
|
118
|
+
...consumed.buildIdentity,
|
|
119
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
120
|
+
...preflight.hookProvenance,
|
|
121
|
+
capabilitySessionId: consumed.sessionId!,
|
|
122
|
+
capabilityTurnId: consumed.turnId!,
|
|
123
|
+
capabilityToolUseId: consumed.toolUseId!,
|
|
124
|
+
controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
|
|
125
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
126
|
+
}
|
|
127
|
+
}, coreOptions(options));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return bridgeFailure(
|
|
130
|
+
error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
|
|
131
|
+
? error.name
|
|
132
|
+
: 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
|
|
133
|
+
error instanceof Error ? error.message : String(error)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function activateCodexOrchestrationDelegation(
|
|
139
|
+
childThreadId: string,
|
|
140
|
+
options: CodexBridgeOptions = {}
|
|
141
|
+
): Promise<OrchestrationResult> {
|
|
142
|
+
try {
|
|
143
|
+
if (!hasActivatableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
|
|
144
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
145
|
+
}
|
|
146
|
+
const store = requiredStore(options);
|
|
147
|
+
const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
|
|
148
|
+
store.apply(resolved.resolution.thread);
|
|
149
|
+
for (const reroute of resolved.reroutes) store.apply(reroute);
|
|
150
|
+
store.apply(resolved.resolution.settings);
|
|
151
|
+
const record = store.read(childThreadId);
|
|
152
|
+
const evidence = record.state.startEvidence;
|
|
153
|
+
if (record.state.status !== 'start-ready' || !evidence) {
|
|
154
|
+
return pauseBridge('ORCHESTRATION_CODEX_START_EVIDENCE_INVALID', 'Codex lifecycle start evidence is not ready', options);
|
|
155
|
+
}
|
|
156
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
157
|
+
const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot, {
|
|
158
|
+
sessionId: evidence.parentThreadId,
|
|
159
|
+
turnId: evidence.parentTurnId,
|
|
160
|
+
toolUseId: evidence.spawnToolUseId
|
|
161
|
+
});
|
|
162
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
163
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
164
|
+
const contextTaskId = contextPath
|
|
165
|
+
? (JSON.parse(fs.readFileSync(contextPath, 'utf8')) as { taskId?: unknown }).taskId
|
|
166
|
+
: null;
|
|
167
|
+
if (contextPath && typeof contextTaskId !== 'string') {
|
|
168
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
169
|
+
}
|
|
170
|
+
const controller = contextPath
|
|
171
|
+
? verifyCodexSandboxControllerContext(contextPath, contextTaskId as string, { repoRoot })
|
|
172
|
+
: null;
|
|
173
|
+
return activateMatchingOrchestrationDelegation('codex', {
|
|
174
|
+
nativeAgent: evidence.nativeAgent,
|
|
175
|
+
childId: evidence.childThreadId,
|
|
176
|
+
parentId: evidence.parentThreadId,
|
|
177
|
+
spawnMode: evidence.spawnMode,
|
|
178
|
+
actualModel: evidence.actualModel.value,
|
|
179
|
+
actualReasoningEffort: evidence.actualReasoningEffort.value,
|
|
180
|
+
...(evidence.modelFallbackReason ? { modelFallbackReason: evidence.modelFallbackReason } : {}),
|
|
181
|
+
...(evidence.reasoningEffortFallbackReason
|
|
182
|
+
? { reasoningEffortFallbackReason: evidence.reasoningEffortFallbackReason }
|
|
183
|
+
: {}),
|
|
184
|
+
hostEvidence: {
|
|
185
|
+
kind: 'codex-lifecycle-v2',
|
|
186
|
+
hookDefinitionHash: evidence.hookDefinitionHash,
|
|
187
|
+
startRevision: record.revision,
|
|
188
|
+
...buildIdentity,
|
|
189
|
+
...preflight.hookProvenance,
|
|
190
|
+
capabilitySessionId: evidence.parentThreadId,
|
|
191
|
+
capabilityTurnId: evidence.parentTurnId,
|
|
192
|
+
spawnToolUseId: evidence.spawnToolUseId,
|
|
193
|
+
spawnObservedAt: record.spawnObservedAt ?? undefined,
|
|
194
|
+
controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
|
|
195
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
196
|
+
}
|
|
197
|
+
}, coreOptions(options));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function activateCodexSpawnDelegation(
|
|
204
|
+
spawn: CodexSpawnIdentity,
|
|
205
|
+
options: CodexBridgeOptions = {}
|
|
206
|
+
): Promise<OrchestrationResult> {
|
|
207
|
+
try {
|
|
208
|
+
const store = requiredStore(options);
|
|
209
|
+
const childThreadId = resolveCodexSpawnedChild(spawn.transcriptPath, spawn);
|
|
210
|
+
const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
|
|
211
|
+
store.applyToSpawn(spawn, {
|
|
212
|
+
type: 'hook-child',
|
|
213
|
+
sessionId: spawn.sessionId,
|
|
214
|
+
turnId: spawn.turnId,
|
|
215
|
+
childThreadId,
|
|
216
|
+
parentThreadId: resolved.resolution.thread.parentThreadId,
|
|
217
|
+
nativeAgent: spawn.nativeAgent,
|
|
218
|
+
source: 'parent-rollout'
|
|
219
|
+
});
|
|
220
|
+
return activateCodexOrchestrationDelegation(childThreadId, {
|
|
221
|
+
...options,
|
|
222
|
+
store,
|
|
223
|
+
resolveThread: async () => resolved
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function sealCodexOrchestrationDelegation(
|
|
231
|
+
childThreadId: string,
|
|
232
|
+
options: CodexBridgeOptions = {}
|
|
233
|
+
): Promise<OrchestrationResult> {
|
|
234
|
+
try {
|
|
235
|
+
if (!hasSealableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
|
|
236
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
237
|
+
}
|
|
238
|
+
const store = requiredStore(options);
|
|
239
|
+
const existing = store.read(childThreadId);
|
|
240
|
+
if (!existing.consumer) {
|
|
241
|
+
store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId));
|
|
242
|
+
}
|
|
243
|
+
const record = store.read(childThreadId);
|
|
244
|
+
const evidence = record.state.stopEvidence;
|
|
245
|
+
if (record.state.status !== 'stop-ready' || !evidence || !record.state.startEvidence) {
|
|
246
|
+
return pauseBridge('ORCHESTRATION_CODEX_STOP_EVIDENCE_INVALID', 'Codex lifecycle stop evidence is not ready', options);
|
|
247
|
+
}
|
|
248
|
+
return sealMatchingOrchestrationDelegationWithHostEvidence(
|
|
249
|
+
'codex',
|
|
250
|
+
{ nativeAgent: record.state.startEvidence.nativeAgent, childId: childThreadId },
|
|
251
|
+
(receipt) => {
|
|
252
|
+
const consumed = store.consume(childThreadId, receipt.id, receipt.hostEvidence?.hookDefinitionHash);
|
|
253
|
+
return {
|
|
254
|
+
stopRevision: consumed.revision,
|
|
255
|
+
consumer: consumed.consumer!,
|
|
256
|
+
consumedAt: consumed.consumedAt!
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
coreOptions(options)
|
|
260
|
+
);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function sealCodexParentDelegation(
|
|
267
|
+
parentThreadId: string,
|
|
268
|
+
options: CodexBridgeOptions = {}
|
|
269
|
+
): Promise<OrchestrationResult> {
|
|
270
|
+
try {
|
|
271
|
+
const store = requiredStore(options);
|
|
272
|
+
const candidates = store.findByParent(parentThreadId);
|
|
273
|
+
if (!candidates.length) return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
274
|
+
for (const consumed of candidates.filter((record) => record.consumer)) {
|
|
275
|
+
const replayed = await sealCodexOrchestrationDelegation(consumed.state.startEvidence!.childThreadId, options);
|
|
276
|
+
if (replayed.error?.code !== 'ORCHESTRATION_DELEGATION_MISSING') return replayed;
|
|
277
|
+
}
|
|
278
|
+
const active = candidates.find((record) => !record.consumer);
|
|
279
|
+
if (!active) return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
280
|
+
const start = active.state.startEvidence!;
|
|
281
|
+
const child = active.state.child!;
|
|
282
|
+
if (!active.state.terminal) {
|
|
283
|
+
store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(start.childThreadId));
|
|
284
|
+
}
|
|
285
|
+
if (!store.read(start.childThreadId).state.stop) {
|
|
286
|
+
store.apply({
|
|
287
|
+
type: 'hook-stop',
|
|
288
|
+
sessionId: parentThreadId,
|
|
289
|
+
turnId: child.turnId,
|
|
290
|
+
childThreadId: start.childThreadId,
|
|
291
|
+
nativeAgent: start.nativeAgent,
|
|
292
|
+
source: 'parent-rollout'
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return sealCodexOrchestrationDelegation(start.childThreadId, options);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error instanceof Error && error.message === 'CODEX_TURN_NOT_TERMINAL') {
|
|
298
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'The matching Codex child has not completed');
|
|
299
|
+
}
|
|
300
|
+
return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function reconcileCodexOrchestrationDelegation(
|
|
305
|
+
childThreadId: string,
|
|
306
|
+
options: CodexBridgeOptions = {}
|
|
307
|
+
): OrchestrationResult {
|
|
308
|
+
return reconcileMatchingOrchestrationDelegation('codex', childThreadId, coreOptions(options));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export {
|
|
312
|
+
activateCodexOrchestrationDelegation,
|
|
313
|
+
activateCodexSpawnDelegation,
|
|
314
|
+
prepareCodexOrchestrationDelegation,
|
|
315
|
+
reconcileCodexOrchestrationDelegation,
|
|
316
|
+
sealCodexParentDelegation,
|
|
317
|
+
sealCodexOrchestrationDelegation
|
|
318
|
+
};
|
|
319
|
+
export type { CodexBridgeOptions };
|
|
@@ -71,6 +71,7 @@ const STAGE_ORDER = [
|
|
|
71
71
|
'review-plan',
|
|
72
72
|
'code',
|
|
73
73
|
'review-code',
|
|
74
|
+
'validation-run',
|
|
74
75
|
'task',
|
|
75
76
|
'other'
|
|
76
77
|
] as const;
|
|
@@ -81,6 +82,7 @@ function stageOf(name: string): string {
|
|
|
81
82
|
if (stem.startsWith('review-analysis')) return 'review-analysis';
|
|
82
83
|
if (stem.startsWith('review-plan')) return 'review-plan';
|
|
83
84
|
if (stem.startsWith('review-code')) return 'review-code';
|
|
85
|
+
if (stem.startsWith('validation-run')) return 'validation-run';
|
|
84
86
|
if (stem.startsWith('analysis')) return 'analysis';
|
|
85
87
|
if (stem.startsWith('plan')) return 'plan';
|
|
86
88
|
if (stem.startsWith('code')) return 'code';
|
|
@@ -0,0 +1,282 @@
|
|
|
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.ts';
|
|
7
|
+
import { detectEngine } from '../../sandbox/engine.ts';
|
|
8
|
+
import { runEngine } from '../../sandbox/shell.ts';
|
|
9
|
+
import { sandboxBranchLabel, sandboxLabel } from '../../sandbox/constants.ts';
|
|
10
|
+
import { fetchSandboxRows, selectSandboxContainer } from '../../sandbox/commands/list-running.ts';
|
|
11
|
+
import { containerNameCandidates } from '../../sandbox/constants.ts';
|
|
12
|
+
import { resolveSandboxTarget } from '../../sandbox/workspace-identity.ts';
|
|
13
|
+
import { sandboxControlPaths } from '../../sandbox/workspace-view.ts';
|
|
14
|
+
import {
|
|
15
|
+
appendSandboxControlAudit,
|
|
16
|
+
readActiveLease,
|
|
17
|
+
readSandboxControlStatus
|
|
18
|
+
} from '../../sandbox/control/state.ts';
|
|
19
|
+
import type { SandboxControlLease, SandboxControlManifest } from '../../sandbox/control/protocol.ts';
|
|
20
|
+
import {
|
|
21
|
+
SANDBOX_CONTROL_FUTURE_SKEW_MS,
|
|
22
|
+
SANDBOX_CONTROL_STATUS_STALE_MS
|
|
23
|
+
} from '../../sandbox/control/protocol.ts';
|
|
24
|
+
import { getProcessStartTime } from '../../server/process-state.ts';
|
|
25
|
+
import { assertGitWorktreeBinding } from '../../git/worktree-identity.ts';
|
|
26
|
+
|
|
27
|
+
const USAGE = `Usage: ai task validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] -- <command> [args...]`;
|
|
28
|
+
const MAX_TIMEOUT_MS = 60 * 60 * 1000;
|
|
29
|
+
const RECOVERY_GRACE_MS = 30_000;
|
|
30
|
+
|
|
31
|
+
type ValidateOptions = {
|
|
32
|
+
target: string;
|
|
33
|
+
scope: 'snapshot' | 'inplace';
|
|
34
|
+
timeoutMs: number;
|
|
35
|
+
format: 'text' | 'json';
|
|
36
|
+
command: string[];
|
|
37
|
+
help: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function parseValidateArgs(args: string[]): ValidateOptions {
|
|
41
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
42
|
+
return { target: '', scope: 'snapshot', timeoutMs: 300_000, format: 'text', command: [], help: true };
|
|
43
|
+
}
|
|
44
|
+
const separator = args.indexOf('--');
|
|
45
|
+
if (separator < 0) throw new Error(`${USAGE}\nA literal -- must separate validation options from the command.`);
|
|
46
|
+
const optionArgs = args.slice(0, separator);
|
|
47
|
+
const command = args.slice(separator + 1);
|
|
48
|
+
let scope: ValidateOptions['scope'] = 'snapshot';
|
|
49
|
+
let timeoutMs = 300_000;
|
|
50
|
+
let format: ValidateOptions['format'] = 'text';
|
|
51
|
+
const positionals: string[] = [];
|
|
52
|
+
for (let index = 0; index < optionArgs.length; index += 1) {
|
|
53
|
+
const arg = optionArgs[index]!;
|
|
54
|
+
if (arg === '--scope') {
|
|
55
|
+
const value = optionArgs[++index];
|
|
56
|
+
if (value !== 'snapshot' && value !== 'inplace') throw new Error('validate --scope must be snapshot or inplace');
|
|
57
|
+
scope = value;
|
|
58
|
+
} else if (arg === '--timeout') {
|
|
59
|
+
const value = Number(optionArgs[++index]);
|
|
60
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TIMEOUT_MS) throw new Error(`validate --timeout must be 1..${MAX_TIMEOUT_MS}`);
|
|
61
|
+
timeoutMs = value;
|
|
62
|
+
} else if (arg === '--format') {
|
|
63
|
+
const value = optionArgs[++index];
|
|
64
|
+
if (value !== 'text' && value !== 'json') throw new Error('validate --format must be text or json');
|
|
65
|
+
format = value;
|
|
66
|
+
} else if (arg.startsWith('-')) {
|
|
67
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
68
|
+
} else {
|
|
69
|
+
positionals.push(arg);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (positionals.length !== 1 || command.length === 0) throw new Error(USAGE);
|
|
73
|
+
return { target: positionals[0]!, scope, timeoutMs, format, command, help: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runValidationCommand(command: string[], cwd: string, scope: string, timeoutMs: number, json: boolean) {
|
|
77
|
+
const [file, ...args] = command;
|
|
78
|
+
const result = spawnSync(file!, args, {
|
|
79
|
+
cwd,
|
|
80
|
+
env: { ...process.env, AGENT_INFRA_VALIDATION_SCOPE: scope },
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
timeout: timeoutMs,
|
|
83
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
84
|
+
shell: process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file!)
|
|
85
|
+
});
|
|
86
|
+
if (json) {
|
|
87
|
+
if (result.stdout) process.stderr.write(result.stdout);
|
|
88
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
89
|
+
} else {
|
|
90
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
91
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
92
|
+
}
|
|
93
|
+
return { exitCode: result.status ?? 1, signal: result.signal ?? null };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function git(root: string, args: string[]): string {
|
|
97
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function waitForState(statusDir: string, predicate: (state: ReturnType<typeof readSandboxControlStatus>) => boolean, timeoutMs = 5_000): void {
|
|
101
|
+
const deadline = Date.now() + timeoutMs;
|
|
102
|
+
while (Date.now() < deadline) {
|
|
103
|
+
try {
|
|
104
|
+
const status = readSandboxControlStatus(statusDir);
|
|
105
|
+
if (predicate(status)) return;
|
|
106
|
+
} catch {
|
|
107
|
+
// Atomic status publication may be between renames.
|
|
108
|
+
}
|
|
109
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
110
|
+
}
|
|
111
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function snapshotValidation(repoRoot: string, commit: string, options: ValidateOptions) {
|
|
115
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
|
|
116
|
+
const worktree = path.join(tempRoot, 'worktree');
|
|
117
|
+
let cleanup = 'not-created';
|
|
118
|
+
try {
|
|
119
|
+
git(repoRoot, ['worktree', 'add', '--detach', worktree, commit]);
|
|
120
|
+
cleanup = 'pending';
|
|
121
|
+
return { ...runValidationCommand(options.command, worktree, 'snapshot', options.timeoutMs, options.format === 'json'), cleanup: () => cleanup };
|
|
122
|
+
} finally {
|
|
123
|
+
let cleanupError: unknown = null;
|
|
124
|
+
if (fs.existsSync(worktree)) {
|
|
125
|
+
try {
|
|
126
|
+
git(repoRoot, ['worktree', 'remove', '--force', worktree]);
|
|
127
|
+
cleanup = 'completed';
|
|
128
|
+
} catch (error) {
|
|
129
|
+
cleanupError = error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
git(repoRoot, ['worktree', 'prune']);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
cleanupError ??= error;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
cleanupError ??= error;
|
|
141
|
+
}
|
|
142
|
+
if (cleanupError) throw cleanupError;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function inplaceValidation(config: ReturnType<typeof loadConfig>, target: ReturnType<typeof resolveSandboxTarget>, options: ValidateOptions) {
|
|
147
|
+
const engine = detectEngine(config);
|
|
148
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
149
|
+
const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
|
|
150
|
+
if (!row) throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
|
|
151
|
+
const control = sandboxControlPaths({
|
|
152
|
+
base: config.controlBase, project: config.project,
|
|
153
|
+
container: row.name, identity: target.workspace
|
|
154
|
+
});
|
|
155
|
+
const manifest = JSON.parse(fs.readFileSync(control.manifestPath, 'utf8')) as SandboxControlManifest;
|
|
156
|
+
const initial = readSandboxControlStatus(control.statusDir);
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
if (initial.generation !== manifest.generation
|
|
159
|
+
|| now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
|
|
160
|
+
|| initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
161
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
|
|
162
|
+
}
|
|
163
|
+
if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
|
|
164
|
+
throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
|
|
165
|
+
}
|
|
166
|
+
const startTime = getProcessStartTime(process.pid);
|
|
167
|
+
if (!startTime) throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
|
|
168
|
+
const leasePath = path.join(control.root, 'lease.json');
|
|
169
|
+
const nonce = randomUUID();
|
|
170
|
+
const issuedAt = Date.now();
|
|
171
|
+
const lease: SandboxControlLease = {
|
|
172
|
+
version: 2, generation: manifest.generation, nonce,
|
|
173
|
+
owner: { pid: process.pid, startTime }, issuedAt,
|
|
174
|
+
expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
|
|
175
|
+
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
|
176
|
+
branch: target.branch, reason: 'manual-validation'
|
|
177
|
+
};
|
|
178
|
+
const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
179
|
+
const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
|
|
180
|
+
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
181
|
+
let stopped = false;
|
|
182
|
+
let restored = false;
|
|
183
|
+
try {
|
|
184
|
+
appendSandboxControlAudit(manifest, 'lease-acquire', { ownerPid: process.pid });
|
|
185
|
+
waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
186
|
+
if (row.running) {
|
|
187
|
+
runEngine(engine, 'docker', ['stop', row.name]);
|
|
188
|
+
stopped = true;
|
|
189
|
+
}
|
|
190
|
+
const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
191
|
+
const refreshed = selectSandboxContainer(
|
|
192
|
+
[...refreshedRows.running, ...refreshedRows.nonRunning],
|
|
193
|
+
containerNameCandidates(config, target.branch)
|
|
194
|
+
);
|
|
195
|
+
if (!refreshed || refreshed.running) throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
|
|
196
|
+
const activeLease = readActiveLease(manifest);
|
|
197
|
+
if (!activeLease || activeLease.nonce !== nonce) throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
198
|
+
assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
|
|
199
|
+
const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
|
|
200
|
+
return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
|
|
201
|
+
} finally {
|
|
202
|
+
const recoveryErrors: string[] = [];
|
|
203
|
+
try {
|
|
204
|
+
const currentBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
205
|
+
if (currentBranch !== originalBranch) git(manifest.worktreeRoot, ['switch', originalBranch]);
|
|
206
|
+
} catch {
|
|
207
|
+
try {
|
|
208
|
+
git(manifest.worktreeRoot, ['checkout', '--detach', originalHead]);
|
|
209
|
+
git(manifest.worktreeRoot, ['switch', originalBranch]);
|
|
210
|
+
} catch (fallbackError) {
|
|
211
|
+
recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (stopped) {
|
|
215
|
+
try {
|
|
216
|
+
runEngine(engine, 'docker', ['start', row.name]);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { nonce?: unknown };
|
|
223
|
+
if (currentLease.nonce !== nonce) throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
224
|
+
} catch (error) {
|
|
225
|
+
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
226
|
+
}
|
|
227
|
+
if (recoveryErrors.length === 0) {
|
|
228
|
+
try {
|
|
229
|
+
fs.unlinkSync(leasePath);
|
|
230
|
+
appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
|
|
231
|
+
waitForState(control.statusDir, (status) => status.state === 'healthy');
|
|
232
|
+
restored = true;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
try {
|
|
235
|
+
if (!fs.existsSync(leasePath)) {
|
|
236
|
+
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
237
|
+
}
|
|
238
|
+
appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
|
|
239
|
+
} catch (leaseError) {
|
|
240
|
+
recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
|
|
241
|
+
}
|
|
242
|
+
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (recoveryErrors.length > 0) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function validate(args: string[]): Promise<number> {
|
|
254
|
+
const options = parseValidateArgs(args);
|
|
255
|
+
if (options.help) {
|
|
256
|
+
process.stdout.write(`${USAGE}\n`);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
const config = loadConfig();
|
|
260
|
+
const target = resolveSandboxTarget(options.target, config.repoRoot);
|
|
261
|
+
const commit = git(config.repoRoot, ['rev-parse', target.branch]);
|
|
262
|
+
const startedAt = new Date().toISOString();
|
|
263
|
+
const result = options.scope === 'snapshot'
|
|
264
|
+
? snapshotValidation(config.repoRoot, commit, options)
|
|
265
|
+
: inplaceValidation(config, target, options);
|
|
266
|
+
const evidence = {
|
|
267
|
+
version: 1,
|
|
268
|
+
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
|
269
|
+
branch: target.branch,
|
|
270
|
+
scope: options.scope,
|
|
271
|
+
commit,
|
|
272
|
+
command: path.basename(options.command[0]!),
|
|
273
|
+
startedAt,
|
|
274
|
+
completedAt: new Date().toISOString(),
|
|
275
|
+
exitCode: result.exitCode,
|
|
276
|
+
signal: result.signal,
|
|
277
|
+
cleanup: result.cleanup()
|
|
278
|
+
};
|
|
279
|
+
if (options.format === 'json') process.stdout.write(`${JSON.stringify(evidence)}\n`);
|
|
280
|
+
else process.stdout.write(`Validation ${result.exitCode === 0 ? 'passed' : 'failed'} (${options.scope}, ${evidence.command}, cleanup=${evidence.cleanup}).\n`);
|
|
281
|
+
return result.exitCode;
|
|
282
|
+
}
|