@fitlab-ai/agent-infra 0.11.12 → 0.11.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +26 -19
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/sandbox/commands/create.js +6 -2
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/control/client.js +4 -64
- package/dist/lib/sandbox/control/executor.js +4 -50
- package/dist/lib/sandbox/control/lifecycle.js +66 -4
- package/dist/lib/sandbox/control/protocol.js +5 -16
- package/dist/lib/sandbox/control/server.js +203 -234
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +28 -9
- package/dist/lib/sandbox/removal.js +19 -0
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +48 -2
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +2 -0
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +14 -0
- package/dist/lib/task/lifecycle.js +203 -31
- 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/adapters/codex-lifecycle/store.ts +20 -23
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +30 -18
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/platform/github-client.ts +34 -3
- package/lib/sandbox/commands/create.ts +6 -1
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/control/client.ts +5 -78
- package/lib/sandbox/control/executor.ts +2 -53
- package/lib/sandbox/control/lifecycle.ts +67 -4
- package/lib/sandbox/control/protocol.ts +6 -20
- package/lib/sandbox/control/server.ts +9 -44
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +35 -16
- package/lib/sandbox/removal.ts +21 -2
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +41 -2
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +1 -0
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +14 -0
- package/lib/task/lifecycle.ts +180 -27
- 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/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- 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/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 +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
|
@@ -215,7 +215,7 @@ function createCodexLifecycleStore(options) {
|
|
|
215
215
|
throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
216
216
|
return matches[0];
|
|
217
217
|
}
|
|
218
|
-
function
|
|
218
|
+
function consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, taskLockHeld) {
|
|
219
219
|
if (!consumer.trim())
|
|
220
220
|
throw new Error('Codex lifecycle consumer is required');
|
|
221
221
|
const current = read(childThreadId);
|
|
@@ -223,8 +223,11 @@ function createCodexLifecycleStore(options) {
|
|
|
223
223
|
if (!binding)
|
|
224
224
|
throw new Error('Codex lifecycle task receipt binding is missing');
|
|
225
225
|
const spawn = current.state.spawn;
|
|
226
|
-
if (!spawn)
|
|
227
|
-
throw new Error(
|
|
226
|
+
if (!spawn) {
|
|
227
|
+
throw new Error(taskLockHeld
|
|
228
|
+
? 'Codex lifecycle task receipt binding is missing'
|
|
229
|
+
: 'Codex lifecycle spawn evidence is missing');
|
|
230
|
+
}
|
|
228
231
|
const key = recordKey(spawn.sessionId, spawn.turnId, spawn.toolUseId);
|
|
229
232
|
return update(binding, key, (latest) => {
|
|
230
233
|
if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
|
|
@@ -241,30 +244,13 @@ function createCodexLifecycleStore(options) {
|
|
|
241
244
|
if (latest.consumer)
|
|
242
245
|
return latest;
|
|
243
246
|
return Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
|
|
244
|
-
});
|
|
247
|
+
}, taskLockHeld);
|
|
248
|
+
}
|
|
249
|
+
function consume(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
|
|
250
|
+
return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, false);
|
|
245
251
|
}
|
|
246
252
|
function consumeWithinTaskLock(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
|
|
247
|
-
|
|
248
|
-
throw new Error('Codex lifecycle consumer is required');
|
|
249
|
-
const current = read(childThreadId);
|
|
250
|
-
const binding = expectedTaskBinding ?? current.taskBinding;
|
|
251
|
-
if (!binding || !current.state.spawn)
|
|
252
|
-
throw new Error('Codex lifecycle task receipt binding is missing');
|
|
253
|
-
const key = recordKey(current.state.spawn.sessionId, current.state.spawn.turnId, current.state.spawn.toolUseId);
|
|
254
|
-
return update(binding, key, (latest) => {
|
|
255
|
-
if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
|
|
256
|
-
throw new Error('Codex lifecycle task binding does not match the expected receipt');
|
|
257
|
-
}
|
|
258
|
-
if (latest.consumer && latest.consumer !== consumer) {
|
|
259
|
-
throw new Error(`Codex lifecycle evidence was already consumed by '${latest.consumer}'`);
|
|
260
|
-
}
|
|
261
|
-
if (latest.state.status !== 'stop-ready')
|
|
262
|
-
throw new Error('Codex lifecycle evidence is not stop-ready');
|
|
263
|
-
if (expectedHookDefinitionHash && latest.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash) {
|
|
264
|
-
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
265
|
-
}
|
|
266
|
-
return latest.consumer ? latest : Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
|
|
267
|
-
}, true);
|
|
253
|
+
return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, true);
|
|
268
254
|
}
|
|
269
255
|
function expireBefore(cutoff) {
|
|
270
256
|
let changed = 0;
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
import { recoverSandboxControl, requestSandboxControl,
|
|
2
|
-
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
|
|
1
|
+
import { recoverSandboxControl, requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.js';
|
|
3
2
|
import { serveSandboxControl } from '../sandbox/control/server.js';
|
|
4
3
|
import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
|
|
5
4
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
6
5
|
import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.js';
|
|
7
|
-
import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
|
|
8
|
-
function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null, requestId = null) {
|
|
9
|
-
process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
|
|
10
|
-
}
|
|
11
|
-
function finalizationErrorStatus(error) {
|
|
12
|
-
if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
|
|
13
|
-
return 'unknown';
|
|
14
|
-
return error.retryable ? 'blocked' : 'failed';
|
|
15
|
-
}
|
|
16
6
|
function writeClientError(error) {
|
|
17
7
|
process.stderr.write(`${error.detail.message}\n`);
|
|
18
8
|
if (error.requestId)
|
|
@@ -31,72 +21,6 @@ function recoveredExitCode(response) {
|
|
|
31
21
|
}
|
|
32
22
|
return response.exitCode ?? 1;
|
|
33
23
|
}
|
|
34
|
-
function finalizationResponseOutput(response) {
|
|
35
|
-
try {
|
|
36
|
-
const value = JSON.parse(response.stdout);
|
|
37
|
-
if (value.version === 2
|
|
38
|
-
&& value.status
|
|
39
|
-
&& ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
|
|
40
|
-
&& (typeof value.changed === 'boolean' || value.changed === null)) {
|
|
41
|
-
return serializeTaskFinalizationEnvelope({
|
|
42
|
-
status: value.status,
|
|
43
|
-
changed: value.changed,
|
|
44
|
-
accepted: true,
|
|
45
|
-
requestId: response.id,
|
|
46
|
-
result: value.result ?? null,
|
|
47
|
-
error: value.error ?? null
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// Preserve non-envelope output for diagnostics from the executor.
|
|
53
|
-
}
|
|
54
|
-
return response.stdout;
|
|
55
|
-
}
|
|
56
|
-
function sandboxFinalizationClient(args) {
|
|
57
|
-
if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
|
|
58
|
-
const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
|
|
59
|
-
writeFinalizationEnvelope('failed', false, error);
|
|
60
|
-
process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
|
|
61
|
-
process.exitCode = 1;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const agent = normalizeAgentToken(args[3]);
|
|
65
|
-
if (!agent) {
|
|
66
|
-
const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
|
|
67
|
-
writeFinalizationEnvelope('failed', false, error);
|
|
68
|
-
process.exitCode = 1;
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
let response;
|
|
72
|
-
try {
|
|
73
|
-
response = requestSandboxTaskFinalization({ agent });
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
if (!(error instanceof SandboxControlClientError))
|
|
77
|
-
throw error;
|
|
78
|
-
const status = finalizationErrorStatus(error.detail);
|
|
79
|
-
writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
|
|
80
|
-
writeClientError(error);
|
|
81
|
-
process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (response.phase === 'rejected') {
|
|
85
|
-
const error = response.error ?? {
|
|
86
|
-
code: 'SANDBOX_CONTROL_REJECTED',
|
|
87
|
-
message: 'sandbox control rejected the finalization request',
|
|
88
|
-
retryable: false
|
|
89
|
-
};
|
|
90
|
-
const status = finalizationErrorStatus(error);
|
|
91
|
-
writeFinalizationEnvelope(status, error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error, status === 'unknown' ? null : false, null, response.id);
|
|
92
|
-
process.stderr.write(`${error.message}\n`);
|
|
93
|
-
process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
process.stdout.write(finalizationResponseOutput(response));
|
|
97
|
-
process.stderr.write(response.stderr);
|
|
98
|
-
process.exitCode = response.exitCode ?? 1;
|
|
99
|
-
}
|
|
100
24
|
async function sandboxControl(args) {
|
|
101
25
|
if (!ensureInternalHandlerRoute('sandbox-control', args))
|
|
102
26
|
return;
|
|
@@ -152,10 +76,6 @@ async function sandboxControl(args) {
|
|
|
152
76
|
}
|
|
153
77
|
if (internalHandlerRoute('sandbox-control', 'client', operation ?? '')) {
|
|
154
78
|
const [family = '', ...commandArgs] = rest;
|
|
155
|
-
if (family === 'task-finalization') {
|
|
156
|
-
sandboxFinalizationClient(commandArgs);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
79
|
let response;
|
|
160
80
|
try {
|
|
161
81
|
response = requestSandboxControl({ family, args: commandArgs });
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../task/control-authority.js';
|
|
2
3
|
import { applyTaskFinalization } from '../task/finalization.js';
|
|
3
4
|
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
5
|
import { resolveSandboxControlTransport } from './task-operation-registry.js';
|
|
5
|
-
import {
|
|
6
|
+
import { quiesceSandboxControlForTaskCompletion } from '../sandbox/control/lifecycle.js';
|
|
7
|
+
import { resolveTaskRuntimeRoot } from '../task/runtime-paths.js';
|
|
6
8
|
import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.js';
|
|
9
|
+
import { verifyTaskEvent } from '../task/verification.js';
|
|
7
10
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
8
11
|
const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
|
|
9
12
|
function envelope(status, changed, accepted, result, error, requestId = null) {
|
|
@@ -64,26 +67,30 @@ async function taskFinalization(args = []) {
|
|
|
64
67
|
};
|
|
65
68
|
const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
|
|
66
69
|
if (transport.kind === 'sandbox-local') {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const clientError = error instanceof SandboxControlClientError ? error : null;
|
|
76
|
-
const detail = clientError
|
|
77
|
-
? clientError.detail
|
|
78
|
-
: { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
|
|
79
|
-
const status = detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
|
|
80
|
-
? 'unknown'
|
|
81
|
-
: detail.retryable ? 'blocked' : 'failed';
|
|
82
|
-
const changed = status === 'unknown' ? null : false;
|
|
83
|
-
process.stdout.write(envelope(status, changed, clientError?.accepted ?? false, null, detail, clientError?.requestId ?? null));
|
|
84
|
-
process.exitCode = exitCode(status);
|
|
70
|
+
const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
71
|
+
const taskId = process.env.AGENT_INFRA_TASK_ID;
|
|
72
|
+
const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
73
|
+
const controlRootId = process.env.AGENT_INFRA_CONTROL_ROOT_ID;
|
|
74
|
+
if (!statusDir || !taskId || !generation || !controlRootId || taskId !== resolved.taskId) {
|
|
75
|
+
const detail = { code: 'TASK_CONTROL_TRANSPORT_INVALID', message: 'sandbox task control identity is incomplete', retryable: false };
|
|
76
|
+
process.stdout.write(envelope('failed', false, true, null, detail));
|
|
77
|
+
process.exitCode = 1;
|
|
85
78
|
return;
|
|
86
79
|
}
|
|
80
|
+
const result = await applyTaskFinalization(boundOperation.request, {
|
|
81
|
+
repoRoot,
|
|
82
|
+
beforeLifecycle: async (currentTaskId) => {
|
|
83
|
+
if (currentTaskId !== taskId)
|
|
84
|
+
throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
|
|
85
|
+
await quiesceSandboxControlForTaskCompletion(path.join(resolveTaskRuntimeRoot(taskId, { repoRoot }), 'sandbox-control'), { taskId, generation, controlRootId, statusDir });
|
|
86
|
+
},
|
|
87
|
+
preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
|
|
88
|
+
});
|
|
89
|
+
const status = result.status === 'prepared' ? 'failed' : result.status;
|
|
90
|
+
process.stdout.write(envelope(status, result.changed, true, result, result.error
|
|
91
|
+
?? (result.status === 'prepared' ? { code: 'TASK_FINALIZATION_PREPARATION_INCOMPLETE', message: 'finalization did not commit', retryable: true } : null)));
|
|
92
|
+
process.exitCode = exitCode(status);
|
|
93
|
+
return;
|
|
87
94
|
}
|
|
88
95
|
const result = await dispatchTaskControlOperation(createDirectHostExecutionContext({ repoRoot }), boundOperation);
|
|
89
96
|
if (result.status === 'prepared') {
|
|
@@ -2,7 +2,7 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
|
|
|
2
2
|
import { lifecycleIntentCatalog } from '../task/lifecycle.js';
|
|
3
3
|
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
4
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
5
|
-
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\n`;
|
|
5
|
+
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.filter((intent) => intent !== 'complete').join(', ')}\nrecover-started: --auto\n`;
|
|
6
6
|
function usageFailure(message) {
|
|
7
7
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
|
|
8
8
|
process.stderr.write(USAGE);
|
|
@@ -18,6 +18,32 @@ function boundedFailureDetail(result) {
|
|
|
18
18
|
? diagnostic
|
|
19
19
|
: `${diagnostic.slice(0, ERROR_DETAIL_LIMIT)}… [truncated]`;
|
|
20
20
|
}
|
|
21
|
+
function requestDiagnostics(args, request, attempt, errorMessage) {
|
|
22
|
+
if (process.env.AGENT_INFRA_GH_DIAGNOSTICS !== '1' && !/rate.?limit/iu.test(errorMessage))
|
|
23
|
+
return '';
|
|
24
|
+
const apiIndex = args.indexOf('api');
|
|
25
|
+
const endpoint = apiIndex < 0
|
|
26
|
+
? args.slice(0, 2).filter((arg) => !arg.startsWith('-')).join(' ')
|
|
27
|
+
: args.slice(apiIndex + 1).find((arg) => !arg.startsWith('-') && !arg.includes('=')) || 'unknown';
|
|
28
|
+
const safeEndpoint = endpoint.startsWith('http')
|
|
29
|
+
? (() => {
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(endpoint);
|
|
32
|
+
return `${url.host}${url.pathname}`;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return 'unknown';
|
|
36
|
+
}
|
|
37
|
+
})()
|
|
38
|
+
: endpoint.split('?')[0] || 'unknown';
|
|
39
|
+
const methodIndex = args.findIndex((arg) => arg === '-X' || arg === '--method');
|
|
40
|
+
const methodFromArgs = methodIndex >= 0 ? args[methodIndex + 1] : undefined;
|
|
41
|
+
const method = methodFromArgs || request.method || (safeEndpoint === 'graphql' ? 'POST' : 'GET');
|
|
42
|
+
const tokenEnvironment = ['GH_TOKEN', 'GITHUB_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_ENTERPRISE_TOKEN']
|
|
43
|
+
.filter((name) => Boolean(process.env[name]));
|
|
44
|
+
const configDirOverride = Boolean(process.env.GH_CONFIG_DIR);
|
|
45
|
+
return ` [gh-diagnostic endpoint=${safeEndpoint} method=${method} attempt=${attempt} tokenEnv=${tokenEnvironment.join(',') || 'none'} ghConfigDirOverride=${configDirOverride}]`;
|
|
46
|
+
}
|
|
21
47
|
function defaultRunner(args, options) {
|
|
22
48
|
const command = process.env.AGENT_INFRA_GH_BIN || 'gh';
|
|
23
49
|
let prefix = [];
|
|
@@ -135,14 +161,18 @@ function createGitHubClient(options = {}) {
|
|
|
135
161
|
if (result.status === 0)
|
|
136
162
|
return { ok: true, value: result.stdout };
|
|
137
163
|
const error = classifyGitHubFailure(result);
|
|
164
|
+
const requestDetail = requestDiagnostics(args, request, attempt + 1, error.message);
|
|
165
|
+
const contextualError = requestDetail
|
|
166
|
+
? { ...error, message: `${error.message}${requestDetail}` }
|
|
167
|
+
: error;
|
|
138
168
|
if (!retryableMethod || !error.requestRetryable)
|
|
139
|
-
return { ok: false, error: clientError(
|
|
169
|
+
return { ok: false, error: clientError(contextualError) };
|
|
140
170
|
if (attempt >= delays.length) {
|
|
141
171
|
return {
|
|
142
172
|
ok: false,
|
|
143
173
|
error: error.code === 'NETWORK_TRANSIENT'
|
|
144
|
-
? { ...clientError(
|
|
145
|
-
: clientError(
|
|
174
|
+
? { ...clientError(contextualError), code: 'NETWORK_RETRY_EXHAUSTED', message: `${contextualError.message} (retry exhausted)` }
|
|
175
|
+
: clientError(contextualError)
|
|
146
176
|
};
|
|
147
177
|
}
|
|
148
178
|
sleep(delays[attempt]);
|
|
@@ -23,7 +23,7 @@ import { inspectSandboxControlContainer } from '../control/container-identity.js
|
|
|
23
23
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
|
|
24
24
|
import { sandboxCoreBindMounts } from '../mounts.js';
|
|
25
25
|
import { materializeSandboxGitMetadata } from '../git-metadata.js';
|
|
26
|
-
import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets, sandboxControlPaths, } from '../workspace-view.js';
|
|
26
|
+
import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets, prepareSandboxCompletedTaskMountSource, sandboxControlPaths, } from '../workspace-view.js';
|
|
27
27
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
|
|
28
28
|
import { validateSelinuxDisableEnv } from '../engines/selinux.js';
|
|
29
29
|
import { captureSandboxAuthority, commandForSandboxAuthority, verifySandboxAuthority } from '../engines/authority.js';
|
|
@@ -1038,7 +1038,10 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
|
|
|
1038
1038
|
const taskSource = target.workspace.mode === 'task-bound'
|
|
1039
1039
|
? assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId)
|
|
1040
1040
|
: null;
|
|
1041
|
-
|
|
1041
|
+
const completedTaskSource = target.workspace.mode === 'task-bound'
|
|
1042
|
+
? prepareSandboxCompletedTaskMountSource(effectiveConfig.repoRoot, target.workspace.taskId)
|
|
1043
|
+
: null;
|
|
1044
|
+
prepareSandboxWorkspaceMountTargets(worktree, target.workspace.mode === 'task-bound' ? target.workspace.taskId : undefined);
|
|
1042
1045
|
if (previousCutoverSnapshot) {
|
|
1043
1046
|
assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
|
|
1044
1047
|
if (previousManifest) {
|
|
@@ -1081,6 +1084,7 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
|
|
|
1081
1084
|
...(target.workspace.mode === 'task-bound'
|
|
1082
1085
|
? {
|
|
1083
1086
|
taskSources: [taskSource],
|
|
1087
|
+
completedTaskSource: completedTaskSource,
|
|
1084
1088
|
taskId: target.workspace.taskId
|
|
1085
1089
|
}
|
|
1086
1090
|
: {})
|
|
@@ -11,11 +11,10 @@ export { containerListFormat, parseLabels } from './list-running.js';
|
|
|
11
11
|
const USAGE = `Usage: ai sandbox ls
|
|
12
12
|
|
|
13
13
|
Lists all containers for the current project. The '#' column is a
|
|
14
|
-
display-only row number; the 'SHORT' column shows the
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
A SHORT value of '-' means this container has no active task short id and may
|
|
14
|
+
display-only row number; the 'SHORT' column shows the task short id bound to
|
|
15
|
+
each task-bound container, or '-' if no short id is registered. Pass the
|
|
16
|
+
SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
17
|
+
A SHORT value of '-' means this container has no task short id and may
|
|
19
18
|
be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
|
|
20
19
|
remove a specific task-bound sandbox.
|
|
21
20
|
|
|
@@ -7,7 +7,7 @@ import { sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceM
|
|
|
7
7
|
import { fetchSandboxRows } from './list-running.js';
|
|
8
8
|
import { rmOne, rmPurge, rmUnbound } from '../removal.js';
|
|
9
9
|
const USAGE = `Usage:
|
|
10
|
-
ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
|
|
10
|
+
ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a retained short id or full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
|
|
11
11
|
ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
|
|
12
12
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
13
13
|
function resolveMissingTaskSandboxBranch(taskId, config) {
|
|
@@ -3,7 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { SANDBOX_CONTROL_ADMISSION_WINDOW_MS, SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
|
|
5
5
|
import { isAgentClientId } from '../../agent-clients/types.js';
|
|
6
|
-
import { normalizeAgentToken } from '../../agent-clients/tokens.js';
|
|
7
6
|
import { readSandboxControlPayload, readSandboxControlStatus } from './state.js';
|
|
8
7
|
import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
|
|
9
8
|
import { readSandboxControlIdentitySentinel } from './identity-sentinel.js';
|
|
@@ -12,7 +11,6 @@ import { hasTaskBoundMarker, isSandbox } from '../environment.js';
|
|
|
12
11
|
import { loadConfig } from '../config.js';
|
|
13
12
|
import { recoverSandboxControlFromHost } from './host-recovery.js';
|
|
14
13
|
const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
15
|
-
const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
|
|
16
14
|
export class SandboxControlClientError extends Error {
|
|
17
15
|
detail;
|
|
18
16
|
accepted;
|
|
@@ -85,8 +83,6 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
|
|
|
85
83
|
function taskViewEffectForRequest(request) {
|
|
86
84
|
if (request.family === 'task-lifecycle')
|
|
87
85
|
return 'progress';
|
|
88
|
-
if (request.family === 'task-finalization')
|
|
89
|
-
return 'recovery';
|
|
90
86
|
return null;
|
|
91
87
|
}
|
|
92
88
|
function parseResponse(raw, id, accepted = false) {
|
|
@@ -180,9 +176,6 @@ function exchangeSandboxControl(request, params) {
|
|
|
180
176
|
|| (requestTask.kind === 'task' && requestTask.taskId !== identity.taskId))) {
|
|
181
177
|
clientError('SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH', 'request task does not match the sandbox identity', false);
|
|
182
178
|
}
|
|
183
|
-
if (identity.mode === 'branch-only' && request.family === 'task-finalization') {
|
|
184
|
-
clientError('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks', false);
|
|
185
|
-
}
|
|
186
179
|
}
|
|
187
180
|
const encoded = `${JSON.stringify(request)}\n`;
|
|
188
181
|
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
@@ -369,9 +362,12 @@ function authority(params) {
|
|
|
369
362
|
export function requestSandboxControl(params) {
|
|
370
363
|
if (!isSandboxControlFamily(params.family))
|
|
371
364
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
|
|
372
|
-
if (params.family === 'task-create' || params.family === 'agent-client'
|
|
365
|
+
if (params.family === 'task-create' || params.family === 'agent-client') {
|
|
373
366
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
|
|
374
367
|
}
|
|
368
|
+
if (params.args[1] === 'complete') {
|
|
369
|
+
clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
|
|
370
|
+
}
|
|
375
371
|
const auth = authority(params);
|
|
376
372
|
const issuedAt = Date.now();
|
|
377
373
|
const request = {
|
|
@@ -400,62 +396,6 @@ export function requestSandboxAgentOperation(params) {
|
|
|
400
396
|
};
|
|
401
397
|
return exchangeSandboxControl(request, params);
|
|
402
398
|
}
|
|
403
|
-
export function requestSandboxTaskFinalization(params) {
|
|
404
|
-
const agent = normalizeAgentToken(params.agent);
|
|
405
|
-
if (!agent)
|
|
406
|
-
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
|
|
407
|
-
const auth = authority(params);
|
|
408
|
-
const issuedAt = Date.now();
|
|
409
|
-
const id = randomUUID();
|
|
410
|
-
const request = {
|
|
411
|
-
version: 4,
|
|
412
|
-
id,
|
|
413
|
-
...auth,
|
|
414
|
-
issuedAt,
|
|
415
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
416
|
-
family: 'task-finalization',
|
|
417
|
-
operation: 'complete',
|
|
418
|
-
agent,
|
|
419
|
-
args: []
|
|
420
|
-
};
|
|
421
|
-
let response;
|
|
422
|
-
try {
|
|
423
|
-
response = exchangeSandboxControl(request, params);
|
|
424
|
-
}
|
|
425
|
-
catch (error) {
|
|
426
|
-
if (!(error instanceof SandboxControlClientError)
|
|
427
|
-
|| error.detail.code !== 'SANDBOX_CONTROL_RESULT_UNKNOWN'
|
|
428
|
-
|| !error.accepted || !error.requestId)
|
|
429
|
-
throw error;
|
|
430
|
-
return recoverAcceptedTaskFinalization(error.requestId, params);
|
|
431
|
-
}
|
|
432
|
-
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
|
|
433
|
-
return recoverAcceptedTaskFinalization(response.id, params);
|
|
434
|
-
}
|
|
435
|
-
return response;
|
|
436
|
-
}
|
|
437
|
-
export function recoverAcceptedTaskFinalization(requestId, params, recover = recoverSandboxControl) {
|
|
438
|
-
const deadline = Date.now() + (params.recoveryBudgetMs ?? SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS);
|
|
439
|
-
while (Date.now() < deadline) {
|
|
440
|
-
const timeoutMs = Math.min(30_000, deadline - Date.now());
|
|
441
|
-
if (timeoutMs <= 0)
|
|
442
|
-
break;
|
|
443
|
-
try {
|
|
444
|
-
const response = recover(requestId, { timeoutMs });
|
|
445
|
-
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
|
|
446
|
-
continue;
|
|
447
|
-
return response;
|
|
448
|
-
}
|
|
449
|
-
catch (error) {
|
|
450
|
-
if (error instanceof SandboxControlClientError
|
|
451
|
-
&& error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
|
|
452
|
-
&& error.requestId === requestId)
|
|
453
|
-
continue;
|
|
454
|
-
throw error;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'accepted finalization request remained unknown after the automatic recovery budget; inspect the receipt and resume by request id', false, true, requestId);
|
|
458
|
-
}
|
|
459
399
|
export function requestSandboxTaskCreate(params) {
|
|
460
400
|
const auth = authority(params);
|
|
461
401
|
const issuedAt = Date.now();
|
|
@@ -4,8 +4,6 @@ import path from 'node:path';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
6
6
|
import { createTask, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
|
|
7
|
-
import { applyTaskFinalization } from '../../task/finalization.js';
|
|
8
|
-
import { serializeTaskFinalizationEnvelope } from '../../task/finalization-envelope.js';
|
|
9
7
|
import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
|
|
10
8
|
import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.js';
|
|
11
9
|
import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSandboxControlAuditContext, appendCriticalAudit, writeSandboxControlTransition } from './audit.js';
|
|
@@ -192,21 +190,6 @@ function taskCreateExecutionFailure(requestId, code, message) {
|
|
|
192
190
|
recovery: 'inspect-domain-state'
|
|
193
191
|
}));
|
|
194
192
|
}
|
|
195
|
-
function finalizationResult(result) {
|
|
196
|
-
const payload = {
|
|
197
|
-
version: 1,
|
|
198
|
-
status: result.status,
|
|
199
|
-
changed: result.changed,
|
|
200
|
-
accepted: true,
|
|
201
|
-
result,
|
|
202
|
-
error: result.error
|
|
203
|
-
};
|
|
204
|
-
return {
|
|
205
|
-
exitCode: result.status === 'completed' ? 0 : result.status === 'blocked' ? 2 : 1,
|
|
206
|
-
stdout: `${JSON.stringify(payload)}\n`,
|
|
207
|
-
stderr: ''
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
193
|
async function executeRequestInner(manifest, manifestPath, request, options = {}) {
|
|
211
194
|
if (request.family === 'task-create') {
|
|
212
195
|
try {
|
|
@@ -227,22 +210,6 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
227
210
|
validateAgentClientOperation(request, manifest);
|
|
228
211
|
return executeAgentClientOperation(request, manifest, manifestPath, options.agentClientOperation);
|
|
229
212
|
}
|
|
230
|
-
if (request.family === 'task-finalization') {
|
|
231
|
-
const parsed = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
|
|
232
|
-
if (parsed.family !== 'task-finalization')
|
|
233
|
-
throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
|
|
234
|
-
const operation = parsed;
|
|
235
|
-
const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
|
|
236
|
-
repoRoot: manifest.repoRoot,
|
|
237
|
-
worktreeRoot: manifest.worktreeRoot,
|
|
238
|
-
runtimeDir: manifest.runtimeDir,
|
|
239
|
-
taskId: manifest.taskId,
|
|
240
|
-
generation: manifest.generation,
|
|
241
|
-
manifestPath,
|
|
242
|
-
requestId: request.id
|
|
243
|
-
}), operation);
|
|
244
|
-
return finalizationResult(await result);
|
|
245
|
-
}
|
|
246
213
|
const boundArgs = bindSandboxControlTask(request, manifest.taskId);
|
|
247
214
|
let operation;
|
|
248
215
|
try {
|
|
@@ -363,7 +330,7 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
|
363
330
|
family: request.family,
|
|
364
331
|
operation: operation && typeof operation === 'object' && 'family' in operation && operation.family === 'task-lifecycle'
|
|
365
332
|
? operation.request.intent
|
|
366
|
-
: request.family === '
|
|
333
|
+
: request.family === 'agent-client' ? request.operation : null,
|
|
367
334
|
phase: 'started-committed',
|
|
368
335
|
outcome: 'in-progress'
|
|
369
336
|
});
|
|
@@ -380,22 +347,9 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
|
380
347
|
});
|
|
381
348
|
const detail = error instanceof Error ? error.message : String(error);
|
|
382
349
|
const code = /^([A-Z][A-Z0-9_]+)/u.exec(detail)?.[1] ?? 'SANDBOX_CONTROL_EXECUTOR_FAILED';
|
|
383
|
-
result = request.family === 'task-
|
|
384
|
-
?
|
|
385
|
-
|
|
386
|
-
stdout: serializeTaskFinalizationEnvelope({
|
|
387
|
-
status: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 'unknown' : 'failed',
|
|
388
|
-
changed: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? null : false,
|
|
389
|
-
accepted: true,
|
|
390
|
-
requestId: request.id,
|
|
391
|
-
result: null,
|
|
392
|
-
error: { code, message: detail, retryable: false }
|
|
393
|
-
}),
|
|
394
|
-
stderr: ''
|
|
395
|
-
}
|
|
396
|
-
: request.family === 'task-create'
|
|
397
|
-
? taskCreateExecutionFailure(request.id, code, detail)
|
|
398
|
-
: lifecycleFailure(code, detail);
|
|
350
|
+
result = request.family === 'task-create'
|
|
351
|
+
? taskCreateExecutionFailure(request.id, code, detail)
|
|
352
|
+
: lifecycleFailure(code, detail);
|
|
399
353
|
}
|
|
400
354
|
process.stdout.write(result.stdout);
|
|
401
355
|
process.stderr.write(result.stderr);
|
|
@@ -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,12 +974,34 @@ 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 matchesControlChildPath = (candidatePath, name) => {
|
|
979
|
+
const resolved = path.resolve(candidatePath);
|
|
980
|
+
if (path.basename(resolved) !== name)
|
|
981
|
+
return false;
|
|
982
|
+
const expected = path.join(canonicalRoot, name);
|
|
983
|
+
try {
|
|
984
|
+
return fs.realpathSync.native(resolved) === expected;
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
if (error.code !== 'ENOENT')
|
|
988
|
+
throw error;
|
|
989
|
+
try {
|
|
990
|
+
return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot;
|
|
991
|
+
}
|
|
992
|
+
catch (parentError) {
|
|
993
|
+
if (parentError.code !== 'ENOENT')
|
|
994
|
+
throw parentError;
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
if (!matchesControlChildPath(candidate.channelDir, 'channel')
|
|
1000
|
+
|| !matchesControlChildPath(candidate.publicStatusDir, 'public')
|
|
1001
|
+
|| !matchesControlChildPath(candidate.processingDir, 'processing')) {
|
|
940
1002
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
941
1003
|
}
|
|
942
|
-
if (
|
|
1004
|
+
if (!matchesControlChildPath(candidate.runtimeDir, 'runtime')) {
|
|
943
1005
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
944
1006
|
}
|
|
945
1007
|
const normalized = {
|