@fitlab-ai/agent-infra 0.9.14 → 0.9.15
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/README.md +10 -0
- package/README.zh-CN.md +10 -0
- package/bin/internal-cli.sh +45 -0
- package/dist/bin/internal-cli.js +93 -30
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
- package/dist/lib/defaults.json +103 -103
- package/dist/lib/fs/durable-write.js +37 -0
- package/dist/lib/fs/file-lock.js +111 -0
- package/dist/lib/host-control/audit.js +19 -0
- package/dist/lib/host-control/client.js +142 -0
- package/dist/lib/host-control/command.js +49 -0
- package/dist/lib/host-control/path.js +160 -0
- package/dist/lib/host-control/secure-fs.js +140 -0
- package/dist/lib/host-control/server.js +159 -0
- package/dist/lib/host-control/service.js +112 -0
- package/dist/lib/init.js +11 -2
- package/dist/lib/internal/cli-route-inventory.js +3 -1
- package/dist/lib/internal/codex-lifecycle.js +6 -6
- package/dist/lib/internal/demo-promotion.js +322 -0
- package/dist/lib/internal/demo-transcript.js +454 -0
- package/dist/lib/internal/host-control.js +28 -0
- package/dist/lib/internal/manual-validation.js +322 -0
- package/dist/lib/internal/platform-pr.js +18 -4
- package/dist/lib/internal/release-workflow.js +63 -84
- package/dist/lib/internal/sandbox-control.js +149 -2
- package/dist/lib/internal/task-artifact.js +15 -262
- package/dist/lib/internal/task-create.js +6 -5
- package/dist/lib/internal/task-event.js +37 -37
- package/dist/lib/internal/task-operation-registry.js +101 -3
- package/dist/lib/internal/task-review.js +8 -53
- package/dist/lib/internal/task-validate.js +30 -10
- package/dist/lib/owned-path.js +33 -0
- package/dist/lib/platform/check-status.js +11 -0
- package/dist/lib/platform/comment-safety.js +4 -1
- package/dist/lib/platform/completion-backfill.js +1 -1
- package/dist/lib/platform/github-data.js +241 -0
- package/dist/lib/platform/github-provider.js +15 -20
- package/dist/lib/platform/issue-comments.js +2 -2
- package/dist/lib/platform/issues.js +1 -179
- package/dist/lib/platform/pr-checks.js +12 -65
- package/dist/lib/platform/pr-summary.js +55 -3
- package/dist/lib/platform/pull-requests.js +2 -105
- package/dist/lib/platform/snapshots.js +2 -0
- package/dist/lib/platform/verification-sync.js +127 -142
- package/dist/lib/release/workflow.js +1 -1
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/rm.js +3 -1514
- package/dist/lib/sandbox/control/audit.js +203 -0
- package/dist/lib/sandbox/control/client.js +93 -54
- package/dist/lib/sandbox/control/executor.js +129 -65
- package/dist/lib/sandbox/control/finalization-response.js +27 -0
- package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
- package/dist/lib/sandbox/control/lifecycle.js +36 -2
- package/dist/lib/sandbox/control/native-file-lock.js +6 -98
- package/dist/lib/sandbox/control/protocol.js +55 -4
- package/dist/lib/sandbox/control/server.js +491 -120
- package/dist/lib/sandbox/control/state.js +58 -0
- package/dist/lib/sandbox/control/task-workflow.js +74 -0
- package/dist/lib/sandbox/control/workflow-executor.js +99 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/recovery.js +28 -4
- package/dist/lib/sandbox/removal.js +1717 -0
- package/dist/lib/sandbox/task-cleanup.js +217 -0
- package/dist/lib/sandbox/workspace-view.js +13 -14
- package/dist/lib/task/activity-intent.js +2 -1
- package/dist/lib/task/activity-log.js +30 -24
- package/dist/lib/task/artifact-command.js +101 -0
- package/dist/lib/task/artifact-lifecycle.js +30 -28
- package/dist/lib/task/artifact-name.js +36 -0
- package/dist/lib/task/artifact-operations.js +23 -69
- package/dist/lib/task/artifact-receipts.js +3 -15
- package/dist/lib/task/artifact-repair-intent.js +87 -16
- package/dist/lib/task/artifact-schema.js +6 -14
- package/dist/lib/task/capabilities.js +12 -19
- package/dist/lib/task/codex-orchestration.js +71 -25
- package/dist/lib/task/commit-identity.js +28 -0
- package/dist/lib/task/commit-operation.js +2 -16
- package/dist/lib/task/control-authority.js +371 -5
- package/dist/lib/task/control-recovery.js +128 -0
- package/dist/lib/task/create.js +62 -9
- package/dist/lib/task/decision-details.js +2 -46
- package/dist/lib/task/events.js +120 -9
- package/dist/lib/task/finalization-state.js +9 -0
- package/dist/lib/task/human-override.js +3 -6
- package/dist/lib/task/implementation-inputs.js +3 -3
- package/dist/lib/task/intermediate-cleanup.js +383 -0
- package/dist/lib/task/invalidation.js +18 -65
- package/dist/lib/task/ledger-intents.js +7 -22
- package/dist/lib/task/ledger.js +1 -3
- package/dist/lib/task/lifecycle.js +19 -1
- package/dist/lib/task/local-artifact-finalization.js +132 -157
- package/dist/lib/task/manual-validation-completion.js +26 -0
- package/dist/lib/task/manual-validation-evidence.js +130 -0
- package/dist/lib/task/manual-validation-receipt.js +101 -0
- package/dist/lib/task/manual-validation-shared.js +40 -0
- package/dist/lib/task/manual-validation-transaction.js +243 -0
- package/dist/lib/task/markdown.js +47 -0
- package/dist/lib/task/orchestration.js +14 -17
- package/dist/lib/task/qualification-audit.js +20 -34
- package/dist/lib/task/review-artifacts.js +2 -18
- package/dist/lib/task/review-command.js +25 -0
- package/dist/lib/task/review-finalization.js +125 -193
- package/dist/lib/task/review-fingerprint.js +2 -2
- package/dist/lib/task/sections.js +34 -56
- package/dist/lib/task/verification-engine.js +116 -323
- package/dist/lib/task/verification.js +1 -1
- package/dist/lib/task/workflow-command.js +17 -0
- package/dist/lib/task/workflow-warning-intents.js +0 -16
- package/dist/lib/task/workflow-warnings.js +28 -2
- package/dist/lib/update.js +2 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
- package/lib/fs/durable-write.ts +29 -0
- package/lib/fs/file-lock.ts +113 -0
- package/lib/host-control/audit.ts +22 -0
- package/lib/host-control/client.ts +172 -0
- package/lib/host-control/command.ts +60 -0
- package/lib/host-control/path.ts +167 -0
- package/lib/host-control/secure-fs.ts +135 -0
- package/lib/host-control/server.ts +180 -0
- package/lib/host-control/service.ts +133 -0
- package/lib/init.ts +15 -2
- package/lib/internal/cli-route-inventory.ts +3 -1
- package/lib/internal/codex-lifecycle.ts +6 -6
- package/lib/internal/demo-promotion.ts +354 -0
- package/lib/internal/demo-transcript.ts +419 -0
- package/lib/internal/host-control.ts +28 -0
- package/lib/internal/manual-validation.ts +306 -0
- package/lib/internal/platform-pr.ts +15 -4
- package/lib/internal/release-workflow.ts +74 -82
- package/lib/internal/sandbox-control.ts +162 -1
- package/lib/internal/task-artifact.ts +13 -216
- package/lib/internal/task-create.ts +5 -5
- package/lib/internal/task-event.ts +20 -11
- package/lib/internal/task-operation-registry.ts +111 -3
- package/lib/internal/task-review.ts +6 -51
- package/lib/internal/task-validate.ts +26 -10
- package/lib/owned-path.ts +26 -0
- package/lib/platform/check-status.ts +9 -0
- package/lib/platform/comment-safety.ts +5 -0
- package/lib/platform/completion-backfill.ts +1 -1
- package/lib/platform/github-data.ts +322 -0
- package/lib/platform/github-provider.ts +15 -20
- package/lib/platform/issue-comments.ts +3 -4
- package/lib/platform/issues.ts +4 -245
- package/lib/platform/pr-checks.ts +13 -77
- package/lib/platform/pr-summary.ts +61 -5
- package/lib/platform/pull-requests.ts +3 -170
- package/lib/platform/snapshots.ts +53 -0
- package/lib/platform/verification-sync.ts +145 -155
- package/lib/release/workflow.ts +3 -3
- package/lib/sandbox/commands/create.ts +7 -6
- package/lib/sandbox/commands/rm.ts +3 -1909
- package/lib/sandbox/control/audit.ts +253 -0
- package/lib/sandbox/control/client.ts +109 -66
- package/lib/sandbox/control/executor.ts +134 -74
- package/lib/sandbox/control/finalization-response.ts +29 -0
- package/lib/sandbox/control/identity-sentinel.ts +137 -0
- package/lib/sandbox/control/lifecycle.ts +45 -3
- package/lib/sandbox/control/native-file-lock.ts +6 -107
- package/lib/sandbox/control/protocol.ts +75 -6
- package/lib/sandbox/control/server.ts +538 -123
- package/lib/sandbox/control/state.ts +78 -0
- package/lib/sandbox/control/task-workflow.ts +95 -0
- package/lib/sandbox/control/workflow-executor.ts +102 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/recovery.ts +28 -5
- package/lib/sandbox/removal.ts +2144 -0
- package/lib/sandbox/task-cleanup.ts +209 -0
- package/lib/sandbox/workspace-view.ts +17 -15
- package/lib/task/activity-intent.ts +2 -2
- package/lib/task/activity-log.ts +27 -19
- package/lib/task/artifact-command.ts +104 -0
- package/lib/task/artifact-lifecycle.ts +31 -26
- package/lib/task/artifact-name.ts +55 -0
- package/lib/task/artifact-operations.ts +22 -68
- package/lib/task/artifact-receipts.ts +3 -14
- package/lib/task/artifact-repair-intent.ts +99 -18
- package/lib/task/artifact-schema.ts +5 -34
- package/lib/task/capabilities.ts +13 -22
- package/lib/task/codex-orchestration.ts +85 -24
- package/lib/task/commit-identity.ts +31 -0
- package/lib/task/commit-operation.ts +2 -16
- package/lib/task/control-authority.ts +502 -5
- package/lib/task/control-recovery.ts +174 -0
- package/lib/task/create.ts +63 -8
- package/lib/task/decision-details.ts +3 -71
- package/lib/task/events.ts +131 -10
- package/lib/task/finalization-state.ts +11 -0
- package/lib/task/human-override.ts +3 -8
- package/lib/task/implementation-inputs.ts +3 -3
- package/lib/task/intermediate-cleanup.ts +455 -0
- package/lib/task/invalidation.ts +18 -62
- package/lib/task/ledger-intents.ts +7 -24
- package/lib/task/ledger.ts +1 -3
- package/lib/task/lifecycle.ts +17 -1
- package/lib/task/local-artifact-finalization.ts +148 -182
- package/lib/task/manual-validation-completion.ts +40 -0
- package/lib/task/manual-validation-evidence.ts +209 -0
- package/lib/task/manual-validation-receipt.ts +152 -0
- package/lib/task/manual-validation-shared.ts +50 -0
- package/lib/task/manual-validation-transaction.ts +304 -0
- package/lib/task/markdown.ts +70 -0
- package/lib/task/orchestration.ts +19 -19
- package/lib/task/qualification-audit.ts +22 -33
- package/lib/task/review-artifacts.ts +1 -20
- package/lib/task/review-command.ts +23 -0
- package/lib/task/review-finalization.ts +131 -259
- package/lib/task/review-fingerprint.ts +2 -2
- package/lib/task/sections.ts +33 -54
- package/lib/task/verification-engine.ts +110 -366
- package/lib/task/verification-types.ts +17 -0
- package/lib/task/verification.ts +1 -1
- package/lib/task/workflow-command.ts +18 -0
- package/lib/task/workflow-warning-intents.ts +0 -13
- package/lib/task/workflow-warnings.ts +21 -2
- package/lib/update.ts +6 -0
- package/package.json +6 -6
- package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
- package/templates/.agents/rules/testing-discipline.en.md +2 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
- package/templates/.agents/scripts/package.json +3 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +7 -3
- package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +17 -6
- package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/dist/lib/platform/adapters.js +0 -56
- package/lib/platform/adapters.ts +0 -197
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { getProcessStartTime } from '../../server/process-state.ts';
|
|
6
6
|
import {
|
|
7
7
|
createTask,
|
|
@@ -11,20 +11,32 @@ import {
|
|
|
11
11
|
type TaskCreateResult
|
|
12
12
|
} from '../../task/create-service.ts';
|
|
13
13
|
import { applyTaskFinalization } from '../../task/finalization.ts';
|
|
14
|
-
import { bindSandboxControlTask, validateSandboxControlRequest, type SandboxControlExecution, type SandboxControlManifest, type SandboxControlRequest } from './protocol.ts';
|
|
15
14
|
import {
|
|
16
|
-
|
|
15
|
+
bindSandboxControlTask,
|
|
16
|
+
validateSandboxControlRequest,
|
|
17
|
+
type SandboxControlExecution,
|
|
18
|
+
type SandboxControlExecutorGateV2,
|
|
19
|
+
type SandboxControlManifest,
|
|
20
|
+
type SandboxControlRequest
|
|
21
|
+
} from './protocol.ts';
|
|
22
|
+
import {
|
|
17
23
|
atomicWriteJson,
|
|
18
24
|
executionPath,
|
|
19
25
|
readActiveLease,
|
|
20
26
|
terminateSandboxControlExecution
|
|
21
27
|
} from './state.ts';
|
|
28
|
+
import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSandboxControlAuditContext, appendCriticalAudit, writeSandboxControlTransition } from './audit.ts';
|
|
29
|
+
import { validateSandboxControlIdentity } from './identity-sentinel.ts';
|
|
22
30
|
import {
|
|
23
31
|
createSandboxExecutorExecutionContext,
|
|
24
32
|
dispatchTaskControlOperation,
|
|
25
33
|
parseTaskControlOperation,
|
|
34
|
+
validateLifecycleRecoveryAttestation,
|
|
35
|
+
type LifecycleAuthorityResponseV1,
|
|
36
|
+
type LifecycleRecoveryAttestationV1,
|
|
26
37
|
type TaskControlOperation
|
|
27
38
|
} from '../../task/control-authority.ts';
|
|
39
|
+
import { executeTaskWorkflow } from './workflow-executor.ts';
|
|
28
40
|
import { assertSandboxControlBrokerOwner, readSandboxControlManifest, type BrokerOwner } from './lifecycle.ts';
|
|
29
41
|
import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
30
42
|
import {
|
|
@@ -86,52 +98,12 @@ function appendExecutorAudit(
|
|
|
86
98
|
fields: Record<string, string | number | boolean | null> = {}
|
|
87
99
|
): void {
|
|
88
100
|
try {
|
|
89
|
-
|
|
101
|
+
appendDiagnosticAudit(manifest, event, { source: 'executor', ...fields });
|
|
90
102
|
} catch {
|
|
91
103
|
// Diagnostics must never change the control protocol.
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
function requestAuditFields(
|
|
96
|
-
manifest: SandboxControlManifest,
|
|
97
|
-
manifestPath: string,
|
|
98
|
-
request: SandboxControlRequest
|
|
99
|
-
): Record<string, string | number | boolean | null> {
|
|
100
|
-
const args = 'args' in request ? request.args : [];
|
|
101
|
-
const encodedArgs = JSON.stringify(args);
|
|
102
|
-
return {
|
|
103
|
-
requestId: request.id,
|
|
104
|
-
requestFamily: request.family,
|
|
105
|
-
sandboxTaskId: manifest.taskId,
|
|
106
|
-
requestGeneration: request.generation,
|
|
107
|
-
requestIssuedAt: request.issuedAt,
|
|
108
|
-
requestExpiresAt: request.expiresAt,
|
|
109
|
-
requestArgCount: args.length,
|
|
110
|
-
requestArgsSha256: createHash('sha256').update(encodedArgs, 'utf8').digest('hex'),
|
|
111
|
-
requestTaskRef: args[0] ?? null,
|
|
112
|
-
requestCommand: args[1] ?? null,
|
|
113
|
-
controllerProofPresent: request.controllerProof !== null,
|
|
114
|
-
hostCwd: process.cwd(),
|
|
115
|
-
manifestPath,
|
|
116
|
-
manifestPathRealpath: safeRealpath(manifestPath),
|
|
117
|
-
repoRoot: manifest.repoRoot,
|
|
118
|
-
repoRootRealpath: safeRealpath(manifest.repoRoot),
|
|
119
|
-
worktreeRoot: manifest.worktreeRoot,
|
|
120
|
-
worktreeRootRealpath: safeRealpath(manifest.worktreeRoot),
|
|
121
|
-
runtimeDir: manifest.runtimeDir,
|
|
122
|
-
runtimeDirRealpath: safeRealpath(manifest.runtimeDir)
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function resultAuditFields(result: SandboxControlExecutionResult): Record<string, string | number | boolean | null> {
|
|
127
|
-
return {
|
|
128
|
-
exitCode: result.exitCode,
|
|
129
|
-
stdoutBytes: Buffer.byteLength(result.stdout, 'utf8'),
|
|
130
|
-
stderrBytes: Buffer.byteLength(result.stderr, 'utf8'),
|
|
131
|
-
stdoutSha256: createHash('sha256').update(result.stdout, 'utf8').digest('hex'),
|
|
132
|
-
stderrSha256: createHash('sha256').update(result.stderr, 'utf8').digest('hex')
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
107
|
|
|
136
108
|
export function nodeEntryArgs(entry: string, args: string[]): string[] {
|
|
137
109
|
return path.extname(entry) === '.ts'
|
|
@@ -155,6 +127,7 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
155
127
|
request: SandboxControlRequest;
|
|
156
128
|
requestPath: string;
|
|
157
129
|
internalCliPath: string;
|
|
130
|
+
lifecycleAuthority?: LifecycleAuthorityResponseV1;
|
|
158
131
|
}): Promise<PreparedSandboxControlExecution> {
|
|
159
132
|
const nonce = randomUUID();
|
|
160
133
|
const child = spawn(
|
|
@@ -208,7 +181,16 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
208
181
|
if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
209
182
|
atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
210
183
|
if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
211
|
-
|
|
184
|
+
const authority = params.lifecycleAuthority?.attestation ?? null;
|
|
185
|
+
child.send({
|
|
186
|
+
version: 2,
|
|
187
|
+
nonce,
|
|
188
|
+
owner: gateOwner,
|
|
189
|
+
requestId: params.request.id,
|
|
190
|
+
operationId: authority?.operationId ?? params.request.id,
|
|
191
|
+
phase: authority?.phase ?? (params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
|
|
192
|
+
authority
|
|
193
|
+
} satisfies SandboxControlExecutorGateV2);
|
|
212
194
|
},
|
|
213
195
|
completion,
|
|
214
196
|
terminate(updateState = true) {
|
|
@@ -222,7 +204,7 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
222
204
|
};
|
|
223
205
|
}
|
|
224
206
|
|
|
225
|
-
function waitForGate(nonce: string, timeoutMs = 2_000): Promise<BrokerOwner
|
|
207
|
+
function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner: BrokerOwner; authority: LifecycleRecoveryAttestationV1 | null; requestId: string; operationId: string; phase: SandboxControlExecutorGateV2['phase'] }>> {
|
|
226
208
|
return new Promise((resolve, reject) => {
|
|
227
209
|
const timer = setTimeout(() => reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_TIMEOUT')), timeoutMs);
|
|
228
210
|
const onDisconnect = () => {
|
|
@@ -237,9 +219,14 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<BrokerOwner> {
|
|
|
237
219
|
version?: unknown;
|
|
238
220
|
nonce?: unknown;
|
|
239
221
|
owner?: Partial<BrokerOwner> | null;
|
|
222
|
+
requestId?: unknown;
|
|
223
|
+
operationId?: unknown;
|
|
224
|
+
phase?: unknown;
|
|
225
|
+
authority?: unknown;
|
|
240
226
|
} | null;
|
|
241
227
|
const owner = value?.owner;
|
|
242
|
-
if (!value ||
|
|
228
|
+
if (!value || Object.keys(value).sort().join(',') !== 'authority,nonce,operationId,owner,phase,requestId,version'
|
|
229
|
+
|| value.version !== 2 || value.nonce !== nonce
|
|
243
230
|
|| !owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
|
|
244
231
|
|| typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
|
|
245
232
|
|| typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
|
|
@@ -247,7 +234,21 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<BrokerOwner> {
|
|
|
247
234
|
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
|
|
248
235
|
return;
|
|
249
236
|
}
|
|
250
|
-
|
|
237
|
+
if (typeof value.requestId !== 'string' || typeof value.operationId !== 'string'
|
|
238
|
+
|| !['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
|
|
239
|
+
|| (value.authority !== null && value.authority !== undefined && (() => {
|
|
240
|
+
try { validateLifecycleRecoveryAttestation(value.authority); return false; } catch { return true; }
|
|
241
|
+
})())) {
|
|
242
|
+
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
resolve({
|
|
246
|
+
owner: owner as BrokerOwner,
|
|
247
|
+
authority: value.authority === null || value.authority === undefined ? null : value.authority as LifecycleRecoveryAttestationV1,
|
|
248
|
+
requestId: value.requestId,
|
|
249
|
+
operationId: value.operationId,
|
|
250
|
+
phase: value.phase as SandboxControlExecutorGateV2['phase']
|
|
251
|
+
});
|
|
251
252
|
});
|
|
252
253
|
});
|
|
253
254
|
}
|
|
@@ -327,6 +328,7 @@ function finalizationResult(result: Awaited<ReturnType<typeof applyTaskFinalizat
|
|
|
327
328
|
type ExecuteRequestOptions = Readonly<{
|
|
328
329
|
buildIdentity?: typeof computeLifecycleBuildIdentity;
|
|
329
330
|
resolveControllerBinding?: typeof resolveCodexControllerBinding;
|
|
331
|
+
lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
|
|
330
332
|
}>;
|
|
331
333
|
|
|
332
334
|
async function executeRequestInner(
|
|
@@ -394,7 +396,7 @@ async function executeRequestInner(
|
|
|
394
396
|
: /^([A-Z][A-Z0-9_]+)/u.exec(error instanceof Error ? error.message : String(error))?.[1]
|
|
395
397
|
?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
|
|
396
398
|
appendExecutorAudit(manifest, 'controller-operation-failed', {
|
|
397
|
-
...requestAuditFields(manifest,
|
|
399
|
+
...requestAuditFields(manifest, request),
|
|
398
400
|
controllerCommand: request.command,
|
|
399
401
|
errorCode,
|
|
400
402
|
errorType: error instanceof Error ? error.name : typeof error,
|
|
@@ -402,6 +404,7 @@ async function executeRequestInner(
|
|
|
402
404
|
return controllerFailure(error);
|
|
403
405
|
}
|
|
404
406
|
}
|
|
407
|
+
if (request.family === 'task-workflow') return executeTaskWorkflow(manifest, request.workflow, options.lifecycleRecoveryAttestation ?? null);
|
|
405
408
|
if (request.family === 'task-finalization') {
|
|
406
409
|
const operation = parseTaskControlOperation(
|
|
407
410
|
'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
|
|
@@ -415,7 +418,8 @@ async function executeRequestInner(
|
|
|
415
418
|
taskId: manifest.taskId!,
|
|
416
419
|
generation: manifest.generation,
|
|
417
420
|
manifestPath,
|
|
418
|
-
requestId: request.id
|
|
421
|
+
requestId: request.id,
|
|
422
|
+
lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
|
|
419
423
|
}),
|
|
420
424
|
operation
|
|
421
425
|
);
|
|
@@ -445,23 +449,31 @@ async function executeRequestInner(
|
|
|
445
449
|
&& operation.input.client === 'codex';
|
|
446
450
|
if (codexPrepare) {
|
|
447
451
|
if (!request.controllerProof) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
452
|
+
const authority = options.lifecycleRecoveryAttestation;
|
|
453
|
+
if (!authority || authority.phase !== 'orchestration.prepare') {
|
|
454
|
+
return orchestrationFailure(
|
|
455
|
+
'CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED',
|
|
456
|
+
'Codex prepare requires a current controller lease proof'
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
controllerBinding = {
|
|
460
|
+
instanceDigest: authority.controllerInstanceDigest,
|
|
461
|
+
controlGeneration: authority.controlGeneration
|
|
462
|
+
};
|
|
463
|
+
} else {
|
|
464
|
+
try {
|
|
465
|
+
controllerBinding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
|
|
466
|
+
manifest,
|
|
467
|
+
manifestPath,
|
|
468
|
+
proof: request.controllerProof,
|
|
469
|
+
buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
|
|
470
|
+
});
|
|
471
|
+
} catch (error) {
|
|
472
|
+
const code = error instanceof CodexControllerRegistrationError
|
|
473
|
+
? error.code
|
|
474
|
+
: 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID';
|
|
475
|
+
return orchestrationFailure(code, `${code}: Codex controller proof was rejected`);
|
|
476
|
+
}
|
|
465
477
|
}
|
|
466
478
|
} else if (request.controllerProof !== null) {
|
|
467
479
|
return orchestrationFailure(
|
|
@@ -487,7 +499,8 @@ async function executeRequestInner(
|
|
|
487
499
|
manifestPath,
|
|
488
500
|
requestId: request.id,
|
|
489
501
|
diagnosticLog,
|
|
490
|
-
...(controllerBinding ? { controllerBinding } : {})
|
|
502
|
+
...(controllerBinding ? { controllerBinding } : {}),
|
|
503
|
+
lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
|
|
491
504
|
});
|
|
492
505
|
const format = (value: unknown): SandboxControlExecutionResult => {
|
|
493
506
|
const result = value as { status?: string };
|
|
@@ -511,7 +524,7 @@ export async function executeRequest(
|
|
|
511
524
|
request: SandboxControlRequest,
|
|
512
525
|
options: ExecuteRequestOptions = {}
|
|
513
526
|
): Promise<SandboxControlExecutionResult> {
|
|
514
|
-
const fields = requestAuditFields(manifest,
|
|
527
|
+
const fields = requestAuditFields(manifest, request);
|
|
515
528
|
appendExecutorAudit(manifest, 'executor-request-start', fields);
|
|
516
529
|
try {
|
|
517
530
|
const result = await executeRequestInner(manifest, manifestPath, request, options);
|
|
@@ -534,11 +547,23 @@ export async function executeRequest(
|
|
|
534
547
|
}
|
|
535
548
|
|
|
536
549
|
export async function runSandboxControlExecutor(requestPath: string, nonce: string): Promise<void> {
|
|
537
|
-
const
|
|
550
|
+
const gate = await waitForGate(nonce);
|
|
551
|
+
const gateOwner = gate.owner;
|
|
538
552
|
process.disconnect?.();
|
|
539
553
|
const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
|
|
540
554
|
if (!manifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
|
|
541
555
|
const manifest = readSandboxControlManifest(manifestPath);
|
|
556
|
+
const identity = validateSandboxControlIdentity({
|
|
557
|
+
publicStatusDir: manifest.publicStatusDir,
|
|
558
|
+
root: path.dirname(path.resolve(manifestPath)),
|
|
559
|
+
mode: manifest.mode,
|
|
560
|
+
taskId: manifest.taskId,
|
|
561
|
+
generation: manifest.generation,
|
|
562
|
+
controlRootId: manifest.controlRootId
|
|
563
|
+
});
|
|
564
|
+
if (identity.state !== 'valid') {
|
|
565
|
+
throw new Error(`SANDBOX_CONTROL_IDENTITY_${identity.state.replaceAll('-', '_').toUpperCase()}`);
|
|
566
|
+
}
|
|
542
567
|
const root = fs.realpathSync.native(process.cwd());
|
|
543
568
|
const expectedRoot = safeRealpath(manifest.repoRoot);
|
|
544
569
|
if (expectedRoot !== root) {
|
|
@@ -551,13 +576,20 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
551
576
|
}
|
|
552
577
|
const raw = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as unknown;
|
|
553
578
|
const request = validateSandboxControlRequest(raw, manifest);
|
|
554
|
-
|
|
579
|
+
if (gate.requestId !== request.id) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_REQUEST_MISMATCH');
|
|
580
|
+
if (gate.authority !== null
|
|
581
|
+
&& (gate.authority.requestId !== request.id
|
|
582
|
+
|| gate.authority.operationId !== gate.operationId
|
|
583
|
+
|| gate.authority.phase !== gate.phase)) {
|
|
584
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_AUTHORITY_MISMATCH');
|
|
585
|
+
}
|
|
586
|
+
appendExecutorAudit(manifest, 'executor-request-validated', requestAuditFields(manifest, request));
|
|
555
587
|
const requestDirectory = path.resolve(path.dirname(requestPath));
|
|
556
588
|
const processingDirectory = path.resolve(path.join(manifest.processingDir, request.id));
|
|
557
589
|
const channelRequestDirectory = path.resolve(path.join(manifest.channelDir, 'requests'));
|
|
558
590
|
if (requestDirectory !== processingDirectory && requestDirectory !== channelRequestDirectory) {
|
|
559
591
|
appendExecutorAudit(manifest, 'executor-request-path-validation-failed', {
|
|
560
|
-
...requestAuditFields(manifest,
|
|
592
|
+
...requestAuditFields(manifest, request),
|
|
561
593
|
requestPath,
|
|
562
594
|
requestDirectory,
|
|
563
595
|
processingDirectory,
|
|
@@ -568,10 +600,32 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
568
600
|
let result: SandboxControlExecutionResult;
|
|
569
601
|
try {
|
|
570
602
|
assertSandboxControlExecutorAuthority(manifest, gateOwner);
|
|
571
|
-
|
|
603
|
+
const operation = request.family === 'task-lifecycle' || request.family === 'task-orchestration'
|
|
604
|
+
? (() => {
|
|
605
|
+
try { return parseTaskControlOperation(request.family, request.args); } catch { return null; }
|
|
606
|
+
})()
|
|
607
|
+
: null;
|
|
608
|
+
const context = createSandboxControlAuditContext(manifest, {
|
|
609
|
+
requestId: request.id,
|
|
610
|
+
family: request.family,
|
|
611
|
+
operation: operation?.family === 'task-orchestration'
|
|
612
|
+
? operation.intent
|
|
613
|
+
: operation?.family === 'task-lifecycle'
|
|
614
|
+
? operation.request.intent
|
|
615
|
+
: request.family === 'task-finalization' ? request.operation : null,
|
|
616
|
+
phase: 'started-committed',
|
|
617
|
+
outcome: 'in-progress'
|
|
618
|
+
});
|
|
619
|
+
appendCriticalAudit(manifest, context, { transition: 'started-committed' });
|
|
620
|
+
writeSandboxControlTransition(manifest, { requestId: request.id, phase: 'started-committed' });
|
|
621
|
+
const execution = readJsonExecution(executionPath(manifest, request.id));
|
|
622
|
+
atomicWriteJson(executionPath(manifest, request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
623
|
+
result = await executeRequest(manifest, manifestPath, request, {
|
|
624
|
+
lifecycleRecoveryAttestation: gate.authority
|
|
625
|
+
});
|
|
572
626
|
} catch (error) {
|
|
573
627
|
appendExecutorAudit(manifest, 'executor-authority-or-dispatch-failed', {
|
|
574
|
-
...requestAuditFields(manifest,
|
|
628
|
+
...requestAuditFields(manifest, request),
|
|
575
629
|
errorType: error instanceof Error ? error.name : typeof error,
|
|
576
630
|
});
|
|
577
631
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -596,6 +650,12 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
596
650
|
process.exitCode = result.exitCode;
|
|
597
651
|
}
|
|
598
652
|
|
|
653
|
+
function readJsonExecution(filePath: string): SandboxControlExecution {
|
|
654
|
+
const value = JSON.parse(fs.readFileSync(filePath, 'utf8')) as SandboxControlExecution;
|
|
655
|
+
if (!value || value.requestId.length === 0) throw new Error('SANDBOX_CONTROL_EXECUTION_INVALID');
|
|
656
|
+
return value;
|
|
657
|
+
}
|
|
658
|
+
|
|
599
659
|
export function disconnectExecutor(child: ChildProcess): void {
|
|
600
660
|
if (child.connected) child.disconnect();
|
|
601
661
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TaskFinalizationReceipt } from '../../task/finalization.ts';
|
|
2
|
+
import type { SandboxControlResponse } from './protocol.ts';
|
|
3
|
+
|
|
4
|
+
export function finalizationTerminalResponse(taskId: string, requestId: string, receipt: TaskFinalizationReceipt): SandboxControlResponse {
|
|
5
|
+
const pendingSteps = [
|
|
6
|
+
receipt.taskComment === 'pending' ? 'task-comment' : null,
|
|
7
|
+
receipt.verification === 'pending' ? 'verification' : null
|
|
8
|
+
].filter((step): step is string => step !== null);
|
|
9
|
+
const completedSteps = ['lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment', receipt.verification === 'pending' ? null : 'verification']
|
|
10
|
+
.filter((step): step is string => step !== null);
|
|
11
|
+
const warnings = receipt.warnings
|
|
12
|
+
.filter((warning) => warning.status === 'open')
|
|
13
|
+
.map(({ status: _status, resolvedAt: _resolvedAt, ...warning }) => warning);
|
|
14
|
+
const result = {
|
|
15
|
+
status: 'completed', changed: false, taskId,
|
|
16
|
+
lifecycle: { status: 'no-op', changed: false, error: null },
|
|
17
|
+
taskComment: receipt.taskComment === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
18
|
+
verification: receipt.verification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
19
|
+
completedSteps, pendingSteps,
|
|
20
|
+
result: pendingSteps.length > 0 || receipt.warningProjection === 'pending' || warnings.length > 0
|
|
21
|
+
? 'completed_with_warnings' : 'completed',
|
|
22
|
+
warnings, error: null
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
version: 2, id: requestId, phase: 'completed', exitCode: 0,
|
|
26
|
+
stdout: `${JSON.stringify({ version: 1, status: 'completed', changed: false, accepted: true, result, error: null })}\n`,
|
|
27
|
+
stderr: '', error: null
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { writeDurableFile } from '../../fs/durable-write.ts';
|
|
5
|
+
|
|
6
|
+
export const SANDBOX_CONTROL_IDENTITY_FILE = 'identity.json';
|
|
7
|
+
|
|
8
|
+
export type SandboxControlIdentitySentinel = Readonly<{
|
|
9
|
+
version: 1;
|
|
10
|
+
mode: 'task-bound' | 'branch-only';
|
|
11
|
+
taskId: string | null;
|
|
12
|
+
generation: string;
|
|
13
|
+
controlRootId: string;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
export type SandboxControlIdentityValidation = Readonly<{
|
|
17
|
+
state: 'valid' | 'missing' | 'malformed' | 'generation-mismatch' | 'root-id-mismatch' | 'topology-mismatch';
|
|
18
|
+
sentinel: SandboxControlIdentitySentinel | null;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
function identityError(code: SandboxControlIdentityValidation['state']): Error {
|
|
22
|
+
return new Error(`SANDBOX_CONTROL_IDENTITY_${code.replaceAll('-', '_').toUpperCase()}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function canonicalIdentity(value: SandboxControlIdentitySentinel): string {
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
version: value.version,
|
|
28
|
+
mode: value.mode,
|
|
29
|
+
taskId: value.taskId,
|
|
30
|
+
generation: value.generation,
|
|
31
|
+
controlRootId: value.controlRootId
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function identitySentinelPath(publicStatusDir: string): string {
|
|
36
|
+
return path.join(publicStatusDir, SANDBOX_CONTROL_IDENTITY_FILE);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createSandboxControlRootId(): string {
|
|
40
|
+
return `${randomBytes(32).toString('hex')}${randomUUID().replaceAll('-', '')}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createSandboxControlIdentitySentinel(params: Readonly<{
|
|
44
|
+
mode: 'task-bound' | 'branch-only';
|
|
45
|
+
taskId: string | null;
|
|
46
|
+
generation: string;
|
|
47
|
+
controlRootId?: string;
|
|
48
|
+
}>): SandboxControlIdentitySentinel {
|
|
49
|
+
const sentinel: SandboxControlIdentitySentinel = {
|
|
50
|
+
version: 1,
|
|
51
|
+
mode: params.mode,
|
|
52
|
+
taskId: params.mode === 'task-bound' ? params.taskId : null,
|
|
53
|
+
generation: params.generation,
|
|
54
|
+
controlRootId: params.controlRootId ?? createSandboxControlRootId()
|
|
55
|
+
};
|
|
56
|
+
parseSandboxControlIdentitySentinel(sentinel);
|
|
57
|
+
return sentinel;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function identityDigest(value: SandboxControlIdentitySentinel): string {
|
|
61
|
+
const normalized = parseSandboxControlIdentitySentinel(value);
|
|
62
|
+
return createHash('sha256').update(canonicalIdentity(normalized), 'utf8').digest('hex');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseSandboxControlIdentitySentinel(value: unknown): SandboxControlIdentitySentinel {
|
|
66
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw identityError('malformed');
|
|
67
|
+
const candidate = value as Partial<SandboxControlIdentitySentinel>;
|
|
68
|
+
if (Object.keys(candidate).sort().join(',') !== 'controlRootId,generation,mode,taskId,version'
|
|
69
|
+
|| candidate.version !== 1
|
|
70
|
+
|| (candidate.mode !== 'task-bound' && candidate.mode !== 'branch-only')
|
|
71
|
+
|| (candidate.taskId !== null && typeof candidate.taskId !== 'string')
|
|
72
|
+
|| (candidate.mode === 'task-bound' && !candidate.taskId)
|
|
73
|
+
|| (candidate.mode === 'branch-only' && candidate.taskId !== null)
|
|
74
|
+
|| typeof candidate.generation !== 'string' || candidate.generation.length === 0
|
|
75
|
+
|| typeof candidate.controlRootId !== 'string' || !/^[a-f0-9]{64,128}$/u.test(candidate.controlRootId)) {
|
|
76
|
+
throw identityError('malformed');
|
|
77
|
+
}
|
|
78
|
+
return candidate as SandboxControlIdentitySentinel;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function writeSandboxControlIdentitySentinel(
|
|
82
|
+
publicStatusDir: string,
|
|
83
|
+
value: SandboxControlIdentitySentinel
|
|
84
|
+
): SandboxControlIdentitySentinel {
|
|
85
|
+
const sentinel = parseSandboxControlIdentitySentinel(value);
|
|
86
|
+
fs.mkdirSync(publicStatusDir, { recursive: true, mode: 0o700 });
|
|
87
|
+
const target = identitySentinelPath(publicStatusDir);
|
|
88
|
+
writeDurableFile(target, `${canonicalIdentity(sentinel)}\n`, { mode: 0o400, replace: true });
|
|
89
|
+
return sentinel;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function readSandboxControlIdentitySentinel(publicStatusDir: string): SandboxControlIdentitySentinel {
|
|
93
|
+
const filePath = identitySentinelPath(publicStatusDir);
|
|
94
|
+
if (!fs.existsSync(filePath)) throw identityError('missing');
|
|
95
|
+
try {
|
|
96
|
+
const stat = fs.lstatSync(filePath);
|
|
97
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw identityError('malformed');
|
|
98
|
+
return parseSandboxControlIdentitySentinel(JSON.parse(fs.readFileSync(filePath, 'utf8')));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof Error && error.message.startsWith('SANDBOX_CONTROL_IDENTITY_')) throw error;
|
|
101
|
+
throw identityError('malformed');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function validateSandboxControlIdentity(params: Readonly<{
|
|
106
|
+
publicStatusDir: string;
|
|
107
|
+
root: string;
|
|
108
|
+
manifestRoot?: string;
|
|
109
|
+
mode: 'task-bound' | 'branch-only';
|
|
110
|
+
taskId: string | null;
|
|
111
|
+
generation: string;
|
|
112
|
+
controlRootId: string;
|
|
113
|
+
}>): SandboxControlIdentityValidation {
|
|
114
|
+
let sentinel: SandboxControlIdentitySentinel;
|
|
115
|
+
try {
|
|
116
|
+
sentinel = readSandboxControlIdentitySentinel(params.publicStatusDir);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const code = error instanceof Error ? error.message : '';
|
|
119
|
+
if (code.endsWith('MISSING')) return { state: 'missing', sentinel: null };
|
|
120
|
+
return { state: 'malformed', sentinel: null };
|
|
121
|
+
}
|
|
122
|
+
if (sentinel.generation !== params.generation) return { state: 'generation-mismatch', sentinel };
|
|
123
|
+
if (sentinel.controlRootId !== params.controlRootId) return { state: 'root-id-mismatch', sentinel };
|
|
124
|
+
if (sentinel.mode !== params.mode || sentinel.taskId !== params.taskId) return { state: 'topology-mismatch', sentinel };
|
|
125
|
+
if (params.manifestRoot && path.resolve(params.manifestRoot) !== path.resolve(params.root)) {
|
|
126
|
+
return { state: 'topology-mismatch', sentinel };
|
|
127
|
+
}
|
|
128
|
+
return { state: 'valid', sentinel };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function assertSandboxControlIdentity(params: Parameters<typeof validateSandboxControlIdentity>[0]): SandboxControlIdentitySentinel {
|
|
132
|
+
const result = validateSandboxControlIdentity(params);
|
|
133
|
+
if (result.state !== 'valid' || !result.sentinel) {
|
|
134
|
+
throw new Error(`SANDBOX_CONTROL_IDENTITY_${result.state.replaceAll('-', '_').toUpperCase()}`);
|
|
135
|
+
}
|
|
136
|
+
return result.sentinel;
|
|
137
|
+
}
|
|
@@ -85,6 +85,7 @@ export type SandboxRemovalTargetCommit = Readonly<{
|
|
|
85
85
|
}>;
|
|
86
86
|
}>[];
|
|
87
87
|
}>;
|
|
88
|
+
|
|
88
89
|
export type SandboxRemovalJournal = Readonly<{
|
|
89
90
|
version: 2;
|
|
90
91
|
operation: 'sandbox-rm';
|
|
@@ -348,7 +349,7 @@ type RemovalJournalCursor = Readonly<{
|
|
|
348
349
|
write(phase: SandboxRemovalJournalPhase): SandboxRemovalJournal;
|
|
349
350
|
}>;
|
|
350
351
|
|
|
351
|
-
function defaultRemovalTarget(manifest: SandboxControlManifest): SandboxRemovalTargetCommit {
|
|
352
|
+
function defaultRemovalTarget(manifest: Pick<SandboxControlManifest, 'project' | 'branch' | 'container' | 'channelDir'>): SandboxRemovalTargetCommit {
|
|
352
353
|
const targetDigest = createHash('sha256')
|
|
353
354
|
.update(`${manifest.project}\0${manifest.branch}\0${manifest.container}\0${manifest.channelDir}`)
|
|
354
355
|
.digest('hex');
|
|
@@ -370,6 +371,18 @@ function defaultRemovalTarget(manifest: SandboxControlManifest): SandboxRemovalT
|
|
|
370
371
|
};
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
export function isDefaultSandboxRemovalJournal(journal: SandboxRemovalJournal): boolean {
|
|
375
|
+
if (journal.phase !== 'carrier-removed' && journal.phase !== 'completed') return false;
|
|
376
|
+
const { target } = journal;
|
|
377
|
+
const expected = defaultRemovalTarget({
|
|
378
|
+
project: target.project, branch: target.branch,
|
|
379
|
+
container: path.basename(path.dirname(target.controlRoot)),
|
|
380
|
+
channelDir: path.join(target.controlRoot, 'channel')
|
|
381
|
+
});
|
|
382
|
+
return Object.entries(expected).every(([key, value]) =>
|
|
383
|
+
JSON.stringify(target[key as keyof SandboxRemovalTargetCommit]) === JSON.stringify(value));
|
|
384
|
+
}
|
|
385
|
+
|
|
373
386
|
function sameRemovalTarget(left: SandboxRemovalTargetCommit, right: SandboxRemovalTargetCommit): boolean {
|
|
374
387
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
375
388
|
}
|
|
@@ -560,6 +573,21 @@ export function advanceSandboxRemovalJournalPhase(
|
|
|
560
573
|
}
|
|
561
574
|
}
|
|
562
575
|
|
|
576
|
+
export function advanceSandboxRemovalJournalToPhase(
|
|
577
|
+
journal: SandboxRemovalJournal,
|
|
578
|
+
phase: SandboxRemovalJournalPhase,
|
|
579
|
+
resourceLock: SandboxResourceLock
|
|
580
|
+
): SandboxRemovalJournal {
|
|
581
|
+
let current = journal;
|
|
582
|
+
while (phaseIndex(current.phase) < phaseIndex(phase)) {
|
|
583
|
+
const next = current.phase === 'target-committed' && phase === 'container-absent'
|
|
584
|
+
? phase : SANDBOX_REMOVAL_JOURNAL_PHASES[phaseIndex(current.phase) + 1];
|
|
585
|
+
if (!next) throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
|
|
586
|
+
current = advanceSandboxRemovalJournalPhase(current, next, { resourceLock });
|
|
587
|
+
}
|
|
588
|
+
return current;
|
|
589
|
+
}
|
|
590
|
+
|
|
563
591
|
async function awaitWithDeadline<T>(
|
|
564
592
|
operation: () => Promise<T>,
|
|
565
593
|
deadlineAt: number,
|
|
@@ -1051,7 +1079,7 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
1051
1079
|
const expectedKeys = [
|
|
1052
1080
|
'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
|
|
1053
1081
|
'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
|
|
1054
|
-
'taskId', 'token', 'worktreeRoot', 'authorityEvidence'
|
|
1082
|
+
'taskId', 'token', 'worktreeRoot', 'authorityEvidence', 'controlRootId',
|
|
1055
1083
|
];
|
|
1056
1084
|
const actualKeys = Object.keys(manifest).sort().join(',');
|
|
1057
1085
|
if (actualKeys !== expectedKeys.sort().join(',')) {
|
|
@@ -1075,6 +1103,7 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
1075
1103
|
|| typeof candidate.processingDir !== 'string' || typeof candidate.runtimeDir !== 'string'
|
|
1076
1104
|
|| typeof candidate.token !== 'string'
|
|
1077
1105
|
|| typeof candidate.generation !== 'string'
|
|
1106
|
+
|| typeof candidate.controlRootId !== 'string' || !/^[a-f0-9]{64,128}$/u.test(candidate.controlRootId)
|
|
1078
1107
|
|| (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
|
|
1079
1108
|
|| (candidate.mode === 'branch-only' && candidate.taskId !== null)
|
|
1080
1109
|
|| !isSandboxAuthorityEvidence(candidate.authorityEvidence)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
@@ -1103,10 +1132,11 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
1103
1132
|
taskId: candidate.taskId,
|
|
1104
1133
|
token: candidate.token,
|
|
1105
1134
|
generation: candidate.generation,
|
|
1135
|
+
controlRootId: candidate.controlRootId,
|
|
1106
1136
|
channelDir: candidate.channelDir,
|
|
1107
1137
|
publicStatusDir: candidate.publicStatusDir,
|
|
1108
1138
|
processingDir: candidate.processingDir,
|
|
1109
|
-
runtimeDir: candidate.runtimeDir
|
|
1139
|
+
runtimeDir: candidate.runtimeDir,
|
|
1110
1140
|
};
|
|
1111
1141
|
return normalized;
|
|
1112
1142
|
}
|
|
@@ -1380,6 +1410,18 @@ export type RemoveSandboxControlOptions = Readonly<{
|
|
|
1380
1410
|
export async function removeSandboxControlRoot(
|
|
1381
1411
|
root: string,
|
|
1382
1412
|
options: RemoveSandboxControlOptions
|
|
1413
|
+
): Promise<void> {
|
|
1414
|
+
try {
|
|
1415
|
+
await removeSandboxControlRootAttempt(root, options);
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
if (!(error instanceof Error) || error.message !== 'SANDBOX_CONTROL_OWNER_UNAVAILABLE') throw error;
|
|
1418
|
+
await removeSandboxControlRootAttempt(root, options);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
async function removeSandboxControlRootAttempt(
|
|
1423
|
+
root: string,
|
|
1424
|
+
options: RemoveSandboxControlOptions
|
|
1383
1425
|
): Promise<void> {
|
|
1384
1426
|
const resolvedRoot = path.resolve(root);
|
|
1385
1427
|
if (!fs.existsSync(resolvedRoot)) return;
|