@fitlab-ai/agent-infra 0.11.11 → 0.11.13
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 -2
- package/dist/lib/agent-clients/adapter.js +7 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
- package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
- package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
- package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
- package/dist/lib/agent-clients/adapters/codex.js +40 -1
- package/dist/lib/agent-clients/adapters/index.js +2 -10
- package/dist/lib/agent-clients/tokens.js +3 -2
- package/dist/lib/internal/agent-client.js +132 -72
- package/dist/lib/internal/cli-route-inventory.js +2 -2
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +26 -19
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -1
- package/dist/lib/internal/task-validate.js +1 -1
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/platform/issues.js +18 -6
- package/dist/lib/runtime/agent-runtime.js +6 -29
- package/dist/lib/sandbox/commands/control-status.js +1 -0
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/control/agent-operation.js +34 -0
- package/dist/lib/sandbox/control/client.js +14 -201
- package/dist/lib/sandbox/control/executor.js +12 -125
- package/dist/lib/sandbox/control/lifecycle.js +66 -4
- package/dist/lib/sandbox/control/protocol.js +15 -54
- package/dist/lib/sandbox/control/server.js +216 -281
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/mounts.js +9 -1
- package/dist/lib/sandbox/recovery.js +31 -10
- package/dist/lib/sandbox/removal.js +28 -5
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +56 -19
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +56 -264
- package/dist/lib/task/control-recovery.js +2 -3
- package/dist/lib/task/create.js +2 -2
- package/dist/lib/task/delegation-receipts.js +74 -161
- package/dist/lib/task/events.js +88 -44
- package/dist/lib/task/finalization.js +35 -6
- package/dist/lib/task/lifecycle.js +274 -115
- package/dist/lib/task/orchestration.js +201 -194
- package/dist/lib/task/runtime-paths.js +10 -0
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +32 -26
- package/dist/lib/task/workspace-snapshot.js +1 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +39 -0
- package/lib/agent-clients/adapters/claude-code.ts +5 -1
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
- package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
- package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
- package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
- package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
- package/lib/agent-clients/adapters/codex.ts +44 -1
- package/lib/agent-clients/adapters/index.ts +7 -20
- package/lib/agent-clients/tokens.ts +4 -3
- package/lib/internal/agent-client.ts +142 -73
- package/lib/internal/cli-route-inventory.ts +2 -2
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +30 -18
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -1
- package/lib/internal/task-validate.ts +1 -1
- package/lib/platform/github-client.ts +34 -3
- package/lib/platform/issues.ts +18 -6
- package/lib/runtime/agent-runtime.ts +6 -41
- package/lib/sandbox/commands/control-status.ts +1 -0
- package/lib/sandbox/commands/create.ts +7 -1
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/control/agent-operation.ts +49 -0
- package/lib/sandbox/control/client.ts +20 -254
- package/lib/sandbox/control/executor.ts +13 -136
- package/lib/sandbox/control/lifecycle.ts +67 -4
- package/lib/sandbox/control/protocol.ts +22 -66
- package/lib/sandbox/control/server.ts +21 -95
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/mounts.ts +10 -1
- package/lib/sandbox/recovery.ts +38 -17
- package/lib/sandbox/removal.ts +33 -7
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +50 -18
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +66 -378
- package/lib/task/control-recovery.ts +3 -4
- package/lib/task/create.ts +3 -3
- package/lib/task/delegation-receipts.ts +87 -231
- package/lib/task/events.ts +100 -42
- package/lib/task/finalization.ts +37 -7
- package/lib/task/lifecycle.ts +257 -109
- package/lib/task/orchestration.ts +260 -237
- package/lib/task/runtime-paths.ts +12 -0
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +30 -25
- package/lib/task/workspace-snapshot.ts +1 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/next-step-output.en.md +6 -19
- package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- 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-manual-validation/SKILL.en.md +0 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
- 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/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
- package/dist/lib/internal/codex-lifecycle.js +0 -385
- package/dist/lib/task/codex-orchestration.js +0 -206
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
- package/lib/internal/codex-lifecycle.ts +0 -390
- package/lib/task/codex-orchestration.ts +0 -276
|
@@ -72,11 +72,10 @@ import {
|
|
|
72
72
|
import type { BrokerOwner } from './lifecycle.ts';
|
|
73
73
|
import { nextSandboxControlBackoff } from './timing.ts';
|
|
74
74
|
import { readTaskFinalizationReceipt } from '../../task/finalization.ts';
|
|
75
|
-
import {
|
|
75
|
+
import { recoverAgentClientOperation, validateAgentClientOperation } from './agent-operation.ts';
|
|
76
76
|
import { validateSandboxControlIdentity } from './identity-sentinel.ts';
|
|
77
77
|
import {
|
|
78
78
|
mergeSandboxTaskView,
|
|
79
|
-
taskViewAfterFinalization,
|
|
80
79
|
taskViewForManifest,
|
|
81
80
|
type SandboxTaskView
|
|
82
81
|
} from './task-view.ts';
|
|
@@ -91,34 +90,6 @@ type ActiveExecution = {
|
|
|
91
90
|
settled: boolean;
|
|
92
91
|
};
|
|
93
92
|
|
|
94
|
-
async function taskViewForFinalizationResult(
|
|
95
|
-
manifest: SandboxControlManifest,
|
|
96
|
-
request: SandboxControlRequest,
|
|
97
|
-
result: SandboxControlExecutionResult | null
|
|
98
|
-
): Promise<SandboxTaskView | null> {
|
|
99
|
-
if (request.family !== 'task-finalization' || result?.exitCode !== 0 || !manifest.taskId) return null;
|
|
100
|
-
let receipt: unknown = null;
|
|
101
|
-
for (let attempt = 0; attempt < 100 && !receipt; attempt += 1) {
|
|
102
|
-
try {
|
|
103
|
-
receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
|
|
104
|
-
} catch {
|
|
105
|
-
// A missing or temporarily moving task directory is retried before publishing completion.
|
|
106
|
-
}
|
|
107
|
-
if (!receipt) await new Promise((resolve) => setTimeout(resolve, 10));
|
|
108
|
-
}
|
|
109
|
-
const finalized = taskViewAfterFinalization({
|
|
110
|
-
taskId: manifest.taskId,
|
|
111
|
-
generation: manifest.generation,
|
|
112
|
-
requestId: request.id,
|
|
113
|
-
receipt
|
|
114
|
-
});
|
|
115
|
-
if (finalized.state === 'unknown') return finalized;
|
|
116
|
-
const canonical = taskViewForManifest({ ...manifest, receipt });
|
|
117
|
-
return canonical.state === 'current' && canonical.observedSource === 'completed'
|
|
118
|
-
? canonical
|
|
119
|
-
: finalized;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
93
|
function appendBrokerAudit(
|
|
123
94
|
manifest: SandboxControlManifest,
|
|
124
95
|
event: string,
|
|
@@ -128,9 +99,8 @@ function appendBrokerAudit(
|
|
|
128
99
|
}
|
|
129
100
|
|
|
130
101
|
function operationKey(request: SandboxControlRequest): string | null {
|
|
131
|
-
if (request.family === 'task-finalization') return request.operation;
|
|
132
102
|
if (request.family === 'task-create') return 'create';
|
|
133
|
-
if (request.family === '
|
|
103
|
+
if (request.family === 'agent-client') return request.operation;
|
|
134
104
|
if (request.family !== 'task-lifecycle') return null;
|
|
135
105
|
try {
|
|
136
106
|
const operation = parseTaskControlOperation(request.family, request.args);
|
|
@@ -318,46 +288,6 @@ function taskCreateDomainEvidence(
|
|
|
318
288
|
}
|
|
319
289
|
}
|
|
320
290
|
|
|
321
|
-
function controllerDomainEvidence(
|
|
322
|
-
manifest: SandboxControlManifest,
|
|
323
|
-
manifestPath: string,
|
|
324
|
-
request: SandboxControlRequest,
|
|
325
|
-
output: Record<string, unknown> | null
|
|
326
|
-
): Readonly<Record<string, unknown>> {
|
|
327
|
-
if (!output || output.error !== null || typeof output.status !== 'string') return { consistent: false };
|
|
328
|
-
try {
|
|
329
|
-
const registration = readCodexControllerRegistration(manifestPath);
|
|
330
|
-
if (request.family !== 'codex-controller') return { consistent: false };
|
|
331
|
-
if (request.command === 'open') {
|
|
332
|
-
const lease = output.lease && typeof output.lease === 'object' && !Array.isArray(output.lease)
|
|
333
|
-
? output.lease as Record<string, unknown> : null;
|
|
334
|
-
return {
|
|
335
|
-
consistent: output.status === 'opened' && output.changed === true
|
|
336
|
-
&& lease?.taskId === registration.taskId
|
|
337
|
-
&& lease.controlGeneration === registration.controlGeneration
|
|
338
|
-
&& lease.controllerInstanceDigest === registration.controllerInstanceDigest
|
|
339
|
-
&& registration.taskId === manifest.taskId
|
|
340
|
-
&& registration.controlGeneration === manifest.generation
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
if (request.command === 'verify') {
|
|
344
|
-
const binding = output.binding && typeof output.binding === 'object' && !Array.isArray(output.binding)
|
|
345
|
-
? output.binding as Record<string, unknown> : null;
|
|
346
|
-
return {
|
|
347
|
-
consistent: output.status === 'verified' && output.changed === false
|
|
348
|
-
&& binding?.taskId === registration.taskId
|
|
349
|
-
&& binding.controlGeneration === registration.controlGeneration
|
|
350
|
-
&& binding.controllerInstanceDigest === registration.controllerInstanceDigest
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
return { consistent: false };
|
|
354
|
-
} catch (error) {
|
|
355
|
-
return { consistent: request.family === 'codex-controller' && request.command === 'close'
|
|
356
|
-
&& (error as { code?: string }).code === 'CODEX_SANDBOX_CONTROLLER_REGISTRATION_MISSING'
|
|
357
|
-
&& output?.status === 'closed' && typeof output.changed === 'boolean' };
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
291
|
async function readRecoveryDomain(
|
|
362
292
|
manifest: SandboxControlManifest,
|
|
363
293
|
manifestPath: string,
|
|
@@ -367,11 +297,9 @@ async function readRecoveryDomain(
|
|
|
367
297
|
payloadOutput: string | null
|
|
368
298
|
): Promise<RecoveryDomainEvidence> {
|
|
369
299
|
if (!operation) return { domain: null };
|
|
370
|
-
const taskRef = request.
|
|
371
|
-
? manifest.taskId
|
|
372
|
-
: 'args' in request ? request.args[0] ?? null : manifest.taskId;
|
|
300
|
+
const taskRef = 'args' in request ? request.args[0] ?? null : manifest.taskId;
|
|
373
301
|
const output = parseControlOutput(payloadOutput);
|
|
374
|
-
if (!taskRef && operation.family !== 'task-create' && operation.family !== '
|
|
302
|
+
if (!taskRef && operation.family !== 'task-create' && operation.family !== 'agent-client') {
|
|
375
303
|
return { domain: null };
|
|
376
304
|
}
|
|
377
305
|
|
|
@@ -399,8 +327,13 @@ async function readRecoveryDomain(
|
|
|
399
327
|
if (operation.family === 'task-create') {
|
|
400
328
|
return { domain: taskCreateDomainEvidence(manifest, output) };
|
|
401
329
|
}
|
|
402
|
-
if (operation.family === '
|
|
403
|
-
return { domain:
|
|
330
|
+
if (operation.family === 'agent-client' && request.family === 'agent-client') {
|
|
331
|
+
return { domain: recoverAgentClientOperation({
|
|
332
|
+
request,
|
|
333
|
+
manifest,
|
|
334
|
+
manifestPath,
|
|
335
|
+
stdout: payloadOutput
|
|
336
|
+
}) };
|
|
404
337
|
}
|
|
405
338
|
return { domain: null };
|
|
406
339
|
}
|
|
@@ -638,6 +571,7 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
|
|
|
638
571
|
request = validateSandboxControlRequest(rawRequest, manifest, {
|
|
639
572
|
now: typeof rawRequest.issuedAt === 'number' ? rawRequest.issuedAt : undefined
|
|
640
573
|
});
|
|
574
|
+
if (request.family === 'agent-client') validateAgentClientOperation(request, manifest);
|
|
641
575
|
} catch {
|
|
642
576
|
// Missing or malformed request evidence remains fail-closed below.
|
|
643
577
|
}
|
|
@@ -876,6 +810,7 @@ export async function serveSandboxControl(
|
|
|
876
810
|
let bindingBackoffMs: number = timing.parkedBindingInitialMs;
|
|
877
811
|
try {
|
|
878
812
|
if (!brokerOwns()) return;
|
|
813
|
+
if (isSandboxControlRootQuiescing(root)) return;
|
|
879
814
|
if (taskView.state === 'current' && taskView.observedSource === 'completed') {
|
|
880
815
|
// Recheck durable re-entry evidence before publishing a trusted startup view.
|
|
881
816
|
const unverified = { ...taskView, state: 'unknown' as const,
|
|
@@ -966,11 +901,6 @@ export async function serveSandboxControl(
|
|
|
966
901
|
settledExecution = active;
|
|
967
902
|
active = null;
|
|
968
903
|
}
|
|
969
|
-
const finalizationView = settledExecution
|
|
970
|
-
? await taskViewForFinalizationResult(manifest, settledExecution.request, settledExecution.result)
|
|
971
|
-
: null;
|
|
972
|
-
if (finalizationView) taskView = finalizationView;
|
|
973
|
-
|
|
974
904
|
let reasonCode: string | null = null;
|
|
975
905
|
try {
|
|
976
906
|
if (!brokerOwns()) break;
|
|
@@ -1039,7 +969,9 @@ export async function serveSandboxControl(
|
|
|
1039
969
|
}
|
|
1040
970
|
|
|
1041
971
|
let retiring = false;
|
|
1042
|
-
|
|
972
|
+
const quiescing = isSandboxControlRootQuiescing(root);
|
|
973
|
+
if (quiescing && !active) break;
|
|
974
|
+
if (!quiescing) for (const name of fs.readdirSync(requestsDir).sort()) {
|
|
1043
975
|
if (!/^[a-f0-9-]{16,64}\.json$/.test(name)) continue;
|
|
1044
976
|
const id = name.slice(0, -5);
|
|
1045
977
|
const source = path.join(requestsDir, name);
|
|
@@ -1047,6 +979,10 @@ export async function serveSandboxControl(
|
|
|
1047
979
|
let validatedRequest: SandboxControlRequest | null = null;
|
|
1048
980
|
let prepared: PreparedSandboxControlExecution | null = null;
|
|
1049
981
|
try {
|
|
982
|
+
if (isSandboxControlRootQuiescing(root)) {
|
|
983
|
+
retiring = true;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
1050
986
|
if (!brokerOwns()) {
|
|
1051
987
|
retiring = true;
|
|
1052
988
|
break;
|
|
@@ -1070,6 +1006,7 @@ export async function serveSandboxControl(
|
|
|
1070
1006
|
if (reasonCode) throw new Error(reasonCode);
|
|
1071
1007
|
if (active) throw new Error('SANDBOX_CONTROL_BUSY');
|
|
1072
1008
|
const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
|
|
1009
|
+
if (request.family === 'agent-client') validateAgentClientOperation(request, manifest);
|
|
1073
1010
|
validatedRequest = request;
|
|
1074
1011
|
criticalRequestPhase(manifest, request, 'validated', 'in-progress');
|
|
1075
1012
|
appendBrokerAudit(manifest, 'request-validated', {
|
|
@@ -1162,12 +1099,6 @@ export async function serveSandboxControl(
|
|
|
1162
1099
|
retiring = true;
|
|
1163
1100
|
continue;
|
|
1164
1101
|
}
|
|
1165
|
-
if (!brokerOwns()) {
|
|
1166
|
-
prepared.terminate(false);
|
|
1167
|
-
active = null;
|
|
1168
|
-
retiring = true;
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
1102
|
criticalRequestPhase(manifest, request, 'accepted-authorized', 'in-progress');
|
|
1172
1103
|
writeAcceptedResponse(manifest, {
|
|
1173
1104
|
version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
|
|
@@ -1269,11 +1200,6 @@ export async function serveSandboxControl(
|
|
|
1269
1200
|
}
|
|
1270
1201
|
}
|
|
1271
1202
|
if (owned && active.result && active.resultEvidenceWritten) {
|
|
1272
|
-
const finalizationView = await taskViewForFinalizationResult(manifest, active.request, active.result);
|
|
1273
|
-
if (finalizationView) {
|
|
1274
|
-
taskView = finalizationView;
|
|
1275
|
-
writeSandboxControlStatus(manifest, broker, 'healthy', null, null, Date.now(), taskView);
|
|
1276
|
-
}
|
|
1277
1203
|
if (publishExecutionResult(manifest, active.request, active.result, brokerOwns)) {
|
|
1278
1204
|
if (brokerOwns()) {
|
|
1279
1205
|
removeAcceptedResponse(manifest, active.request.id);
|
|
@@ -180,25 +180,6 @@ export function projectSandboxTaskView(input: TaskViewProjectionInput): SandboxT
|
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
export function taskViewAfterFinalization(params: Readonly<{
|
|
184
|
-
taskId: string;
|
|
185
|
-
generation: string;
|
|
186
|
-
requestId: string;
|
|
187
|
-
receipt: unknown;
|
|
188
|
-
}>): SandboxTaskView {
|
|
189
|
-
const receipt = receiptIdentity(params.receipt, params.generation, params.requestId);
|
|
190
|
-
if (!receipt) {
|
|
191
|
-
return {
|
|
192
|
-
state: 'unknown', taskId: params.taskId, observedSource: 'unknown', receipt: null,
|
|
193
|
-
reasonCode: 'SANDBOX_TASK_VIEW_RECEIPT_INVALID'
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
state: 'finalized-stale', taskId: params.taskId, observedSource: 'active', receipt,
|
|
198
|
-
reasonCode: 'SANDBOX_TASK_VIEW_FINALIZED'
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
183
|
export function mergeSandboxTaskView(
|
|
203
184
|
canonical: SandboxTaskView,
|
|
204
185
|
previous: SandboxTaskView
|
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function sandboxCoreBindMounts(
|
|
|
31
31
|
worktreeGitFile?: string;
|
|
32
32
|
runtimeDir?: string;
|
|
33
33
|
taskSources?: string[];
|
|
34
|
+
completedTaskSource?: string;
|
|
34
35
|
taskId?: string;
|
|
35
36
|
}
|
|
36
37
|
): SandboxBindMountDeclaration[] {
|
|
@@ -71,11 +72,19 @@ export function sandboxCoreBindMounts(
|
|
|
71
72
|
...workspaceMounts,
|
|
72
73
|
];
|
|
73
74
|
if (taskBound) {
|
|
75
|
+
const taskMountPath = path.posix.join('/workspace/.agents/workspace/active', overrides.taskId!);
|
|
74
76
|
mounts.push({
|
|
75
77
|
hostPaths: taskSources,
|
|
76
|
-
containerPath:
|
|
78
|
+
containerPath: taskMountPath,
|
|
77
79
|
readOnly: false
|
|
78
80
|
});
|
|
81
|
+
if (overrides.completedTaskSource) {
|
|
82
|
+
mounts.push({
|
|
83
|
+
hostPaths: [overrides.completedTaskSource],
|
|
84
|
+
containerPath: path.posix.join('/workspace/.agents/workspace/completed', overrides.taskId!),
|
|
85
|
+
readOnly: false
|
|
86
|
+
});
|
|
87
|
+
}
|
|
79
88
|
}
|
|
80
89
|
mounts.push(
|
|
81
90
|
{
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { sandboxCoreBindMounts } from './mounts.ts';
|
|
|
25
25
|
import { resolveSandboxGitMetadata } from './git-metadata.ts';
|
|
26
26
|
import {
|
|
27
27
|
assertSandboxTaskSource,
|
|
28
|
+
sandboxCompletedTaskMountSource,
|
|
28
29
|
sandboxControlPaths,
|
|
29
30
|
sandboxWorkspaceViewPaths
|
|
30
31
|
} from './workspace-view.ts';
|
|
@@ -341,6 +342,7 @@ async function ensureSandboxControlBroker(params: {
|
|
|
341
342
|
}): Promise<void> {
|
|
342
343
|
const control = sandboxControlPaths({
|
|
343
344
|
base: params.config.controlBase ?? path.join(params.config.home, '.agent-infra', 'sandbox-control'),
|
|
345
|
+
repoRoot: params.config.repoRoot,
|
|
344
346
|
project: params.config.project,
|
|
345
347
|
container: params.container,
|
|
346
348
|
identity: params.workspace
|
|
@@ -672,31 +674,43 @@ export function startCompletedSandboxContainer(params: {
|
|
|
672
674
|
const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
|
|
673
675
|
const historicalSource = path.join(activeRoot, params.taskId);
|
|
674
676
|
fs.mkdirSync(activeRoot, { recursive: true });
|
|
677
|
+
let replacedEmptyRoot = false;
|
|
678
|
+
let emptyRootMode = 0o700;
|
|
675
679
|
try {
|
|
676
|
-
fs.lstatSync(historicalSource);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
+
const existing = fs.lstatSync(historicalSource);
|
|
681
|
+
if (existing.isSymbolicLink() || !existing.isDirectory() || fs.readdirSync(historicalSource).length > 0) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
emptyRootMode = existing.mode & 0o777;
|
|
687
|
+
fs.rmdirSync(historicalSource);
|
|
688
|
+
replacedEmptyRoot = true;
|
|
680
689
|
} catch (error) {
|
|
681
690
|
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
|
|
682
691
|
}
|
|
683
692
|
|
|
684
|
-
|
|
685
|
-
completedSource,
|
|
686
|
-
historicalSource,
|
|
687
|
-
process.platform === 'win32' ? 'junction' : 'dir'
|
|
688
|
-
);
|
|
693
|
+
let temporaryLink = false;
|
|
689
694
|
try {
|
|
695
|
+
fs.symlinkSync(
|
|
696
|
+
completedSource,
|
|
697
|
+
historicalSource,
|
|
698
|
+
process.platform === 'win32' ? 'junction' : 'dir'
|
|
699
|
+
);
|
|
700
|
+
temporaryLink = true;
|
|
690
701
|
params.start(params.engine, params.container);
|
|
691
702
|
} finally {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
703
|
+
if (temporaryLink) {
|
|
704
|
+
const temporarySource = fs.lstatSync(historicalSource);
|
|
705
|
+
const resolvedSource = fs.realpathSync.native(historicalSource);
|
|
706
|
+
if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
`SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
fs.unlinkSync(historicalSource);
|
|
698
712
|
}
|
|
699
|
-
fs.
|
|
713
|
+
if (replacedEmptyRoot) fs.mkdirSync(historicalSource, { mode: emptyRootMode });
|
|
700
714
|
}
|
|
701
715
|
}
|
|
702
716
|
|
|
@@ -739,6 +753,7 @@ function expectedMounts(params: {
|
|
|
739
753
|
});
|
|
740
754
|
const control = sandboxControlPaths({
|
|
741
755
|
base: config.controlBase ?? path.join(config.home, '.agent-infra', 'sandbox-control'),
|
|
756
|
+
repoRoot: config.repoRoot,
|
|
742
757
|
project: config.project,
|
|
743
758
|
container: params.container,
|
|
744
759
|
identity: params.workspace
|
|
@@ -747,6 +762,7 @@ function expectedMounts(params: {
|
|
|
747
762
|
? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
|
|
748
763
|
: null;
|
|
749
764
|
const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
|
|
765
|
+
const completedTaskSource = taskId ? sandboxCompletedTaskMountSource(config.repoRoot, taskId) : null;
|
|
750
766
|
const worktrees = worktreeDirCandidates(config, branch).filter((candidate) =>
|
|
751
767
|
fs.existsSync(path.join(candidate, '.git'))
|
|
752
768
|
);
|
|
@@ -768,6 +784,7 @@ function expectedMounts(params: {
|
|
|
768
784
|
: {
|
|
769
785
|
runtimeDir: control.runtimeDir,
|
|
770
786
|
taskSources: taskSources!.accessiblePaths,
|
|
787
|
+
completedTaskSource: completedTaskSource!,
|
|
771
788
|
taskId: taskId!
|
|
772
789
|
})
|
|
773
790
|
}).map((mount) => ({
|
|
@@ -778,6 +795,10 @@ function expectedMounts(params: {
|
|
|
778
795
|
&& mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
|
|
779
796
|
? { sourceAccessiblePaths: taskSources!.accessiblePaths }
|
|
780
797
|
: {}),
|
|
798
|
+
...(completedTaskSource !== null && taskId !== null
|
|
799
|
+
&& mount.containerPath === `/workspace/.agents/workspace/completed/${taskId}`
|
|
800
|
+
? { sourceAccessiblePaths: [completedTaskSource] }
|
|
801
|
+
: {}),
|
|
781
802
|
expectedRW: !mount.readOnly
|
|
782
803
|
}));
|
|
783
804
|
const live = tools.flatMap((tool) =>
|
|
@@ -1275,7 +1296,7 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
1275
1296
|
let completedReentry: { manifest: SandboxControlManifest; evidence: Awaited<ReturnType<typeof prepareCompletedReentry>> } | null = null;
|
|
1276
1297
|
if (params.reentry === 'completed' && params.workspace?.mode === 'task-bound') {
|
|
1277
1298
|
const control = sandboxControlPaths({
|
|
1278
|
-
base: params.config.controlBase, project: params.config.project,
|
|
1299
|
+
base: params.config.controlBase, repoRoot: params.config.repoRoot, project: params.config.project,
|
|
1279
1300
|
container: params.row.name, identity: params.workspace
|
|
1280
1301
|
});
|
|
1281
1302
|
const manifest = readSandboxControlManifest(control.manifestPath);
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -829,7 +829,7 @@ function resolveRmTarget(
|
|
|
829
829
|
? matchedContainers
|
|
830
830
|
: [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
|
|
831
831
|
const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
|
|
832
|
-
base: config.controlBase, project: config.project, container, identity
|
|
832
|
+
base: config.controlBase, repoRoot: config.repoRoot, project: config.project, container, identity
|
|
833
833
|
}).root));
|
|
834
834
|
const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
|
|
835
835
|
base: config.workspaceViewBase, project: config.project, container, identity
|
|
@@ -1007,7 +1007,9 @@ async function removeUncheckedSandbox(
|
|
|
1007
1007
|
}
|
|
1008
1008
|
for (const root of controlRoots) {
|
|
1009
1009
|
fs.rmSync(root, { recursive: true, force: true });
|
|
1010
|
-
|
|
1010
|
+
if (target.workspace.mode !== 'task-bound') {
|
|
1011
|
+
removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
|
|
1012
|
+
}
|
|
1011
1013
|
}
|
|
1012
1014
|
for (const root of workspaceViewRoots) {
|
|
1013
1015
|
fs.rmSync(root, { recursive: true, force: true });
|
|
@@ -1027,14 +1029,19 @@ async function removeUncheckedSandbox(
|
|
|
1027
1029
|
assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
|
|
1028
1030
|
runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
|
|
1029
1031
|
}
|
|
1030
|
-
|
|
1031
|
-
releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
|
|
1032
|
-
}
|
|
1032
|
+
releaseTaskBoundSandboxIdentity(config.repoRoot, target.workspace);
|
|
1033
1033
|
|
|
1034
1034
|
if (!options.quiet) p.outro('Sandbox removed');
|
|
1035
1035
|
return null;
|
|
1036
1036
|
}
|
|
1037
1037
|
|
|
1038
|
+
function releaseTaskBoundSandboxIdentity(
|
|
1039
|
+
repoRoot: string,
|
|
1040
|
+
workspace: SandboxWorkspaceKey
|
|
1041
|
+
): void {
|
|
1042
|
+
if (workspace.mode === 'task-bound') releaseStaleShortIdRegistry(repoRoot, workspace.taskId);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1038
1045
|
async function rmOneCore(
|
|
1039
1046
|
config: SandboxConfig,
|
|
1040
1047
|
tools: SandboxTool[],
|
|
@@ -1066,11 +1073,25 @@ async function rmPurgeCore(
|
|
|
1066
1073
|
const confirm = prompt.confirm ?? p.confirm;
|
|
1067
1074
|
const isCancel = prompt.isCancel ?? p.isCancel;
|
|
1068
1075
|
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
1069
|
-
|
|
1076
|
+
const listed = fetchSandboxRows(
|
|
1077
|
+
engine,
|
|
1078
|
+
sandboxLabel(config),
|
|
1079
|
+
sandboxBranchLabel(config),
|
|
1080
|
+
{ mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
|
|
1081
|
+
);
|
|
1082
|
+
const rows = [...listed.running, ...listed.nonRunning];
|
|
1070
1083
|
const containers = runEngine(engine, 'docker', [
|
|
1071
1084
|
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
1072
1085
|
]).split('\n').filter(Boolean);
|
|
1086
|
+
const completedTaskIds = [...new Set(rows
|
|
1087
|
+
.filter((row) => row.workspaceMode === 'task-bound' && row.taskId)
|
|
1088
|
+
.map((row) => row.taskId!))];
|
|
1073
1089
|
for (const name of containers) runSafeEngine(engine, 'docker', ['rm', '-f', name]);
|
|
1090
|
+
if (!sandboxContainersRemoved(engine, containers)) {
|
|
1091
|
+
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${containers.join(', ')}`);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
let cleanupCompleted = true;
|
|
1074
1095
|
|
|
1075
1096
|
const worktrees = fs.existsSync(config.worktreeBase)
|
|
1076
1097
|
? fs.readdirSync(config.worktreeBase).map((entry) => path.join(config.worktreeBase, entry))
|
|
@@ -1080,21 +1101,26 @@ async function rmPurgeCore(
|
|
|
1080
1101
|
if (!isCancel(selected) && selected) {
|
|
1081
1102
|
for (const worktree of worktrees) fs.rmSync(worktree, { recursive: true, force: true });
|
|
1082
1103
|
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
1083
|
-
}
|
|
1104
|
+
} else cleanupCompleted = false;
|
|
1084
1105
|
}
|
|
1085
1106
|
|
|
1086
1107
|
for (const dir of projectToolDirs(config, tools)) fs.rmSync(dir, { recursive: true, force: true });
|
|
1087
1108
|
if (fs.existsSync(config.shellConfigBase)) {
|
|
1088
1109
|
const selected = await confirm({ message: `Remove all shell config dirs in ${config.shellConfigBase}?`, initialValue: true });
|
|
1089
1110
|
if (!isCancel(selected) && selected) fs.rmSync(config.shellConfigBase, { recursive: true, force: true });
|
|
1111
|
+
else cleanupCompleted = false;
|
|
1090
1112
|
}
|
|
1091
1113
|
if (fs.existsSync(config.shareBase)) {
|
|
1092
1114
|
const selected = await confirm({ message: `Remove all share dirs for project (${config.shareBase})?`, initialValue: true });
|
|
1093
1115
|
if (!isCancel(selected) && selected) fs.rmSync(config.shareBase, { recursive: true, force: true });
|
|
1116
|
+
else cleanupCompleted = false;
|
|
1094
1117
|
}
|
|
1095
1118
|
for (const base of [config.workspaceViewBase, config.controlBase]) {
|
|
1096
1119
|
fs.rmSync(path.join(base, config.project), { recursive: true, force: true });
|
|
1097
1120
|
}
|
|
1121
|
+
if (cleanupCompleted) {
|
|
1122
|
+
for (const taskId of completedTaskIds) releaseStaleShortIdRegistry(config.repoRoot, taskId);
|
|
1123
|
+
}
|
|
1098
1124
|
const removeImage = await confirm({ message: `Remove image ${config.imageName}?`, initialValue: false });
|
|
1099
1125
|
if (!isCancel(removeImage) && removeImage) runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
|
|
1100
1126
|
p.outro(pc.green('All project sandboxes removed'));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isRemovedHashShortIdInput } from '../task/short-id.ts';
|
|
4
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
4
5
|
import {
|
|
5
6
|
resolveTaskWorkspace,
|
|
6
7
|
type TaskWorkspaceState
|
|
@@ -76,6 +77,14 @@ function taskIdentity(repoRoot: string, taskId: string, registry: Record<string,
|
|
|
76
77
|
return { mode: 'task-bound', taskId, shortId: shortIds[0]! };
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
function cleanupTaskId(requestedRef: string, repoRoot: string): string | null {
|
|
81
|
+
if (TASK_ID_RE.test(requestedRef)) return requestedRef;
|
|
82
|
+
if (!SHORT_ID_RE.test(requestedRef)) return null;
|
|
83
|
+
const task = resolveTaskRef(requestedRef, { repoRoot });
|
|
84
|
+
if (!task.ok) throw new Error(task.message);
|
|
85
|
+
return task.taskId;
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
export function resolveSandboxTarget(requestedRef: string, repoRoot: string): SandboxTarget {
|
|
80
89
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
81
90
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
@@ -130,23 +139,24 @@ export function resolveSandboxCleanupTarget(
|
|
|
130
139
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
const taskId = cleanupTaskId(requestedRef, repoRoot);
|
|
143
|
+
if (taskId) {
|
|
134
144
|
let task: ReturnType<typeof resolveTaskWorkspace>;
|
|
135
145
|
try {
|
|
136
|
-
task = resolveTaskWorkspace(
|
|
146
|
+
task = resolveTaskWorkspace(taskId, repoRoot);
|
|
137
147
|
} catch (error) {
|
|
138
|
-
if (!(error instanceof Error && error.message === `Task not found: ${
|
|
139
|
-
const branch = options.resolveMissingTask?.(
|
|
148
|
+
if (!(error instanceof Error && error.message === `Task not found: ${taskId}`)) throw error;
|
|
149
|
+
const branch = options.resolveMissingTask?.(taskId);
|
|
140
150
|
if (!branch) throw error;
|
|
141
151
|
return {
|
|
142
152
|
requestedRef,
|
|
143
153
|
branch,
|
|
144
|
-
workspace: { mode: 'task-bound', taskId
|
|
154
|
+
workspace: { mode: 'task-bound', taskId },
|
|
145
155
|
taskState: 'unknown'
|
|
146
156
|
};
|
|
147
157
|
}
|
|
148
158
|
if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
|
|
149
|
-
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${
|
|
159
|
+
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${taskId} is ${task.state}`);
|
|
150
160
|
}
|
|
151
161
|
return {
|
|
152
162
|
requestedRef,
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import type { SandboxWorkspaceIdentity, SandboxWorkspaceKey } from './workspace-identity.ts';
|
|
5
5
|
import { resolveTaskWorkspace } from './task-resolver.ts';
|
|
6
6
|
import type { SandboxControlManifest } from './control/protocol.ts';
|
|
7
|
+
import { resolveTaskRuntimeRoot } from '../task/runtime-paths.ts';
|
|
7
8
|
import {
|
|
8
9
|
createSandboxControlIdentitySentinel,
|
|
9
10
|
writeSandboxControlIdentitySentinel
|
|
@@ -63,15 +64,17 @@ export type SandboxControlManifestDraft = Readonly<Omit<SandboxControlManifest,
|
|
|
63
64
|
|
|
64
65
|
export function sandboxControlPaths(params: Readonly<{
|
|
65
66
|
base: string;
|
|
67
|
+
repoRoot?: string;
|
|
66
68
|
project: string;
|
|
67
69
|
container: string;
|
|
68
70
|
identity: SandboxWorkspaceIdentity | SandboxWorkspaceKey;
|
|
69
71
|
}>): Readonly<{ root: string; channelDir: string; statusDir: string; processingDir: string; runtimeDir: string; manifestPath: string }> {
|
|
70
|
-
const
|
|
71
|
-
?
|
|
72
|
-
:
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
const root = params.identity.mode === 'task-bound'
|
|
73
|
+
? path.join(resolveTaskRuntimeRoot(params.identity.taskId, { repoRoot: params.repoRoot }), 'sandbox-control')
|
|
74
|
+
: (() => {
|
|
75
|
+
const digest = createHash('sha256').update('branch-only').digest('hex').slice(0, 16);
|
|
76
|
+
return path.resolve(params.base, params.project, params.container, digest);
|
|
77
|
+
})();
|
|
75
78
|
return {
|
|
76
79
|
root,
|
|
77
80
|
channelDir: path.join(root, 'channel'),
|
|
@@ -154,13 +157,15 @@ export function assertSandboxTaskSource(repoRoot: string, taskId: string): strin
|
|
|
154
157
|
return canonical;
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
export function prepareSandboxWorkspaceMountTargets(worktreeRoot: string): void {
|
|
160
|
+
export function prepareSandboxWorkspaceMountTargets(worktreeRoot: string, taskId?: string): void {
|
|
158
161
|
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
159
162
|
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
160
163
|
const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
|
|
161
164
|
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
162
165
|
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
163
|
-
|
|
166
|
+
const completedTaskTarget = taskId ? path.join(workspaceRoot, 'completed', taskId) : null;
|
|
167
|
+
if (taskId && !/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
168
|
+
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget, ...(completedTaskTarget ? [completedTaskTarget] : [])]) {
|
|
164
169
|
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
165
170
|
}
|
|
166
171
|
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
@@ -168,10 +173,47 @@ export function prepareSandboxWorkspaceMountTargets(worktreeRoot: string): void
|
|
|
168
173
|
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
169
174
|
fs.chmodSync(hostPath, 0o700);
|
|
170
175
|
}
|
|
176
|
+
if (completedTaskTarget) fs.mkdirSync(completedTaskTarget, { recursive: true, mode: 0o700 });
|
|
171
177
|
fs.chmodSync(workspaceRoot, 0o700);
|
|
172
178
|
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
export function prepareSandboxCompletedTaskMountSource(repoRoot: string, taskId: string): string {
|
|
182
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
183
|
+
const workspaceRoot = path.resolve(repoRoot, '.agents', 'workspace');
|
|
184
|
+
fs.mkdirSync(path.join(workspaceRoot, 'completed'), { recursive: true, mode: 0o700 });
|
|
185
|
+
const completedRoot = path.join(workspaceRoot, 'completed');
|
|
186
|
+
const completedStat = fs.lstatSync(completedRoot);
|
|
187
|
+
if (!completedStat.isDirectory() || completedStat.isSymbolicLink()) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
188
|
+
const target = path.join(completedRoot, taskId);
|
|
189
|
+
try {
|
|
190
|
+
const stat = fs.lstatSync(target);
|
|
191
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(target).length > 0) {
|
|
192
|
+
throw new Error(`SANDBOX_COMPLETED_MOUNT_CONFLICT: ${target}`);
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
196
|
+
fs.mkdirSync(target, { mode: 0o700 });
|
|
197
|
+
}
|
|
198
|
+
return sandboxCompletedTaskMountSource(repoRoot, taskId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function sandboxCompletedTaskMountSource(repoRoot: string, taskId: string): string {
|
|
202
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
203
|
+
const source = path.resolve(repoRoot, '.agents', 'workspace', 'completed', taskId);
|
|
204
|
+
const stat = fs.lstatSync(source);
|
|
205
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
206
|
+
const canonicalWorkspace = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
|
|
207
|
+
const canonicalSource = fs.realpathSync.native(source);
|
|
208
|
+
const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
|
|
209
|
+
const workspaceRelative = path.relative(canonicalRepoRoot, canonicalWorkspace);
|
|
210
|
+
const relative = path.relative(canonicalWorkspace, canonicalSource);
|
|
211
|
+
if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)
|
|
212
|
+
|| path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
|
|
213
|
+
|| relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
214
|
+
return canonicalSource;
|
|
215
|
+
}
|
|
216
|
+
|
|
175
217
|
export function materializeSandboxWorkspaceView(params: Readonly<{
|
|
176
218
|
base: string;
|
|
177
219
|
project: string;
|
|
@@ -221,7 +263,7 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
221
263
|
}>;
|
|
222
264
|
}>): SandboxControlSetup {
|
|
223
265
|
const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
|
|
224
|
-
assertSafeDirectory(root, path.resolve(params.base));
|
|
266
|
+
assertSafeDirectory(root, params.identity.mode === 'task-bound' ? path.dirname(root) : path.resolve(params.base));
|
|
225
267
|
if (fs.existsSync(root)) {
|
|
226
268
|
if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
|
|
227
269
|
throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
|
|
@@ -234,17 +276,7 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
234
276
|
fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
|
|
235
277
|
for (const directory of [statusDir, processingDir, runtimeDir]) {
|
|
236
278
|
assertSafeDirectory(directory, root);
|
|
237
|
-
if (directory === runtimeDir) fs.rmSync(directory, { recursive: true, force: true });
|
|
238
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
239
|
-
}
|
|
240
|
-
for (const directory of [
|
|
241
|
-
path.join(runtimeDir, 'clients'),
|
|
242
|
-
path.join(runtimeDir, 'clients', 'codex', 'capabilities'),
|
|
243
|
-
path.join(runtimeDir, 'clients', 'codex', 'lifecycle')
|
|
244
|
-
]) {
|
|
245
|
-
assertSafeDirectory(directory, runtimeDir);
|
|
246
279
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
247
|
-
fs.chmodSync(directory, 0o700);
|
|
248
280
|
}
|
|
249
281
|
for (const queue of ['requests', 'responses']) {
|
|
250
282
|
const directory = path.join(channelDir, queue);
|