@fitlab-ai/agent-infra 0.11.13 → 0.11.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/dist/bin/internal-cli.js +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/task-finalization.js +7 -5
- package/dist/lib/internal/task-lifecycle.js +3 -1
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/pr-change-report.js +10 -3
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/process-data/index.js +15 -4
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +5 -4
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +12 -47
- package/dist/lib/sandbox/control/executor.js +6 -10
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +22 -37
- package/dist/lib/sandbox/control/protocol.js +8 -36
- package/dist/lib/sandbox/control/server.js +2 -21
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -59
- package/dist/lib/sandbox/recovery.js +2 -3
- package/dist/lib/sandbox/removal.js +19 -4
- package/dist/lib/sandbox/workspace-identity.js +30 -15
- package/dist/lib/sandbox/workspace-view.js +41 -4
- package/dist/lib/task/control-authority.js +39 -17
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +13 -2
- package/dist/lib/task/lifecycle.js +122 -35
- package/dist/lib/task/short-id.js +2 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/task-finalization.ts +8 -6
- package/lib/internal/task-lifecycle.ts +3 -1
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/pr-change-report.ts +9 -3
- package/lib/platform/release-notes.ts +2 -2
- package/lib/process-data/index.ts +15 -4
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +5 -4
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +13 -61
- package/lib/sandbox/control/executor.ts +7 -13
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +22 -37
- package/lib/sandbox/control/protocol.ts +9 -45
- package/lib/sandbox/control/server.ts +2 -17
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +92 -75
- package/lib/sandbox/recovery.ts +2 -3
- package/lib/sandbox/removal.ts +16 -4
- package/lib/sandbox/workspace-identity.ts +36 -13
- package/lib/sandbox/workspace-view.ts +40 -5
- package/lib/task/control-authority.ts +40 -26
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +15 -2
- package/lib/task/lifecycle.ts +135 -33
- package/lib/task/short-id.ts +5 -2
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -2
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +12 -2
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +12 -2
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +13 -2
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +13 -2
- 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/watch-pr/SKILL.en.md +19 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +14 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +15 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/runtime/agent-runtime.js +0 -33
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/dist/lib/task/runtime-paths.js +0 -10
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/runtime/agent-runtime.ts +0 -41
- package/lib/sandbox/control/agent-operation.ts +0 -49
- package/lib/task/runtime-paths.ts +0 -12
|
@@ -24,20 +24,73 @@ function manifestDigest(manifest: SandboxControlManifest): string {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
function readManagedManifest(manifestPath: string): SandboxControlManifest {
|
|
28
|
+
try { return readSandboxControlManifest(manifestPath); } catch { throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID'); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function branchOnlyManifests(parentPath: string): SandboxControlManifest[] {
|
|
32
|
+
const manifests: SandboxControlManifest[] = [];
|
|
33
|
+
for (const identity of fs.readdirSync(parentPath, { withFileTypes: true })) {
|
|
34
|
+
if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name)) continue;
|
|
35
|
+
manifests.push(readManagedManifest(path.join(parentPath, identity.name, 'manifest.json')));
|
|
36
|
+
}
|
|
37
|
+
return manifests;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function taskBoundManifests(params: Readonly<{
|
|
41
|
+
tasksRoot: string;
|
|
42
|
+
controlBase: string;
|
|
43
|
+
project: string;
|
|
44
|
+
repoRoot: string;
|
|
45
|
+
}>): SandboxControlManifest[] {
|
|
46
|
+
assertDirectory(params.tasksRoot);
|
|
47
|
+
const canonicalManagedRoot = fs.realpathSync.native(path.dirname(params.tasksRoot));
|
|
48
|
+
const canonicalTasksRoot = fs.realpathSync.native(params.tasksRoot);
|
|
49
|
+
if (path.dirname(canonicalTasksRoot) !== canonicalManagedRoot) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
50
|
+
const canonicalRepoRoot = fs.realpathSync.native(params.repoRoot);
|
|
51
|
+
const manifests: SandboxControlManifest[] = [];
|
|
52
|
+
for (const task of fs.readdirSync(params.tasksRoot, { withFileTypes: true })) {
|
|
53
|
+
if (!/^TASK-\d{8}-\d{6}$/u.test(task.name)) continue;
|
|
54
|
+
if (!task.isDirectory() || task.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
55
|
+
const taskRoot = path.join(params.tasksRoot, task.name);
|
|
56
|
+
assertNoSymlinkPath(params.controlBase, taskRoot);
|
|
57
|
+
const canonicalTaskRoot = fs.realpathSync.native(taskRoot);
|
|
58
|
+
if (path.dirname(canonicalTaskRoot) !== canonicalTasksRoot) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
59
|
+
const manifestPath = path.join(taskRoot, 'manifest.json');
|
|
60
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
61
|
+
const manifest = readManagedManifest(manifestPath);
|
|
62
|
+
if (manifest.mode !== 'task-bound' || manifest.taskId !== task.name || manifest.project !== params.project
|
|
63
|
+
|| fs.realpathSync.native(manifest.repoRoot) !== canonicalRepoRoot
|
|
64
|
+
|| path.dirname(fs.realpathSync.native(manifestPath)) !== canonicalTaskRoot) {
|
|
65
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
38
66
|
}
|
|
67
|
+
manifests.push(manifest);
|
|
68
|
+
}
|
|
69
|
+
return manifests;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function managedManifestCandidates(params: Readonly<{
|
|
73
|
+
managedRoot: string;
|
|
74
|
+
controlBase?: string;
|
|
75
|
+
project?: string;
|
|
76
|
+
repoRoot?: string;
|
|
77
|
+
}>): SandboxControlManifest[] {
|
|
78
|
+
const managedRoot = params.managedRoot;
|
|
79
|
+
const { controlBase, project, repoRoot } = params;
|
|
80
|
+
if (controlBase && project && path.resolve(managedRoot) !== path.resolve(controlBase, project)) {
|
|
81
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
39
82
|
}
|
|
40
|
-
return
|
|
83
|
+
if (!fs.existsSync(managedRoot)) return [];
|
|
84
|
+
assertDirectory(managedRoot);
|
|
85
|
+
return fs.readdirSync(managedRoot, { withFileTypes: true })
|
|
86
|
+
.flatMap((parent) => {
|
|
87
|
+
const parentPath = path.join(managedRoot, parent.name);
|
|
88
|
+
if (parent.name === 'tasks' && controlBase && project && repoRoot) {
|
|
89
|
+
return taskBoundManifests({ tasksRoot: parentPath, controlBase, project, repoRoot });
|
|
90
|
+
}
|
|
91
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()) return [];
|
|
92
|
+
return branchOnlyManifests(parentPath);
|
|
93
|
+
});
|
|
41
94
|
}
|
|
42
95
|
|
|
43
96
|
function assertDirectory(directory: string): void {
|
|
@@ -45,6 +98,17 @@ function assertDirectory(directory: string): void {
|
|
|
45
98
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
46
99
|
}
|
|
47
100
|
|
|
101
|
+
function assertNoSymlinkPath(root: string, target: string): void {
|
|
102
|
+
const relative = path.relative(root, target);
|
|
103
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
104
|
+
let current = path.resolve(root);
|
|
105
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
106
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
107
|
+
current = path.join(current, segment);
|
|
108
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
48
112
|
type AuditRecord = Record<string, unknown>;
|
|
49
113
|
|
|
50
114
|
function readAudit(manifest: SandboxControlManifest): AuditRecord[] {
|
|
@@ -77,12 +141,16 @@ function auditProof(manifest: SandboxControlManifest, requestId: string): AuditR
|
|
|
77
141
|
return related;
|
|
78
142
|
}
|
|
79
143
|
|
|
80
|
-
function publishedEvidence(records: AuditRecord[],
|
|
144
|
+
function publishedEvidence(records: AuditRecord[], manifest: SandboxControlManifest): AuditRecord | null {
|
|
81
145
|
const publications = records.filter((record) => record.event === 'executor-result-published');
|
|
82
146
|
let selected: AuditRecord | null = null;
|
|
83
147
|
let selectedSummary: string | null = null;
|
|
148
|
+
const digest = manifestDigest(manifest);
|
|
84
149
|
for (const record of publications) {
|
|
85
|
-
if (record.version !== 2 || record.
|
|
150
|
+
if (record.version !== 2 || record.generation !== manifest.generation || record.identityDigest !== digest
|
|
151
|
+
|| typeof record.requestFamily !== 'string'
|
|
152
|
+
|| !['task-lifecycle', 'task-create'].includes(record.requestFamily)
|
|
153
|
+
|| (record.sandboxTaskId !== null && typeof record.sandboxTaskId !== 'string')
|
|
86
154
|
|| !Number.isSafeInteger(record.exitCode)
|
|
87
155
|
|| !Number.isSafeInteger(record.outputBytes) || !Number.isSafeInteger(record.errorBytes)
|
|
88
156
|
|| typeof record.outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.outputDigest)
|
|
@@ -90,7 +158,7 @@ function publishedEvidence(records: AuditRecord[], generation: string): AuditRec
|
|
|
90
158
|
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
91
159
|
}
|
|
92
160
|
const summary = JSON.stringify([
|
|
93
|
-
record.requestFamily
|
|
161
|
+
record.requestFamily, record.sandboxTaskId, record.generation, record.identityDigest, record.exitCode,
|
|
94
162
|
record.outputBytes, record.errorBytes, record.outputDigest, record.errorDigest
|
|
95
163
|
]);
|
|
96
164
|
if (selectedSummary !== null && summary !== selectedSummary) {
|
|
@@ -99,6 +167,9 @@ function publishedEvidence(records: AuditRecord[], generation: string): AuditRec
|
|
|
99
167
|
selected ??= record;
|
|
100
168
|
selectedSummary ??= summary;
|
|
101
169
|
}
|
|
170
|
+
if (selected && selected.sandboxTaskId !== manifest.taskId) {
|
|
171
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
172
|
+
}
|
|
102
173
|
return selected;
|
|
103
174
|
}
|
|
104
175
|
|
|
@@ -118,7 +189,7 @@ function verifiedResponse(manifest: SandboxControlManifest, requestId: string, r
|
|
|
118
189
|
if (!['completed', 'rejected'].includes(response.phase)) throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
119
190
|
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') return unknown(requestId);
|
|
120
191
|
const evidencePath = path.join(manifest.processingDir, requestId, 'result.json');
|
|
121
|
-
const published = publishedEvidence(records, manifest
|
|
192
|
+
const published = publishedEvidence(records, manifest);
|
|
122
193
|
if (!published) return unknown(requestId);
|
|
123
194
|
let evidence = null;
|
|
124
195
|
if (fs.existsSync(evidencePath)) {
|
|
@@ -167,17 +238,19 @@ function verifiedResponse(manifest: SandboxControlManifest, requestId: string, r
|
|
|
167
238
|
}
|
|
168
239
|
|
|
169
240
|
export function recoverSandboxControlFromHost(requestId: string, params: Readonly<{
|
|
170
|
-
managedRoot: string;
|
|
241
|
+
managedRoot: string;
|
|
242
|
+
controlBase?: string;
|
|
243
|
+
project?: string;
|
|
244
|
+
repoRoot?: string;
|
|
245
|
+
timeoutMs?: number;
|
|
171
246
|
}>): SandboxControlResponse {
|
|
172
247
|
if (!REQUEST_ID.test(requestId)) throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
|
|
173
248
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
174
249
|
let candidates: Array<{ manifest: SandboxControlManifest; records: AuditRecord[] }> = [];
|
|
175
250
|
while (Date.now() < deadline) {
|
|
176
251
|
candidates = [];
|
|
177
|
-
for (const
|
|
178
|
-
|
|
179
|
-
try { manifest = readSandboxControlManifest(manifestPath); } catch { throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID'); }
|
|
180
|
-
if (manifest.project !== path.basename(path.resolve(params.managedRoot)) || !manifest.generation) {
|
|
252
|
+
for (const manifest of managedManifestCandidates(params)) {
|
|
253
|
+
if (manifest.project !== (params.project ?? path.basename(path.resolve(params.managedRoot))) || !manifest.generation) {
|
|
181
254
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
182
255
|
}
|
|
183
256
|
const records = auditProof(manifest, requestId);
|
|
@@ -661,28 +661,11 @@ function readTaskCompletionManifest(
|
|
|
661
661
|
return manifest;
|
|
662
662
|
}
|
|
663
663
|
|
|
664
|
-
export
|
|
664
|
+
export function validateSandboxControlTaskIdentity(
|
|
665
665
|
root: string,
|
|
666
|
-
binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }
|
|
667
|
-
|
|
668
|
-
)
|
|
669
|
-
const resolvedRoot = path.resolve(root);
|
|
670
|
-
const manifest = readTaskCompletionManifest(resolvedRoot, binding);
|
|
671
|
-
markSandboxControlRootQuiescing(resolvedRoot);
|
|
672
|
-
const deadline = Date.now() + timeoutMs;
|
|
673
|
-
const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
674
|
-
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
675
|
-
while (true) {
|
|
676
|
-
const startup = readStartupOwner(startupPath);
|
|
677
|
-
if (fs.existsSync(startupPath) && !startup) throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
678
|
-
const broker = readBrokerOwner(brokerPath);
|
|
679
|
-
if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
680
|
-
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
681
|
-
}
|
|
682
|
-
if (!startup && !broker) return;
|
|
683
|
-
if (Date.now() >= deadline) throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
|
|
684
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
685
|
-
}
|
|
666
|
+
binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }>
|
|
667
|
+
): void {
|
|
668
|
+
readTaskCompletionManifest(path.resolve(root), binding);
|
|
686
669
|
}
|
|
687
670
|
|
|
688
671
|
function recordSandboxRemovalPending(
|
|
@@ -1155,30 +1138,32 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
1155
1138
|
|| !isSandboxAuthorityEvidence(candidate.authorityEvidence)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
1156
1139
|
const root = path.dirname(path.resolve(manifestPath));
|
|
1157
1140
|
const canonicalRoot = fs.realpathSync.native(root);
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1141
|
+
const resolveControlChildPath = (candidatePath: string, name: string): string | null => {
|
|
1142
|
+
if (path.isAbsolute(candidatePath) || candidatePath !== name) return null;
|
|
1143
|
+
const resolved = path.resolve(root, candidatePath);
|
|
1161
1144
|
const expected = path.join(canonicalRoot, name);
|
|
1162
1145
|
try {
|
|
1163
|
-
|
|
1146
|
+
const stat = fs.lstatSync(resolved);
|
|
1147
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
|
1148
|
+
return fs.realpathSync.native(resolved) === expected ? expected : null;
|
|
1164
1149
|
} catch (error) {
|
|
1165
1150
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
1166
1151
|
try {
|
|
1167
|
-
return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot;
|
|
1152
|
+
return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot ? expected : null;
|
|
1168
1153
|
} catch (parentError) {
|
|
1169
1154
|
if ((parentError as NodeJS.ErrnoException).code !== 'ENOENT') throw parentError;
|
|
1170
|
-
return
|
|
1155
|
+
return null;
|
|
1171
1156
|
}
|
|
1172
1157
|
}
|
|
1173
1158
|
};
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (!matchesControlChildPath(candidate.runtimeDir, 'runtime')) {
|
|
1159
|
+
const channelDir = resolveControlChildPath(candidate.channelDir, 'channel');
|
|
1160
|
+
const publicStatusDir = resolveControlChildPath(candidate.publicStatusDir, 'public');
|
|
1161
|
+
const processingDir = resolveControlChildPath(candidate.processingDir, 'processing');
|
|
1162
|
+
const runtimeDir = resolveControlChildPath(candidate.runtimeDir, 'runtime');
|
|
1163
|
+
if (!channelDir || !publicStatusDir || !processingDir) {
|
|
1180
1164
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
1181
1165
|
}
|
|
1166
|
+
if (!runtimeDir) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
1182
1167
|
const normalized: SandboxControlManifest = {
|
|
1183
1168
|
engine: candidate.engine,
|
|
1184
1169
|
repoRoot: candidate.repoRoot,
|
|
@@ -1196,10 +1181,10 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
1196
1181
|
token: candidate.token,
|
|
1197
1182
|
generation: candidate.generation,
|
|
1198
1183
|
controlRootId: candidate.controlRootId,
|
|
1199
|
-
channelDir
|
|
1200
|
-
publicStatusDir
|
|
1201
|
-
processingDir
|
|
1202
|
-
runtimeDir
|
|
1184
|
+
channelDir,
|
|
1185
|
+
publicStatusDir,
|
|
1186
|
+
processingDir,
|
|
1187
|
+
runtimeDir,
|
|
1203
1188
|
};
|
|
1204
1189
|
return normalized;
|
|
1205
1190
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
3
|
-
import { isAgentClientId } from '../../agent-clients/types.ts';
|
|
4
3
|
import type { ProcessIdentity } from '../../server/process-state.ts';
|
|
5
4
|
import type { SandboxAuthorityEvidenceV1 } from '../engines/authority.ts';
|
|
6
5
|
import type { SandboxTaskView } from './task-view.ts';
|
|
@@ -10,11 +9,10 @@ export const SANDBOX_CONTROL_MAX_LOGICAL_RECORDS = 1024;
|
|
|
10
9
|
export const SANDBOX_CONTROL_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
11
10
|
export const SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES = 1024 * 1024;
|
|
12
11
|
export const SANDBOX_CONTROL_RESERVATION_BYTES = SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES + 4 * 1024;
|
|
13
|
-
export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
|
|
14
12
|
export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
15
13
|
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
16
14
|
export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
|
|
17
|
-
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-create'
|
|
15
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-create'] as const;
|
|
18
16
|
export type SandboxControlTimingPolicy = Readonly<{
|
|
19
17
|
controlTickMs: number;
|
|
20
18
|
parkedBindingInitialMs: number;
|
|
@@ -55,22 +53,14 @@ export type SandboxControlExecutorGateV2 = Readonly<{
|
|
|
55
53
|
owner: SandboxControlBrokerOwner;
|
|
56
54
|
requestId: string;
|
|
57
55
|
}>;
|
|
58
|
-
type RequestBase = Readonly<{
|
|
59
|
-
version: 4; id: string; token: string; generation: string; issuedAt: number; expiresAt: number;
|
|
60
|
-
}>;
|
|
56
|
+
type RequestBase = Readonly<{ version: 5; id: string }>;
|
|
61
57
|
export type SandboxTaskCommandRequest = RequestBase & Readonly<{
|
|
62
58
|
family: 'task-lifecycle'; args: string[];
|
|
63
59
|
}>;
|
|
64
60
|
export type SandboxTaskCreateRequest = RequestBase & Readonly<{
|
|
65
61
|
family: 'task-create'; candidate: TaskCreateCandidateV1;
|
|
66
62
|
}>;
|
|
67
|
-
export type
|
|
68
|
-
family: 'agent-client';
|
|
69
|
-
agentClient: string;
|
|
70
|
-
operation: string;
|
|
71
|
-
payload: Record<string, unknown>;
|
|
72
|
-
}>;
|
|
73
|
-
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskCreateRequest | SandboxAgentClientRequest;
|
|
63
|
+
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskCreateRequest;
|
|
74
64
|
export type SandboxControlError = Readonly<{ code: string; message: string; retryable: boolean }>;
|
|
75
65
|
export type SandboxControlResultEvidence = Readonly<{
|
|
76
66
|
version: 1;
|
|
@@ -220,53 +210,28 @@ export function isSandboxControlFamily(value: string): value is SandboxControlFa
|
|
|
220
210
|
|
|
221
211
|
export function validateSandboxControlRequest(
|
|
222
212
|
value: unknown,
|
|
223
|
-
manifest: SandboxControlManifest
|
|
224
|
-
options: { now?: number } = {}
|
|
213
|
+
manifest: SandboxControlManifest
|
|
225
214
|
): SandboxControlRequest {
|
|
226
215
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
227
216
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
|
|
228
217
|
}
|
|
229
218
|
const request = value as Record<string, unknown>;
|
|
230
219
|
if (
|
|
231
|
-
request.version !==
|
|
232
|
-
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
220
|
+
request.version !== 5 || typeof request.id !== 'string'
|
|
221
|
+
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
233
222
|
|| typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
|
|
234
|
-
|| !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt)
|
|
235
223
|
) fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
236
|
-
if (request.generation !== manifest.generation) {
|
|
237
|
-
fail('SANDBOX_CONTROL_GENERATION_INVALID', 'request generation does not match the broker');
|
|
238
|
-
}
|
|
239
|
-
const issuedAt = request.issuedAt as number;
|
|
240
|
-
const expiresAt = request.expiresAt as number;
|
|
241
|
-
const now = options.now ?? Date.now();
|
|
242
|
-
if (expiresAt <= issuedAt || expiresAt - issuedAt > SANDBOX_CONTROL_ADMISSION_WINDOW_MS || issuedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
243
|
-
fail('SANDBOX_CONTROL_REQUEST_DEADLINE_INVALID', 'request admission deadline is invalid');
|
|
244
|
-
}
|
|
245
|
-
if (now > expiresAt) fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
|
|
246
224
|
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
247
225
|
fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
|
|
248
226
|
}
|
|
249
227
|
if (request.family === 'task-create') {
|
|
250
|
-
const expected = ['candidate', '
|
|
228
|
+
const expected = ['candidate', 'family', 'id', 'version'];
|
|
251
229
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
|
|
252
230
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
253
231
|
}
|
|
254
232
|
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) } as SandboxTaskCreateRequest;
|
|
255
233
|
}
|
|
256
|
-
|
|
257
|
-
const expected = ['agentClient', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'payload', 'token', 'version'];
|
|
258
|
-
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
259
|
-
|| !isAgentClientId(request.agentClient)
|
|
260
|
-
|| typeof request.operation !== 'string' || !/^[a-z][a-z0-9.-]{0,63}$/u.test(request.operation)
|
|
261
|
-
|| !request.payload || typeof request.payload !== 'object' || Array.isArray(request.payload)) {
|
|
262
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client request schema is invalid');
|
|
263
|
-
}
|
|
264
|
-
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
265
|
-
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot coordinate agent clients');
|
|
266
|
-
}
|
|
267
|
-
return request as SandboxAgentClientRequest;
|
|
268
|
-
}
|
|
269
|
-
const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
234
|
+
const expected = ['args', 'family', 'id', 'version'];
|
|
270
235
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
271
236
|
|| !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
|
|
272
237
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
@@ -284,7 +249,7 @@ export function validateSandboxControlRequest(
|
|
|
284
249
|
}
|
|
285
250
|
|
|
286
251
|
export function bindSandboxControlTask(request: SandboxControlRequest, taskId: string): string[] {
|
|
287
|
-
if (request.family === 'task-create'
|
|
252
|
+
if (request.family === 'task-create') {
|
|
288
253
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
|
|
289
254
|
}
|
|
290
255
|
if (request.args.length === 0) fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
|
|
@@ -300,7 +265,6 @@ export function controlError(error: unknown): SandboxControlError {
|
|
|
300
265
|
const retryable = new Set([
|
|
301
266
|
'SANDBOX_CONTROL_BUSY',
|
|
302
267
|
'SANDBOX_CONTROL_HANDOFF_ACTIVE',
|
|
303
|
-
'SANDBOX_CONTROL_REQUEST_EXPIRED',
|
|
304
268
|
'SANDBOX_WORKTREE_BINDING_LOST'
|
|
305
269
|
]).has(code);
|
|
306
270
|
return { code, message, retryable };
|
|
@@ -72,7 +72,6 @@ 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 { recoverAgentClientOperation, validateAgentClientOperation } from './agent-operation.ts';
|
|
76
75
|
import { validateSandboxControlIdentity } from './identity-sentinel.ts';
|
|
77
76
|
import {
|
|
78
77
|
mergeSandboxTaskView,
|
|
@@ -100,8 +99,6 @@ function appendBrokerAudit(
|
|
|
100
99
|
|
|
101
100
|
function operationKey(request: SandboxControlRequest): string | null {
|
|
102
101
|
if (request.family === 'task-create') return 'create';
|
|
103
|
-
if (request.family === 'agent-client') return request.operation;
|
|
104
|
-
if (request.family !== 'task-lifecycle') return null;
|
|
105
102
|
try {
|
|
106
103
|
const operation = parseTaskControlOperation(request.family, request.args);
|
|
107
104
|
return operation.family === 'task-lifecycle' ? operation.request.intent : null;
|
|
@@ -299,7 +296,7 @@ async function readRecoveryDomain(
|
|
|
299
296
|
if (!operation) return { domain: null };
|
|
300
297
|
const taskRef = 'args' in request ? request.args[0] ?? null : manifest.taskId;
|
|
301
298
|
const output = parseControlOutput(payloadOutput);
|
|
302
|
-
if (!taskRef && operation.family !== 'task-create'
|
|
299
|
+
if (!taskRef && operation.family !== 'task-create') {
|
|
303
300
|
return { domain: null };
|
|
304
301
|
}
|
|
305
302
|
|
|
@@ -327,14 +324,6 @@ async function readRecoveryDomain(
|
|
|
327
324
|
if (operation.family === 'task-create') {
|
|
328
325
|
return { domain: taskCreateDomainEvidence(manifest, output) };
|
|
329
326
|
}
|
|
330
|
-
if (operation.family === 'agent-client' && request.family === 'agent-client') {
|
|
331
|
-
return { domain: recoverAgentClientOperation({
|
|
332
|
-
request,
|
|
333
|
-
manifest,
|
|
334
|
-
manifestPath,
|
|
335
|
-
stdout: payloadOutput
|
|
336
|
-
}) };
|
|
337
|
-
}
|
|
338
327
|
return { domain: null };
|
|
339
328
|
}
|
|
340
329
|
|
|
@@ -568,10 +557,7 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
|
|
|
568
557
|
let terminalReconciled = false;
|
|
569
558
|
try {
|
|
570
559
|
const rawRequest = JSON.parse(fs.readFileSync(path.join(processingDirectory, 'request.json'), 'utf8')) as Record<string, unknown>;
|
|
571
|
-
request = validateSandboxControlRequest(rawRequest, manifest
|
|
572
|
-
now: typeof rawRequest.issuedAt === 'number' ? rawRequest.issuedAt : undefined
|
|
573
|
-
});
|
|
574
|
-
if (request.family === 'agent-client') validateAgentClientOperation(request, manifest);
|
|
560
|
+
request = validateSandboxControlRequest(rawRequest, manifest);
|
|
575
561
|
} catch {
|
|
576
562
|
// Missing or malformed request evidence remains fail-closed below.
|
|
577
563
|
}
|
|
@@ -1006,7 +992,6 @@ export async function serveSandboxControl(
|
|
|
1006
992
|
if (reasonCode) throw new Error(reasonCode);
|
|
1007
993
|
if (active) throw new Error('SANDBOX_CONTROL_BUSY');
|
|
1008
994
|
const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
|
|
1009
|
-
if (request.family === 'agent-client') validateAgentClientOperation(request, manifest);
|
|
1010
995
|
validatedRequest = request;
|
|
1011
996
|
criticalRequestPhase(manifest, request, 'validated', 'in-progress');
|
|
1012
997
|
appendBrokerAudit(manifest, 'request-validated', {
|
|
@@ -12,14 +12,13 @@ export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
|
|
|
12
12
|
const TASK_MARKER_KEYS = [
|
|
13
13
|
'AGENT_INFRA_SANDBOX',
|
|
14
14
|
'AGENT_INFRA_TASK_ID',
|
|
15
|
-
'AGENT_INFRA_CONTROL_TOKEN',
|
|
16
15
|
'AGENT_INFRA_CONTROL_GENERATION',
|
|
17
16
|
'AGENT_INFRA_CONTROL_DIR',
|
|
17
|
+
'AGENT_INFRA_CONTROL_ROOT_DIR',
|
|
18
18
|
'AGENT_INFRA_CONTROL_STATUS_DIR',
|
|
19
19
|
'AGENT_INFRA_RUNTIME_DIR'
|
|
20
20
|
] as const;
|
|
21
21
|
const TASK_CONTROL_MARKER_KEYS = [
|
|
22
|
-
'AGENT_INFRA_CONTROL_TOKEN',
|
|
23
22
|
'AGENT_INFRA_CONTROL_GENERATION',
|
|
24
23
|
'AGENT_INFRA_CONTROL_DIR',
|
|
25
24
|
'AGENT_INFRA_CONTROL_STATUS_DIR'
|
|
@@ -88,7 +87,6 @@ export function resolveSandboxControlTransport(
|
|
|
88
87
|
): SandboxControlTransportDecision {
|
|
89
88
|
const markerState = sandboxTaskMarkerState(env);
|
|
90
89
|
const hasAnyMarker = markerState !== 'none'
|
|
91
|
-
|| Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
|
|
92
90
|
|| Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
|
|
93
91
|
const fixedStatusDir = options.statusMountPath ?? SANDBOX_CONTROL_STATUS_MOUNT;
|
|
94
92
|
const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
|
|
@@ -109,6 +107,10 @@ export function resolveSandboxControlTransport(
|
|
|
109
107
|
if (!hasCompleteConfig) {
|
|
110
108
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
111
109
|
}
|
|
110
|
+
if (markerState === 'task-bound'
|
|
111
|
+
&& (!env.AGENT_INFRA_CONTROL_ROOT_DIR || !path.isAbsolute(env.AGENT_INFRA_CONTROL_ROOT_DIR))) {
|
|
112
|
+
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
113
|
+
}
|
|
112
114
|
const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
113
115
|
const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
|
|
114
116
|
? nativeDirectoryProbe(configuredStatusDir) : 'absent';
|
|
@@ -119,7 +121,7 @@ export function resolveSandboxControlTransport(
|
|
|
119
121
|
? configuredStatusProbe === 'present' ? configuredStatusDir : null
|
|
120
122
|
: fixedStatusMounted ? fixedStatusDir : null;
|
|
121
123
|
const statusMounted = statusDir !== null;
|
|
122
|
-
if (env.AGENT_INFRA_EXECUTOR_MANIFEST
|
|
124
|
+
if (env.AGENT_INFRA_EXECUTOR_MANIFEST) {
|
|
123
125
|
return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
|
|
124
126
|
}
|
|
125
127
|
const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
|
|
@@ -134,9 +136,6 @@ export function resolveSandboxControlTransport(
|
|
|
134
136
|
? 'SANDBOX_CONTROL_IDENTITY_MISSING'
|
|
135
137
|
: 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
|
|
136
138
|
}
|
|
137
|
-
if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
|
|
138
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
|
|
139
|
-
}
|
|
140
139
|
if (sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
|
|
141
140
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
|
|
142
141
|
}
|
|
@@ -144,7 +143,7 @@ export function resolveSandboxControlTransport(
|
|
|
144
143
|
&& (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
|
|
145
144
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
146
145
|
}
|
|
147
|
-
if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
|
|
146
|
+
if (sentinel.mode === 'branch-only' && (taskBound || runtime || Boolean(env.AGENT_INFRA_CONTROL_ROOT_DIR))) {
|
|
148
147
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
149
148
|
}
|
|
150
149
|
return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
|