@fitlab-ai/agent-infra 0.11.12 → 0.11.14
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 +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +29 -20
- package/dist/lib/internal/task-lifecycle.js +4 -2
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +11 -6
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +15 -110
- package/dist/lib/sandbox/control/executor.js +9 -59
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +75 -9
- package/dist/lib/sandbox/control/protocol.js +11 -50
- package/dist/lib/sandbox/control/server.js +203 -253
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -52
- package/dist/lib/sandbox/recovery.js +30 -12
- package/dist/lib/sandbox/removal.js +37 -3
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +89 -6
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +41 -17
- package/dist/lib/task/control-recovery.js +2 -4
- package/dist/lib/task/finalization.js +27 -2
- package/dist/lib/task/lifecycle.js +294 -63
- package/dist/lib/task/orchestration.js +26 -72
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +4 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +21 -27
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +33 -19
- package/lib/internal/task-lifecycle.ts +4 -2
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/github-client.ts +34 -3
- package/lib/platform/release-notes.ts +2 -2
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +11 -5
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +17 -138
- package/lib/sandbox/control/executor.ts +8 -65
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +75 -10
- package/lib/sandbox/control/protocol.ts +12 -62
- package/lib/sandbox/control/server.ts +11 -61
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +93 -68
- package/lib/sandbox/recovery.ts +37 -19
- package/lib/sandbox/removal.ts +36 -5
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +81 -7
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +41 -26
- package/lib/task/control-recovery.ts +3 -5
- package/lib/task/finalization.ts +29 -2
- package/lib/task/lifecycle.ts +282 -57
- package/lib/task/orchestration.ts +30 -68
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +3 -9
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/sandbox/control/agent-operation.ts +0 -49
package/dist/bin/internal-cli.js
CHANGED
|
@@ -17,9 +17,9 @@ const taskWorkflowCommand = command === 'task-artifact'
|
|
|
17
17
|
const taskPlatformRecoveryCommand = command === 'task-platform-recovery';
|
|
18
18
|
const manualValidationWorkflowCommand = command === 'manual-validation';
|
|
19
19
|
const taskControlMarkers = [
|
|
20
|
-
'AGENT_INFRA_TASK_ID', '
|
|
20
|
+
'AGENT_INFRA_TASK_ID', 'AGENT_INFRA_CONTROL_GENERATION',
|
|
21
21
|
'AGENT_INFRA_CONTROL_DIR', 'AGENT_INFRA_CONTROL_STATUS_DIR', 'AGENT_INFRA_RUNTIME_DIR',
|
|
22
|
-
'
|
|
22
|
+
'AGENT_INFRA_EXECUTOR_MANIFEST'
|
|
23
23
|
];
|
|
24
24
|
const markerlessHelp = ['--help', '-h'].includes(process.argv[3] ?? '')
|
|
25
25
|
&& taskControlMarkers.every((key) => !process.env[key]);
|
|
@@ -115,11 +115,6 @@ if (!controlRouted && !taskViewGuardFailed && internalRouteRegistered)
|
|
|
115
115
|
await codexLifecycle(process.argv.slice(3));
|
|
116
116
|
break;
|
|
117
117
|
}
|
|
118
|
-
case 'codex-sandbox-controller': {
|
|
119
|
-
const { codexSandboxController } = await import('../lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js');
|
|
120
|
-
await codexSandboxController(process.argv.slice(3));
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
118
|
case 'git-workflow': {
|
|
124
119
|
const { gitWorkflow } = await import('../lib/internal/git-workflow.js');
|
|
125
120
|
gitWorkflow(process.argv.slice(3));
|
|
@@ -434,9 +434,6 @@ function defineAgentClientAdapter(candidate) {
|
|
|
434
434
|
...(candidate.orchestrationAdapter === undefined
|
|
435
435
|
? {}
|
|
436
436
|
: { orchestrationAdapter: Object.freeze({ ...candidate.orchestrationAdapter }) }),
|
|
437
|
-
...(candidate.sandboxControlOperation === undefined
|
|
438
|
-
? {}
|
|
439
|
-
: { sandboxControlOperation: Object.freeze({ ...candidate.sandboxControlOperation }) }),
|
|
440
437
|
project: Object.freeze({
|
|
441
438
|
ownedPathPrefixes: Object.freeze(paths),
|
|
442
439
|
...projectAssets,
|
|
@@ -114,7 +114,7 @@ function validateCodexLifecycleHookConfig(value) {
|
|
|
114
114
|
if (!valid)
|
|
115
115
|
throw new Error('CODEX_PREFLIGHT_HOOKS_INVALID: lifecycle hooks are invalid');
|
|
116
116
|
}
|
|
117
|
-
function parseCodexHooksList(value, repoRoot
|
|
117
|
+
function parseCodexHooksList(value, repoRoot) {
|
|
118
118
|
const root = object(value);
|
|
119
119
|
const entries = Array.isArray(root?.data) ? root.data : [];
|
|
120
120
|
const entry = entries.map(object).find((candidate) => candidate?.cwd === repoRoot);
|
|
@@ -145,10 +145,8 @@ function parseCodexHooksList(value, repoRoot, expectedSource = 'direct-host') {
|
|
|
145
145
|
&& hook.currentHash.length > 0
|
|
146
146
|
&& hook.sourcePathDigest.length === 64
|
|
147
147
|
&& hook.pluginId === null
|
|
148
|
-
&& (
|
|
149
|
-
|
|
150
|
-
: (hook.source === 'project' && hook.trustStatus === 'trusted')
|
|
151
|
-
|| hook.isManaged && hook.trustStatus === 'managed')));
|
|
148
|
+
&& (hook.source === 'project' && hook.trustStatus === 'trusted'
|
|
149
|
+
|| hook.isManaged && hook.trustStatus === 'managed')));
|
|
152
150
|
if (!valid)
|
|
153
151
|
throw new Error('CODEX_PREFLIGHT_HOOKS_NOT_LOADED: lifecycle hooks are not loaded for this workspace');
|
|
154
152
|
return Object.freeze(hooks);
|
|
@@ -546,7 +544,7 @@ async function discoverLifecycleHooks(repoRoot) {
|
|
|
546
544
|
let discoveredHooks;
|
|
547
545
|
try {
|
|
548
546
|
await transport.initialize();
|
|
549
|
-
discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot
|
|
547
|
+
discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot);
|
|
550
548
|
}
|
|
551
549
|
finally {
|
|
552
550
|
transport.close();
|
|
@@ -564,9 +562,7 @@ async function discoverLifecycleHooks(repoRoot) {
|
|
|
564
562
|
throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hooks do not share one exact source');
|
|
565
563
|
}
|
|
566
564
|
const source = [...sources][0];
|
|
567
|
-
const hookSource =
|
|
568
|
-
? 'isolated-user'
|
|
569
|
-
: source === 'managed' ? 'managed' : 'project';
|
|
565
|
+
const hookSource = source === 'managed' ? 'managed' : 'project';
|
|
570
566
|
return {
|
|
571
567
|
discoveredHooks,
|
|
572
568
|
hookProvenance: Object.freeze({
|
|
@@ -4,7 +4,6 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread } from './app-server.js';
|
|
6
6
|
import { createCodexLifecycleStore } from './store.js';
|
|
7
|
-
import { verifyCodexSandboxControllerContextWithWarnings } from './sandbox-controller.js';
|
|
8
7
|
import { resolveTaskRef } from '../../../task/resolve-ref.js';
|
|
9
8
|
import { resolveTaskContext } from '../../../task/resolve-ref.js';
|
|
10
9
|
import { hasSealableOrchestrationDelegation, readRun } from '../../../task/orchestration.js';
|
|
@@ -78,10 +77,7 @@ function hookDefinitionHash() {
|
|
|
78
77
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
79
78
|
}
|
|
80
79
|
function resolveLifecycleTaskContext() {
|
|
81
|
-
const
|
|
82
|
-
const taskId = controllerPath
|
|
83
|
-
? verifyCodexSandboxControllerContextWithWarnings(controllerPath, { repoRoot: process.cwd() }).context.taskId
|
|
84
|
-
: process.env.AGENT_INFRA_TASK_ID;
|
|
80
|
+
const taskId = process.env.AGENT_INFRA_TASK_ID;
|
|
85
81
|
const context = taskId
|
|
86
82
|
? resolveTaskRef(taskId, { repoRoot: process.cwd() })
|
|
87
83
|
: resolveTaskContext(undefined, { repoRoot: process.cwd() });
|
|
@@ -136,14 +136,10 @@ function hostEvidenceMatches(record, receipt, host, start) {
|
|
|
136
136
|
&& host.spawnToolUseId === start.spawnToolUseId
|
|
137
137
|
&& Boolean(host.spawnObservedAt)
|
|
138
138
|
&& host.spawnObservedAt === record.spawnObservedAt
|
|
139
|
-
&& typeof host.controllerInstanceDigest === 'string'
|
|
140
|
-
&& typeof host.controlGeneration === 'string'
|
|
141
139
|
&& hostRevisionMatches(record, receipt, host);
|
|
142
140
|
}
|
|
143
141
|
function hostRevisionMatches(record, receipt, host) {
|
|
144
|
-
return
|
|
145
|
-
&& typeof host.controlGeneration === 'string'
|
|
146
|
-
&& host.startRevision >= 1
|
|
142
|
+
return host.startRevision >= 1
|
|
147
143
|
&& record.revision > host.startRevision
|
|
148
144
|
&& record.state.child?.childThreadId === receipt.childId
|
|
149
145
|
&& record.state.stop?.childThreadId === receipt.childId;
|
|
@@ -215,7 +215,7 @@ function createCodexLifecycleStore(options) {
|
|
|
215
215
|
throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
216
216
|
return matches[0];
|
|
217
217
|
}
|
|
218
|
-
function
|
|
218
|
+
function consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, taskLockHeld) {
|
|
219
219
|
if (!consumer.trim())
|
|
220
220
|
throw new Error('Codex lifecycle consumer is required');
|
|
221
221
|
const current = read(childThreadId);
|
|
@@ -223,8 +223,11 @@ function createCodexLifecycleStore(options) {
|
|
|
223
223
|
if (!binding)
|
|
224
224
|
throw new Error('Codex lifecycle task receipt binding is missing');
|
|
225
225
|
const spawn = current.state.spawn;
|
|
226
|
-
if (!spawn)
|
|
227
|
-
throw new Error(
|
|
226
|
+
if (!spawn) {
|
|
227
|
+
throw new Error(taskLockHeld
|
|
228
|
+
? 'Codex lifecycle task receipt binding is missing'
|
|
229
|
+
: 'Codex lifecycle spawn evidence is missing');
|
|
230
|
+
}
|
|
228
231
|
const key = recordKey(spawn.sessionId, spawn.turnId, spawn.toolUseId);
|
|
229
232
|
return update(binding, key, (latest) => {
|
|
230
233
|
if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
|
|
@@ -241,30 +244,13 @@ function createCodexLifecycleStore(options) {
|
|
|
241
244
|
if (latest.consumer)
|
|
242
245
|
return latest;
|
|
243
246
|
return Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
|
|
244
|
-
});
|
|
247
|
+
}, taskLockHeld);
|
|
248
|
+
}
|
|
249
|
+
function consume(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
|
|
250
|
+
return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, false);
|
|
245
251
|
}
|
|
246
252
|
function consumeWithinTaskLock(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
|
|
247
|
-
|
|
248
|
-
throw new Error('Codex lifecycle consumer is required');
|
|
249
|
-
const current = read(childThreadId);
|
|
250
|
-
const binding = expectedTaskBinding ?? current.taskBinding;
|
|
251
|
-
if (!binding || !current.state.spawn)
|
|
252
|
-
throw new Error('Codex lifecycle task receipt binding is missing');
|
|
253
|
-
const key = recordKey(current.state.spawn.sessionId, current.state.spawn.turnId, current.state.spawn.toolUseId);
|
|
254
|
-
return update(binding, key, (latest) => {
|
|
255
|
-
if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
|
|
256
|
-
throw new Error('Codex lifecycle task binding does not match the expected receipt');
|
|
257
|
-
}
|
|
258
|
-
if (latest.consumer && latest.consumer !== consumer) {
|
|
259
|
-
throw new Error(`Codex lifecycle evidence was already consumed by '${latest.consumer}'`);
|
|
260
|
-
}
|
|
261
|
-
if (latest.state.status !== 'stop-ready')
|
|
262
|
-
throw new Error('Codex lifecycle evidence is not stop-ready');
|
|
263
|
-
if (expectedHookDefinitionHash && latest.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash) {
|
|
264
|
-
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
265
|
-
}
|
|
266
|
-
return latest.consumer ? latest : Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
|
|
267
|
-
}, true);
|
|
253
|
+
return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, true);
|
|
268
254
|
}
|
|
269
255
|
function expireBefore(cutoff) {
|
|
270
256
|
let changed = 0;
|
|
@@ -215,9 +215,7 @@ async function activateCodexOrchestrationDelegation(childThreadId, options = {})
|
|
|
215
215
|
capabilitySessionId: evidence.parentThreadId,
|
|
216
216
|
capabilityTurnId: evidence.parentTurnId,
|
|
217
217
|
spawnToolUseId: evidence.spawnToolUseId,
|
|
218
|
-
spawnObservedAt: record.spawnObservedAt ?? undefined
|
|
219
|
-
controllerInstanceDigest: null,
|
|
220
|
-
controlGeneration: null
|
|
218
|
+
spawnObservedAt: record.spawnObservedAt ?? undefined
|
|
221
219
|
}
|
|
222
220
|
}
|
|
223
221
|
}, coreOptions(options));
|
|
@@ -3,7 +3,6 @@ import { hostJoin } from '../../sandbox/engines/wsl2-paths.js';
|
|
|
3
3
|
import { codexBeforeContainerCreateHook, codexRecoveryChecks } from './codex-sandbox.js';
|
|
4
4
|
import { parseCodexLifecycleBinding } from './codex-lifecycle/binding.js';
|
|
5
5
|
import { isCodexDelegationReceiptEvidence, isCodexLifecycleActivationEvidence } from './codex-lifecycle/evidence.js';
|
|
6
|
-
import { codexControllerOperation } from './codex-lifecycle/controller-operation.js';
|
|
7
6
|
function codexCarrierError(code) {
|
|
8
7
|
const error = new Error(code);
|
|
9
8
|
error.name = code;
|
|
@@ -72,7 +71,6 @@ const codexAdapter = defineAgentClientAdapter({
|
|
|
72
71
|
return recoverStartedLifecycleFromAdapter(...args);
|
|
73
72
|
}
|
|
74
73
|
},
|
|
75
|
-
sandboxControlOperation: codexControllerOperation,
|
|
76
74
|
project: {
|
|
77
75
|
ownedPathPrefixes: ['.codex/'],
|
|
78
76
|
managed: ['.codex/hooks.json', '.codex/agents/'],
|
|
@@ -35,7 +35,6 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
|
|
|
35
35
|
'sandbox-control': ['serve', 'execute', 'recover', 'client'],
|
|
36
36
|
'agent-client': ['next-steps', 'model-selection', 'launch-carrier'],
|
|
37
37
|
'codex-lifecycle': ['preflight', 'hook-event', 'resolve-start', 'resolve-stop', 'consume'],
|
|
38
|
-
'codex-sandbox-controller': ['verify-context', 'run'],
|
|
39
38
|
'git-workflow': ['inspect', 'preview-tree', 'snapshot', 'compare-trees', 'commit', 'push-rebased'],
|
|
40
39
|
'task-delivery': ['deliver'],
|
|
41
40
|
'release-workflow': ['inspect', 'prepare', 'publish', 'post-prepare', 'post-publish'],
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
import { recoverSandboxControl, requestSandboxControl,
|
|
2
|
-
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
|
|
1
|
+
import { recoverSandboxControl, requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.js';
|
|
3
2
|
import { serveSandboxControl } from '../sandbox/control/server.js';
|
|
4
3
|
import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
|
|
5
4
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
6
5
|
import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.js';
|
|
7
|
-
import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
|
|
8
|
-
function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null, requestId = null) {
|
|
9
|
-
process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
|
|
10
|
-
}
|
|
11
|
-
function finalizationErrorStatus(error) {
|
|
12
|
-
if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
|
|
13
|
-
return 'unknown';
|
|
14
|
-
return error.retryable ? 'blocked' : 'failed';
|
|
15
|
-
}
|
|
16
6
|
function writeClientError(error) {
|
|
17
7
|
process.stderr.write(`${error.detail.message}\n`);
|
|
18
8
|
if (error.requestId)
|
|
@@ -31,72 +21,6 @@ function recoveredExitCode(response) {
|
|
|
31
21
|
}
|
|
32
22
|
return response.exitCode ?? 1;
|
|
33
23
|
}
|
|
34
|
-
function finalizationResponseOutput(response) {
|
|
35
|
-
try {
|
|
36
|
-
const value = JSON.parse(response.stdout);
|
|
37
|
-
if (value.version === 2
|
|
38
|
-
&& value.status
|
|
39
|
-
&& ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
|
|
40
|
-
&& (typeof value.changed === 'boolean' || value.changed === null)) {
|
|
41
|
-
return serializeTaskFinalizationEnvelope({
|
|
42
|
-
status: value.status,
|
|
43
|
-
changed: value.changed,
|
|
44
|
-
accepted: true,
|
|
45
|
-
requestId: response.id,
|
|
46
|
-
result: value.result ?? null,
|
|
47
|
-
error: value.error ?? null
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// Preserve non-envelope output for diagnostics from the executor.
|
|
53
|
-
}
|
|
54
|
-
return response.stdout;
|
|
55
|
-
}
|
|
56
|
-
function sandboxFinalizationClient(args) {
|
|
57
|
-
if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
|
|
58
|
-
const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
|
|
59
|
-
writeFinalizationEnvelope('failed', false, error);
|
|
60
|
-
process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
|
|
61
|
-
process.exitCode = 1;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const agent = normalizeAgentToken(args[3]);
|
|
65
|
-
if (!agent) {
|
|
66
|
-
const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
|
|
67
|
-
writeFinalizationEnvelope('failed', false, error);
|
|
68
|
-
process.exitCode = 1;
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
let response;
|
|
72
|
-
try {
|
|
73
|
-
response = requestSandboxTaskFinalization({ agent });
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
if (!(error instanceof SandboxControlClientError))
|
|
77
|
-
throw error;
|
|
78
|
-
const status = finalizationErrorStatus(error.detail);
|
|
79
|
-
writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
|
|
80
|
-
writeClientError(error);
|
|
81
|
-
process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (response.phase === 'rejected') {
|
|
85
|
-
const error = response.error ?? {
|
|
86
|
-
code: 'SANDBOX_CONTROL_REJECTED',
|
|
87
|
-
message: 'sandbox control rejected the finalization request',
|
|
88
|
-
retryable: false
|
|
89
|
-
};
|
|
90
|
-
const status = finalizationErrorStatus(error);
|
|
91
|
-
writeFinalizationEnvelope(status, error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error, status === 'unknown' ? null : false, null, response.id);
|
|
92
|
-
process.stderr.write(`${error.message}\n`);
|
|
93
|
-
process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
process.stdout.write(finalizationResponseOutput(response));
|
|
97
|
-
process.stderr.write(response.stderr);
|
|
98
|
-
process.exitCode = response.exitCode ?? 1;
|
|
99
|
-
}
|
|
100
24
|
async function sandboxControl(args) {
|
|
101
25
|
if (!ensureInternalHandlerRoute('sandbox-control', args))
|
|
102
26
|
return;
|
|
@@ -152,10 +76,6 @@ async function sandboxControl(args) {
|
|
|
152
76
|
}
|
|
153
77
|
if (internalHandlerRoute('sandbox-control', 'client', operation ?? '')) {
|
|
154
78
|
const [family = '', ...commandArgs] = rest;
|
|
155
|
-
if (family === 'task-finalization') {
|
|
156
|
-
sandboxFinalizationClient(commandArgs);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
79
|
let response;
|
|
160
80
|
try {
|
|
161
81
|
response = requestSandboxControl({ family, args: commandArgs });
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../task/control-authority.js';
|
|
2
3
|
import { applyTaskFinalization } from '../task/finalization.js';
|
|
3
4
|
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
5
|
import { resolveSandboxControlTransport } from './task-operation-registry.js';
|
|
5
|
-
import {
|
|
6
|
+
import { quiesceSandboxControlForTaskCompletion } from '../sandbox/control/lifecycle.js';
|
|
7
|
+
import { loadSandboxControlLocation } from '../sandbox/config.js';
|
|
6
8
|
import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
|
|
9
|
+
import { verifyTaskEvent } from '../task/verification.js';
|
|
7
10
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
8
11
|
const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
|
|
9
12
|
function envelope(status, changed, accepted, result, error, requestId = null) {
|
|
@@ -64,28 +67,34 @@ async function taskFinalization(args = []) {
|
|
|
64
67
|
};
|
|
65
68
|
const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
|
|
66
69
|
if (transport.kind === 'sandbox-local') {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const detail = clientError
|
|
77
|
-
? clientError.detail
|
|
78
|
-
: { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
|
|
79
|
-
const status = detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
|
|
80
|
-
? 'unknown'
|
|
81
|
-
: detail.retryable ? 'blocked' : 'failed';
|
|
82
|
-
const changed = status === 'unknown' ? null : false;
|
|
83
|
-
process.stdout.write(envelope(status, changed, clientError?.accepted ?? false, null, detail, clientError?.requestId ?? null));
|
|
84
|
-
process.exitCode = exitCode(status);
|
|
70
|
+
const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
71
|
+
const taskId = process.env.AGENT_INFRA_TASK_ID;
|
|
72
|
+
const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
73
|
+
const controlRootId = process.env.AGENT_INFRA_CONTROL_ROOT_ID;
|
|
74
|
+
const controlRoot = process.env.AGENT_INFRA_CONTROL_ROOT_DIR;
|
|
75
|
+
if (!statusDir || !taskId || !generation || !controlRootId || !controlRoot || taskId !== resolved.taskId) {
|
|
76
|
+
const detail = { code: 'TASK_CONTROL_TRANSPORT_INVALID', message: 'sandbox task control identity is incomplete', retryable: false };
|
|
77
|
+
process.stdout.write(envelope('failed', false, true, null, detail));
|
|
78
|
+
process.exitCode = 1;
|
|
85
79
|
return;
|
|
86
80
|
}
|
|
81
|
+
const result = await applyTaskFinalization(boundOperation.request, {
|
|
82
|
+
repoRoot,
|
|
83
|
+
beforeLifecycle: async (currentTaskId) => {
|
|
84
|
+
if (currentTaskId !== taskId)
|
|
85
|
+
throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
|
|
86
|
+
await quiesceSandboxControlForTaskCompletion(controlRoot, { taskId, generation, controlRootId, statusDir });
|
|
87
|
+
},
|
|
88
|
+
preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
|
|
89
|
+
});
|
|
90
|
+
const status = result.status === 'prepared' ? 'failed' : result.status;
|
|
91
|
+
process.stdout.write(envelope(status, result.changed, true, result, result.error
|
|
92
|
+
?? (result.status === 'prepared' ? { code: 'TASK_FINALIZATION_PREPARATION_INCOMPLETE', message: 'finalization did not commit', retryable: true } : null)));
|
|
93
|
+
process.exitCode = exitCode(status);
|
|
94
|
+
return;
|
|
87
95
|
}
|
|
88
|
-
const
|
|
96
|
+
const controlLocation = loadSandboxControlLocation(repoRoot);
|
|
97
|
+
const result = await dispatchTaskControlOperation(createDirectHostExecutionContext({ repoRoot, ...(controlLocation ?? {}) }), boundOperation);
|
|
89
98
|
if (result.status === 'prepared') {
|
|
90
99
|
const detail = {
|
|
91
100
|
code: 'TASK_FINALIZATION_DIRECT_COMMIT_REQUIRED',
|
|
@@ -2,7 +2,8 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
|
|
|
2
2
|
import { lifecycleIntentCatalog } from '../task/lifecycle.js';
|
|
3
3
|
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
4
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
5
|
-
|
|
5
|
+
import { loadSandboxControlLocation } from '../sandbox/config.js';
|
|
6
|
+
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.filter((intent) => intent !== 'complete').join(', ')}\nrecover-started: --auto\n`;
|
|
6
7
|
function usageFailure(message) {
|
|
7
8
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
|
|
8
9
|
process.stderr.write(USAGE);
|
|
@@ -52,7 +53,8 @@ async function taskLifecycle(args = []) {
|
|
|
52
53
|
}
|
|
53
54
|
repoRoot = resolved.repoRoot;
|
|
54
55
|
}
|
|
55
|
-
const
|
|
56
|
+
const controlLocation = loadSandboxControlLocation(repoRoot);
|
|
57
|
+
const result = await dispatchTaskControlOperation(createDirectHostExecutionContext({ repoRoot, ...(controlLocation ?? {}) }), operation);
|
|
56
58
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
57
59
|
if (result.status === 'failed' || result.status === 'owner-unknown' || result.status === 'conflict')
|
|
58
60
|
process.exitCode = 1;
|
|
@@ -25,8 +25,6 @@ function internalTaskRoutes() {
|
|
|
25
25
|
descriptor('internal', 'codex-lifecycle', 'resolve-start', 'conditional', 'progress'),
|
|
26
26
|
descriptor('internal', 'codex-lifecycle', 'resolve-stop', 'conditional', 'progress'),
|
|
27
27
|
descriptor('internal', 'codex-lifecycle', 'consume', 'conditional', 'progress'),
|
|
28
|
-
descriptor('internal', 'codex-sandbox-controller', 'verify-context', 'task-bound', 'diagnostic'),
|
|
29
|
-
descriptor('internal', 'codex-sandbox-controller', 'run', 'task-bound', 'progress'),
|
|
30
28
|
descriptor('internal', 'git-workflow', 'inspect', 'conditional', 'diagnostic'),
|
|
31
29
|
descriptor('internal', 'git-workflow', 'preview-tree', 'conditional', 'diagnostic'),
|
|
32
30
|
descriptor('internal', 'git-workflow', 'snapshot', 'conditional', 'diagnostic'),
|
|
@@ -175,7 +173,7 @@ export const TASK_OPERATION_DESCRIPTORS = Object.freeze([
|
|
|
175
173
|
...PUBLIC_OPERATION_DESCRIPTORS
|
|
176
174
|
]);
|
|
177
175
|
export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
|
|
178
|
-
'task-create', 'task-qualification', 'sandbox-control', 'agent-client', 'codex-lifecycle',
|
|
176
|
+
'task-create', 'task-qualification', 'sandbox-control', 'agent-client', 'codex-lifecycle',
|
|
179
177
|
'git-workflow', 'task-delivery', 'release-workflow', 'platform-release-notes', 'platform-security', 'platform-metadata', 'platform-context',
|
|
180
178
|
'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
|
|
181
179
|
'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
|
|
@@ -2,26 +2,15 @@ import { execFileSync, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
6
5
|
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
6
|
import { resolveSandboxTarget } from '../sandbox/workspace-identity.js';
|
|
13
|
-
import {
|
|
14
|
-
import { appendSandboxControlAudit, readActiveLease, readSandboxControlStatus } from '../sandbox/control/state.js';
|
|
15
|
-
import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
|
|
16
|
-
import { SANDBOX_CONTROL_FUTURE_SKEW_MS, SANDBOX_CONTROL_STATUS_STALE_MS } from '../sandbox/control/protocol.js';
|
|
17
|
-
import { getProcessStartTime } from '../server/process-state.js';
|
|
7
|
+
import { resolveBranchWorktree } from '../git/branch-worktree.js';
|
|
18
8
|
import { assertGitWorktreeBinding } from '../git/worktree-identity.js';
|
|
19
9
|
import { createManualValidationEvidence, manualValidationEvidenceSummary } from '../task/manual-validation-evidence.js';
|
|
20
10
|
import { writeJsonAtomic } from '../task/manual-validation-shared.js';
|
|
21
11
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
22
12
|
const USAGE = `Usage: agent-infra-internal task-validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] [--evidence-file <path>] -- <command> [args...]`;
|
|
23
13
|
const MAX_TIMEOUT_MS = 60 * 60 * 1000;
|
|
24
|
-
const RECOVERY_GRACE_MS = 30_000;
|
|
25
14
|
export function parseValidateArgs(args) {
|
|
26
15
|
if (args.includes('--help') || args.includes('-h')) {
|
|
27
16
|
return { target: '', scope: 'snapshot', timeoutMs: 300_000, format: 'text', evidenceFile: null, command: [], help: true };
|
|
@@ -100,21 +89,6 @@ function runValidationCommand(command, cwd, scope, timeoutMs, json) {
|
|
|
100
89
|
function git(root, args) {
|
|
101
90
|
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
102
91
|
}
|
|
103
|
-
function waitForState(statusDir, predicate, timeoutMs = 5_000) {
|
|
104
|
-
const deadline = Date.now() + timeoutMs;
|
|
105
|
-
while (Date.now() < deadline) {
|
|
106
|
-
try {
|
|
107
|
-
const status = readSandboxControlStatus(statusDir);
|
|
108
|
-
if (predicate(status))
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// Atomic status publication may be between renames.
|
|
113
|
-
}
|
|
114
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
115
|
-
}
|
|
116
|
-
throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
|
|
117
|
-
}
|
|
118
92
|
function snapshotValidation(repoRoot, commit, options) {
|
|
119
93
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
|
|
120
94
|
const worktree = path.join(tempRoot, 'worktree');
|
|
@@ -146,117 +120,33 @@ function snapshotValidation(repoRoot, commit, options) {
|
|
|
146
120
|
}
|
|
147
121
|
}
|
|
148
122
|
function inplaceValidation(config, target, options) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
base: config.controlBase, repoRoot: config.repoRoot, project: config.project,
|
|
156
|
-
container: row.name, identity: target.workspace
|
|
157
|
-
});
|
|
158
|
-
const manifest = readSandboxControlManifest(control.manifestPath);
|
|
159
|
-
const initial = readSandboxControlStatus(control.statusDir);
|
|
160
|
-
const now = Date.now();
|
|
161
|
-
if (initial.generation !== manifest.generation
|
|
162
|
-
|| now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
|
|
163
|
-
|| initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
164
|
-
throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
|
|
165
|
-
}
|
|
166
|
-
if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
|
|
167
|
-
throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
|
|
168
|
-
}
|
|
169
|
-
const startTime = getProcessStartTime(process.pid);
|
|
170
|
-
if (!startTime)
|
|
171
|
-
throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
|
|
172
|
-
const leasePath = path.join(control.root, 'lease.json');
|
|
173
|
-
const nonce = randomUUID();
|
|
174
|
-
const issuedAt = Date.now();
|
|
175
|
-
const lease = {
|
|
176
|
-
version: 2, generation: manifest.generation, nonce,
|
|
177
|
-
owner: { pid: process.pid, startTime }, issuedAt,
|
|
178
|
-
expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
|
|
179
|
-
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
|
180
|
-
branch: target.branch, reason: 'manual-validation'
|
|
181
|
-
};
|
|
182
|
-
const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
183
|
-
const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
|
|
184
|
-
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
185
|
-
let stopped = false;
|
|
123
|
+
const targetRoot = resolveBranchWorktree(config.repoRoot, target.branch);
|
|
124
|
+
if (!targetRoot)
|
|
125
|
+
throw new Error(`TASK_VALIDATION_WORKTREE_NOT_FOUND: ${target.branch}`);
|
|
126
|
+
const worktree = assertGitWorktreeBinding(config.repoRoot, targetRoot, target.branch);
|
|
127
|
+
const originalBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
128
|
+
const originalHead = git(worktree.worktreeRoot, ['rev-parse', 'HEAD']);
|
|
186
129
|
let restored = false;
|
|
187
130
|
try {
|
|
188
|
-
|
|
189
|
-
waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
190
|
-
if (row.running) {
|
|
191
|
-
runEngine(engine, 'docker', ['stop', row.name]);
|
|
192
|
-
stopped = true;
|
|
193
|
-
}
|
|
194
|
-
const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
195
|
-
const refreshed = selectSandboxContainer([...refreshedRows.running, ...refreshedRows.nonRunning], containerNameCandidates(config, target.branch));
|
|
196
|
-
if (!refreshed || refreshed.running)
|
|
197
|
-
throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
|
|
198
|
-
const activeLease = readActiveLease(manifest);
|
|
199
|
-
if (!activeLease || activeLease.nonce !== nonce)
|
|
200
|
-
throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
201
|
-
assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
|
|
202
|
-
const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
|
|
131
|
+
const result = runValidationCommand(options.command, worktree.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
|
|
203
132
|
return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
|
|
204
133
|
}
|
|
205
134
|
finally {
|
|
206
|
-
const recoveryErrors = [];
|
|
207
135
|
try {
|
|
208
|
-
const currentBranch = git(
|
|
136
|
+
const currentBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
209
137
|
if (currentBranch !== originalBranch)
|
|
210
|
-
git(
|
|
138
|
+
git(worktree.worktreeRoot, ['switch', originalBranch]);
|
|
211
139
|
}
|
|
212
140
|
catch {
|
|
213
141
|
try {
|
|
214
|
-
git(
|
|
215
|
-
git(
|
|
216
|
-
}
|
|
217
|
-
catch (fallbackError) {
|
|
218
|
-
recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (stopped) {
|
|
222
|
-
try {
|
|
223
|
-
runEngine(engine, 'docker', ['start', row.name]);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
try {
|
|
230
|
-
const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
|
|
231
|
-
if (currentLease.nonce !== nonce)
|
|
232
|
-
throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
236
|
-
}
|
|
237
|
-
if (recoveryErrors.length === 0) {
|
|
238
|
-
try {
|
|
239
|
-
fs.unlinkSync(leasePath);
|
|
240
|
-
appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
|
|
241
|
-
waitForState(control.statusDir, (status) => status.state === 'healthy');
|
|
242
|
-
restored = true;
|
|
142
|
+
git(worktree.worktreeRoot, ['checkout', '--detach', originalHead]);
|
|
143
|
+
git(worktree.worktreeRoot, ['switch', originalBranch]);
|
|
243
144
|
}
|
|
244
145
|
catch (error) {
|
|
245
|
-
|
|
246
|
-
if (!fs.existsSync(leasePath)) {
|
|
247
|
-
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
248
|
-
}
|
|
249
|
-
appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
|
|
250
|
-
}
|
|
251
|
-
catch (leaseError) {
|
|
252
|
-
recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
|
|
253
|
-
}
|
|
254
|
-
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
146
|
+
throw new Error(`TASK_VALIDATION_WORKTREE_RESTORE_FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
255
147
|
}
|
|
256
148
|
}
|
|
257
|
-
|
|
258
|
-
throw new Error(`SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`);
|
|
259
|
-
}
|
|
149
|
+
restored = true;
|
|
260
150
|
}
|
|
261
151
|
}
|
|
262
152
|
function errorCode(message) {
|