@fitlab-ai/agent-infra 0.11.12 → 0.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +29 -20
- package/dist/lib/internal/task-lifecycle.js +4 -2
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +11 -6
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +15 -110
- package/dist/lib/sandbox/control/executor.js +9 -59
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +75 -9
- package/dist/lib/sandbox/control/protocol.js +11 -50
- package/dist/lib/sandbox/control/server.js +203 -253
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -52
- package/dist/lib/sandbox/recovery.js +30 -12
- package/dist/lib/sandbox/removal.js +37 -3
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +89 -6
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +41 -17
- package/dist/lib/task/control-recovery.js +2 -4
- package/dist/lib/task/finalization.js +27 -2
- package/dist/lib/task/lifecycle.js +294 -63
- package/dist/lib/task/orchestration.js +26 -72
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +4 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +21 -27
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +33 -19
- package/lib/internal/task-lifecycle.ts +4 -2
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/github-client.ts +34 -3
- package/lib/platform/release-notes.ts +2 -2
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +11 -5
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +17 -138
- package/lib/sandbox/control/executor.ts +8 -65
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +75 -10
- package/lib/sandbox/control/protocol.ts +12 -62
- package/lib/sandbox/control/server.ts +11 -61
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +93 -68
- package/lib/sandbox/recovery.ts +37 -19
- package/lib/sandbox/removal.ts +36 -5
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +81 -7
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +41 -26
- package/lib/task/control-recovery.ts +3 -5
- package/lib/task/finalization.ts +29 -2
- package/lib/task/lifecycle.ts +282 -57
- package/lib/task/orchestration.ts +30 -68
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +3 -9
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/sandbox/control/agent-operation.ts +0 -49
|
@@ -19,30 +19,92 @@ function manifestDigest(manifest) {
|
|
|
19
19
|
generation: manifest.generation, controlRootId: manifest.controlRootId
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
function readManagedManifest(manifestPath) {
|
|
23
|
+
try {
|
|
24
|
+
return readSandboxControlManifest(manifestPath);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
27
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function branchOnlyManifests(parentPath) {
|
|
31
|
+
const manifests = [];
|
|
32
|
+
for (const identity of fs.readdirSync(parentPath, { withFileTypes: true })) {
|
|
33
|
+
if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name))
|
|
31
34
|
continue;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
manifests.push(readManagedManifest(path.join(parentPath, identity.name, 'manifest.json')));
|
|
36
|
+
}
|
|
37
|
+
return manifests;
|
|
38
|
+
}
|
|
39
|
+
function taskBoundManifests(params) {
|
|
40
|
+
assertDirectory(params.tasksRoot);
|
|
41
|
+
const canonicalManagedRoot = fs.realpathSync.native(path.dirname(params.tasksRoot));
|
|
42
|
+
const canonicalTasksRoot = fs.realpathSync.native(params.tasksRoot);
|
|
43
|
+
if (path.dirname(canonicalTasksRoot) !== canonicalManagedRoot)
|
|
44
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
45
|
+
const canonicalRepoRoot = fs.realpathSync.native(params.repoRoot);
|
|
46
|
+
const manifests = [];
|
|
47
|
+
for (const task of fs.readdirSync(params.tasksRoot, { withFileTypes: true })) {
|
|
48
|
+
if (!/^TASK-\d{8}-\d{6}$/u.test(task.name))
|
|
49
|
+
continue;
|
|
50
|
+
if (!task.isDirectory() || task.isSymbolicLink())
|
|
51
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
52
|
+
const taskRoot = path.join(params.tasksRoot, task.name);
|
|
53
|
+
assertNoSymlinkPath(params.controlBase, taskRoot);
|
|
54
|
+
const canonicalTaskRoot = fs.realpathSync.native(taskRoot);
|
|
55
|
+
if (path.dirname(canonicalTaskRoot) !== canonicalTasksRoot)
|
|
56
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
57
|
+
const manifestPath = path.join(taskRoot, 'manifest.json');
|
|
58
|
+
if (!fs.existsSync(manifestPath))
|
|
59
|
+
continue;
|
|
60
|
+
const manifest = readManagedManifest(manifestPath);
|
|
61
|
+
if (manifest.mode !== 'task-bound' || manifest.taskId !== task.name || manifest.project !== params.project
|
|
62
|
+
|| fs.realpathSync.native(manifest.repoRoot) !== canonicalRepoRoot
|
|
63
|
+
|| path.dirname(fs.realpathSync.native(manifestPath)) !== canonicalTaskRoot) {
|
|
64
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
37
65
|
}
|
|
66
|
+
manifests.push(manifest);
|
|
67
|
+
}
|
|
68
|
+
return manifests;
|
|
69
|
+
}
|
|
70
|
+
function managedManifestCandidates(params) {
|
|
71
|
+
const managedRoot = params.managedRoot;
|
|
72
|
+
const { controlBase, project, repoRoot } = params;
|
|
73
|
+
if (controlBase && project && path.resolve(managedRoot) !== path.resolve(controlBase, project)) {
|
|
74
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
38
75
|
}
|
|
39
|
-
|
|
76
|
+
if (!fs.existsSync(managedRoot))
|
|
77
|
+
return [];
|
|
78
|
+
assertDirectory(managedRoot);
|
|
79
|
+
return fs.readdirSync(managedRoot, { withFileTypes: true })
|
|
80
|
+
.flatMap((parent) => {
|
|
81
|
+
const parentPath = path.join(managedRoot, parent.name);
|
|
82
|
+
if (parent.name === 'tasks' && controlBase && project && repoRoot) {
|
|
83
|
+
return taskBoundManifests({ tasksRoot: parentPath, controlBase, project, repoRoot });
|
|
84
|
+
}
|
|
85
|
+
if (!parent.isDirectory() || parent.isSymbolicLink())
|
|
86
|
+
return [];
|
|
87
|
+
return branchOnlyManifests(parentPath);
|
|
88
|
+
});
|
|
40
89
|
}
|
|
41
90
|
function assertDirectory(directory) {
|
|
42
91
|
const stat = fs.lstatSync(directory);
|
|
43
92
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
44
93
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
45
94
|
}
|
|
95
|
+
function assertNoSymlinkPath(root, target) {
|
|
96
|
+
const relative = path.relative(root, target);
|
|
97
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
98
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
99
|
+
let current = path.resolve(root);
|
|
100
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
|
|
101
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
102
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
103
|
+
current = path.join(current, segment);
|
|
104
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
|
|
105
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
46
108
|
function readAudit(manifest) {
|
|
47
109
|
const root = path.dirname(path.resolve(manifest.publicStatusDir));
|
|
48
110
|
const records = [];
|
|
@@ -80,12 +142,16 @@ function auditProof(manifest, requestId) {
|
|
|
80
142
|
return [];
|
|
81
143
|
return related;
|
|
82
144
|
}
|
|
83
|
-
function publishedEvidence(records,
|
|
145
|
+
function publishedEvidence(records, manifest) {
|
|
84
146
|
const publications = records.filter((record) => record.event === 'executor-result-published');
|
|
85
147
|
let selected = null;
|
|
86
148
|
let selectedSummary = null;
|
|
149
|
+
const digest = manifestDigest(manifest);
|
|
87
150
|
for (const record of publications) {
|
|
88
|
-
if (record.version !== 2 || record.
|
|
151
|
+
if (record.version !== 2 || record.generation !== manifest.generation || record.identityDigest !== digest
|
|
152
|
+
|| typeof record.requestFamily !== 'string'
|
|
153
|
+
|| !['task-lifecycle', 'task-create'].includes(record.requestFamily)
|
|
154
|
+
|| (record.sandboxTaskId !== null && typeof record.sandboxTaskId !== 'string')
|
|
89
155
|
|| !Number.isSafeInteger(record.exitCode)
|
|
90
156
|
|| !Number.isSafeInteger(record.outputBytes) || !Number.isSafeInteger(record.errorBytes)
|
|
91
157
|
|| typeof record.outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.outputDigest)
|
|
@@ -93,7 +159,7 @@ function publishedEvidence(records, generation) {
|
|
|
93
159
|
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
94
160
|
}
|
|
95
161
|
const summary = JSON.stringify([
|
|
96
|
-
record.requestFamily
|
|
162
|
+
record.requestFamily, record.sandboxTaskId, record.generation, record.identityDigest, record.exitCode,
|
|
97
163
|
record.outputBytes, record.errorBytes, record.outputDigest, record.errorDigest
|
|
98
164
|
]);
|
|
99
165
|
if (selectedSummary !== null && summary !== selectedSummary) {
|
|
@@ -102,6 +168,9 @@ function publishedEvidence(records, generation) {
|
|
|
102
168
|
selected ??= record;
|
|
103
169
|
selectedSummary ??= summary;
|
|
104
170
|
}
|
|
171
|
+
if (selected && selected.sandboxTaskId !== manifest.taskId) {
|
|
172
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
173
|
+
}
|
|
105
174
|
return selected;
|
|
106
175
|
}
|
|
107
176
|
function verifiedResponse(manifest, requestId, records) {
|
|
@@ -131,7 +200,7 @@ function verifiedResponse(manifest, requestId, records) {
|
|
|
131
200
|
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
|
|
132
201
|
return unknown(requestId);
|
|
133
202
|
const evidencePath = path.join(manifest.processingDir, requestId, 'result.json');
|
|
134
|
-
const published = publishedEvidence(records, manifest
|
|
203
|
+
const published = publishedEvidence(records, manifest);
|
|
135
204
|
if (!published)
|
|
136
205
|
return unknown(requestId);
|
|
137
206
|
let evidence = null;
|
|
@@ -187,15 +256,8 @@ export function recoverSandboxControlFromHost(requestId, params) {
|
|
|
187
256
|
let candidates = [];
|
|
188
257
|
while (Date.now() < deadline) {
|
|
189
258
|
candidates = [];
|
|
190
|
-
for (const
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
manifest = readSandboxControlManifest(manifestPath);
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
197
|
-
}
|
|
198
|
-
if (manifest.project !== path.basename(path.resolve(params.managedRoot)) || !manifest.generation) {
|
|
259
|
+
for (const manifest of managedManifestCandidates(params)) {
|
|
260
|
+
if (manifest.project !== (params.project ?? path.basename(path.resolve(params.managedRoot))) || !manifest.generation) {
|
|
199
261
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
200
262
|
}
|
|
201
263
|
const records = auditProof(manifest, requestId);
|
|
@@ -9,6 +9,7 @@ import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
|
|
|
9
9
|
import { inspectSandboxControlContainer } from './container-identity.js';
|
|
10
10
|
import { acquireSandboxResourceLock, resolveSandboxLockNamespace } from './native-file-lock.js';
|
|
11
11
|
import { isSandboxAuthorityEvidence } from '../engines/authority.js';
|
|
12
|
+
import { validateSandboxControlIdentity } from './identity-sentinel.js';
|
|
12
13
|
import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
|
|
13
14
|
const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
|
|
14
15
|
const QUIESCING_FILE = 'quiescing.json';
|
|
@@ -500,6 +501,45 @@ function markSandboxControlRootQuiescing(root) {
|
|
|
500
501
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
501
502
|
}
|
|
502
503
|
}
|
|
504
|
+
function readTaskCompletionManifest(root, binding) {
|
|
505
|
+
const manifest = readSandboxControlManifest(path.join(root, 'manifest.json'));
|
|
506
|
+
const identity = validateSandboxControlIdentity({
|
|
507
|
+
publicStatusDir: binding.statusDir,
|
|
508
|
+
root,
|
|
509
|
+
mode: manifest.mode,
|
|
510
|
+
taskId: manifest.taskId,
|
|
511
|
+
generation: manifest.generation,
|
|
512
|
+
controlRootId: manifest.controlRootId
|
|
513
|
+
});
|
|
514
|
+
if (identity.state !== 'valid' || manifest.mode !== 'task-bound'
|
|
515
|
+
|| manifest.taskId !== binding.taskId || manifest.generation !== binding.generation
|
|
516
|
+
|| manifest.controlRootId !== binding.controlRootId) {
|
|
517
|
+
throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
|
|
518
|
+
}
|
|
519
|
+
return manifest;
|
|
520
|
+
}
|
|
521
|
+
export async function quiesceSandboxControlForTaskCompletion(root, binding, timeoutMs = DEFAULT_QUIESCE_TIMEOUT_MS) {
|
|
522
|
+
const resolvedRoot = path.resolve(root);
|
|
523
|
+
const manifest = readTaskCompletionManifest(resolvedRoot, binding);
|
|
524
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
525
|
+
const deadline = Date.now() + timeoutMs;
|
|
526
|
+
const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
527
|
+
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
528
|
+
while (true) {
|
|
529
|
+
const startup = readStartupOwner(startupPath);
|
|
530
|
+
if (fs.existsSync(startupPath) && !startup)
|
|
531
|
+
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
532
|
+
const broker = readBrokerOwner(brokerPath);
|
|
533
|
+
if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
534
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
535
|
+
}
|
|
536
|
+
if (!startup && !broker)
|
|
537
|
+
return;
|
|
538
|
+
if (Date.now() >= deadline)
|
|
539
|
+
throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
|
|
540
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
503
543
|
function recordSandboxRemovalPending(root, manifest, phase, journal) {
|
|
504
544
|
const pendingPath = path.join(root, 'removal-pending.json');
|
|
505
545
|
const temporary = `${pendingPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -934,14 +974,40 @@ function readSandboxControlManifestValue(manifestPath) {
|
|
|
934
974
|
|| !isSandboxAuthorityEvidence(candidate.authorityEvidence))
|
|
935
975
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
936
976
|
const root = path.dirname(path.resolve(manifestPath));
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
977
|
+
const canonicalRoot = fs.realpathSync.native(root);
|
|
978
|
+
const resolveControlChildPath = (candidatePath, name) => {
|
|
979
|
+
if (path.isAbsolute(candidatePath) || candidatePath !== name)
|
|
980
|
+
return null;
|
|
981
|
+
const resolved = path.resolve(root, candidatePath);
|
|
982
|
+
const expected = path.join(canonicalRoot, name);
|
|
983
|
+
try {
|
|
984
|
+
const stat = fs.lstatSync(resolved);
|
|
985
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
986
|
+
return null;
|
|
987
|
+
return fs.realpathSync.native(resolved) === expected ? expected : null;
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
if (error.code !== 'ENOENT')
|
|
991
|
+
throw error;
|
|
992
|
+
try {
|
|
993
|
+
return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot ? expected : null;
|
|
994
|
+
}
|
|
995
|
+
catch (parentError) {
|
|
996
|
+
if (parentError.code !== 'ENOENT')
|
|
997
|
+
throw parentError;
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const channelDir = resolveControlChildPath(candidate.channelDir, 'channel');
|
|
1003
|
+
const publicStatusDir = resolveControlChildPath(candidate.publicStatusDir, 'public');
|
|
1004
|
+
const processingDir = resolveControlChildPath(candidate.processingDir, 'processing');
|
|
1005
|
+
const runtimeDir = resolveControlChildPath(candidate.runtimeDir, 'runtime');
|
|
1006
|
+
if (!channelDir || !publicStatusDir || !processingDir) {
|
|
940
1007
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
941
1008
|
}
|
|
942
|
-
if (
|
|
1009
|
+
if (!runtimeDir)
|
|
943
1010
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
944
|
-
}
|
|
945
1011
|
const normalized = {
|
|
946
1012
|
engine: candidate.engine,
|
|
947
1013
|
repoRoot: candidate.repoRoot,
|
|
@@ -959,10 +1025,10 @@ function readSandboxControlManifestValue(manifestPath) {
|
|
|
959
1025
|
token: candidate.token,
|
|
960
1026
|
generation: candidate.generation,
|
|
961
1027
|
controlRootId: candidate.controlRootId,
|
|
962
|
-
channelDir
|
|
963
|
-
publicStatusDir
|
|
964
|
-
processingDir
|
|
965
|
-
runtimeDir
|
|
1028
|
+
channelDir,
|
|
1029
|
+
publicStatusDir,
|
|
1030
|
+
processingDir,
|
|
1031
|
+
runtimeDir,
|
|
966
1032
|
};
|
|
967
1033
|
return normalized;
|
|
968
1034
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { validateTaskCreateCandidate } from '../../task/create.js';
|
|
3
|
-
import { normalizeAgentToken } from '../../agent-clients/tokens.js';
|
|
4
|
-
import { isAgentClientId } from '../../agent-clients/types.js';
|
|
5
3
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
6
4
|
export const SANDBOX_CONTROL_MAX_LOGICAL_RECORDS = 1024;
|
|
7
5
|
export const SANDBOX_CONTROL_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
8
6
|
export const SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES = 1024 * 1024;
|
|
9
7
|
export const SANDBOX_CONTROL_RESERVATION_BYTES = SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES + 4 * 1024;
|
|
10
|
-
export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
|
|
11
8
|
export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
12
9
|
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
13
10
|
export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
|
|
14
|
-
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-
|
|
11
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-create'];
|
|
15
12
|
export const DEFAULT_SANDBOX_CONTROL_TIMING = Object.freeze({
|
|
16
13
|
controlTickMs: 250,
|
|
17
14
|
parkedBindingInitialMs: 1_000,
|
|
@@ -107,75 +104,40 @@ export function parseSandboxControlReservation(value) {
|
|
|
107
104
|
export function isSandboxControlFamily(value) {
|
|
108
105
|
return SANDBOX_CONTROL_FAMILIES.includes(value);
|
|
109
106
|
}
|
|
110
|
-
export function validateSandboxControlRequest(value, manifest
|
|
107
|
+
export function validateSandboxControlRequest(value, manifest) {
|
|
111
108
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
112
109
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
|
|
113
110
|
}
|
|
114
111
|
const request = value;
|
|
115
|
-
if (request.version !==
|
|
116
|
-
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
117
|
-
|| typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
|
|
118
|
-
|| !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
|
|
112
|
+
if (request.version !== 5 || typeof request.id !== 'string'
|
|
113
|
+
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
114
|
+
|| typeof request.family !== 'string' || !isSandboxControlFamily(request.family))
|
|
119
115
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
120
|
-
if (request.generation !== manifest.generation) {
|
|
121
|
-
fail('SANDBOX_CONTROL_GENERATION_INVALID', 'request generation does not match the broker');
|
|
122
|
-
}
|
|
123
|
-
const issuedAt = request.issuedAt;
|
|
124
|
-
const expiresAt = request.expiresAt;
|
|
125
|
-
const now = options.now ?? Date.now();
|
|
126
|
-
if (expiresAt <= issuedAt || expiresAt - issuedAt > SANDBOX_CONTROL_ADMISSION_WINDOW_MS || issuedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
127
|
-
fail('SANDBOX_CONTROL_REQUEST_DEADLINE_INVALID', 'request admission deadline is invalid');
|
|
128
|
-
}
|
|
129
|
-
if (now > expiresAt)
|
|
130
|
-
fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
|
|
131
116
|
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
132
117
|
fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
|
|
133
118
|
}
|
|
134
119
|
if (request.family === 'task-create') {
|
|
135
|
-
const expected = ['candidate', '
|
|
120
|
+
const expected = ['candidate', 'family', 'id', 'version'];
|
|
136
121
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
|
|
137
122
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
138
123
|
}
|
|
139
124
|
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
|
|
140
125
|
}
|
|
141
|
-
|
|
142
|
-
const expected = ['agentClient', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'payload', 'token', 'version'];
|
|
143
|
-
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
144
|
-
|| !isAgentClientId(request.agentClient)
|
|
145
|
-
|| typeof request.operation !== 'string' || !/^[a-z][a-z0-9.-]{0,63}$/u.test(request.operation)
|
|
146
|
-
|| !request.payload || typeof request.payload !== 'object' || Array.isArray(request.payload)) {
|
|
147
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client request schema is invalid');
|
|
148
|
-
}
|
|
149
|
-
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
150
|
-
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot coordinate agent clients');
|
|
151
|
-
}
|
|
152
|
-
return request;
|
|
153
|
-
}
|
|
154
|
-
if (request.family === 'task-finalization') {
|
|
155
|
-
const expected = ['agent', 'args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
156
|
-
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
157
|
-
|| request.operation !== 'complete'
|
|
158
|
-
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
159
|
-
|| typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent) {
|
|
160
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
|
|
161
|
-
}
|
|
162
|
-
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
163
|
-
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks');
|
|
164
|
-
}
|
|
165
|
-
return request;
|
|
166
|
-
}
|
|
167
|
-
const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
126
|
+
const expected = ['args', 'family', 'id', 'version'];
|
|
168
127
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
169
128
|
|| !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
|
|
170
129
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
171
130
|
}
|
|
131
|
+
if (request.args[1] === 'complete') {
|
|
132
|
+
fail('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization');
|
|
133
|
+
}
|
|
172
134
|
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
173
135
|
fail('SANDBOX_CONTROL_BRANCH_ONLY', "branch-only sandboxes cannot coordinate tasks; return to the host and run 'ai sandbox start --recreate <task-ref-or-correct-branch>'");
|
|
174
136
|
}
|
|
175
137
|
return request;
|
|
176
138
|
}
|
|
177
139
|
export function bindSandboxControlTask(request, taskId) {
|
|
178
|
-
if (request.family === 'task-create'
|
|
140
|
+
if (request.family === 'task-create') {
|
|
179
141
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
|
|
180
142
|
}
|
|
181
143
|
if (request.args.length === 0)
|
|
@@ -191,7 +153,6 @@ export function controlError(error) {
|
|
|
191
153
|
const retryable = new Set([
|
|
192
154
|
'SANDBOX_CONTROL_BUSY',
|
|
193
155
|
'SANDBOX_CONTROL_HANDOFF_ACTIVE',
|
|
194
|
-
'SANDBOX_CONTROL_REQUEST_EXPIRED',
|
|
195
156
|
'SANDBOX_WORKTREE_BINDING_LOST'
|
|
196
157
|
]).has(code);
|
|
197
158
|
return { code, message, retryable };
|