@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
|
@@ -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]);
|
|
@@ -106,8 +106,8 @@ function configuredPlatform(cwd) {
|
|
|
106
106
|
}
|
|
107
107
|
async function releaseNoteContext(input, options = {}) {
|
|
108
108
|
const historyLimit = input.historyLimit ?? 3;
|
|
109
|
-
if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit >
|
|
110
|
-
return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to
|
|
109
|
+
if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 100) {
|
|
110
|
+
return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to 100 are required', retryable: false } };
|
|
111
111
|
}
|
|
112
112
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
113
113
|
const platformType = options.platformType ?? configuredPlatform(cwd);
|
|
@@ -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';
|
|
@@ -741,7 +741,6 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
|
|
|
741
741
|
const container = containerName(effectiveConfig, branch);
|
|
742
742
|
const controlPaths = sandboxControlPaths({
|
|
743
743
|
base: effectiveConfig.controlBase,
|
|
744
|
-
repoRoot: effectiveConfig.repoRoot,
|
|
745
744
|
project: effectiveConfig.project,
|
|
746
745
|
container,
|
|
747
746
|
identity: target.workspace
|
|
@@ -1038,7 +1037,10 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
|
|
|
1038
1037
|
const taskSource = target.workspace.mode === 'task-bound'
|
|
1039
1038
|
? assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId)
|
|
1040
1039
|
: null;
|
|
1041
|
-
|
|
1040
|
+
const completedTaskSource = target.workspace.mode === 'task-bound'
|
|
1041
|
+
? prepareSandboxCompletedTaskMountSource(effectiveConfig.repoRoot, target.workspace.taskId)
|
|
1042
|
+
: null;
|
|
1043
|
+
prepareSandboxWorkspaceMountTargets(worktree, target.workspace.mode === 'task-bound' ? target.workspace.taskId : undefined);
|
|
1042
1044
|
if (previousCutoverSnapshot) {
|
|
1043
1045
|
assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
|
|
1044
1046
|
if (previousManifest) {
|
|
@@ -1076,11 +1078,13 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
|
|
|
1076
1078
|
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1077
1079
|
workspaceViewRoot: workspaceView.root,
|
|
1078
1080
|
controlDir: control.channelDir,
|
|
1081
|
+
...(target.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
|
|
1079
1082
|
controlStatusDir: control.statusDir,
|
|
1080
1083
|
...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
|
|
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
|
: {})
|
|
@@ -1158,8 +1162,6 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
|
|
|
1158
1162
|
]
|
|
1159
1163
|
: []),
|
|
1160
1164
|
'-e',
|
|
1161
|
-
`AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
|
|
1162
|
-
'-e',
|
|
1163
1165
|
`AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
|
|
1164
1166
|
'-e',
|
|
1165
1167
|
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
@@ -1168,7 +1170,10 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
|
|
|
1168
1170
|
'-e',
|
|
1169
1171
|
`AGENT_INFRA_CONTROL_ROOT_ID=${control.controlRootId}`,
|
|
1170
1172
|
...(target.workspace.mode === 'task-bound'
|
|
1171
|
-
? [
|
|
1173
|
+
? [
|
|
1174
|
+
'-e', 'AGENT_INFRA_CONTROL_ROOT_DIR=/run/agent-infra/control-root',
|
|
1175
|
+
'-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime'
|
|
1176
|
+
]
|
|
1172
1177
|
: []),
|
|
1173
1178
|
'--label',
|
|
1174
1179
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
@@ -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) {
|
|
@@ -153,4 +153,16 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
|
|
|
153
153
|
}
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
+
export function loadSandboxControlLocation(repoRoot = detectRepoRoot()) {
|
|
157
|
+
const configPath = path.join(repoRoot, '.agents', '.airc.json');
|
|
158
|
+
if (!fs.existsSync(configPath))
|
|
159
|
+
return null;
|
|
160
|
+
const airc = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
161
|
+
if (typeof airc.project !== 'string' || !airc.project)
|
|
162
|
+
return null;
|
|
163
|
+
const home = homedir();
|
|
164
|
+
if (!home)
|
|
165
|
+
throw new Error('sandbox: home directory is required');
|
|
166
|
+
return { controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'), project: airc.project };
|
|
167
|
+
}
|
|
156
168
|
//# sourceMappingURL=config.js.map
|
|
@@ -14,8 +14,7 @@ const SAFE_OPERATION = /^[a-z][a-z0-9.-]{0,96}$/u;
|
|
|
14
14
|
const AUDIT_MAX_BYTES = 1024 * 1024;
|
|
15
15
|
export function requestAuditFields(manifest, request) {
|
|
16
16
|
return {
|
|
17
|
-
requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId
|
|
18
|
-
requestGeneration: request.generation, requestIssuedAt: request.issuedAt, requestExpiresAt: request.expiresAt
|
|
17
|
+
requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId
|
|
19
18
|
};
|
|
20
19
|
}
|
|
21
20
|
export function resultAuditFields(result) {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import {
|
|
5
|
-
import { isAgentClientId } from '../../agent-clients/types.js';
|
|
6
|
-
import { normalizeAgentToken } from '../../agent-clients/tokens.js';
|
|
4
|
+
import { SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
|
|
7
5
|
import { readSandboxControlPayload, readSandboxControlStatus } from './state.js';
|
|
8
6
|
import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
|
|
9
7
|
import { readSandboxControlIdentitySentinel } from './identity-sentinel.js';
|
|
@@ -12,7 +10,6 @@ import { hasTaskBoundMarker, isSandbox } from '../environment.js';
|
|
|
12
10
|
import { loadConfig } from '../config.js';
|
|
13
11
|
import { recoverSandboxControlFromHost } from './host-recovery.js';
|
|
14
12
|
const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
15
|
-
const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
|
|
16
13
|
export class SandboxControlClientError extends Error {
|
|
17
14
|
detail;
|
|
18
15
|
accepted;
|
|
@@ -31,7 +28,7 @@ function sleep(milliseconds) {
|
|
|
31
28
|
function clientError(code, message, retryable, accepted = false, requestId = null) {
|
|
32
29
|
throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted, requestId);
|
|
33
30
|
}
|
|
34
|
-
function preflight(statusDir,
|
|
31
|
+
function preflight(statusDir, taskViewEffect = null, now = Date.now(), env = process.env) {
|
|
35
32
|
if (fs.existsSync(statusDir)) {
|
|
36
33
|
let identity;
|
|
37
34
|
try {
|
|
@@ -43,9 +40,6 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
|
|
|
43
40
|
: 'SANDBOX_CONTROL_IDENTITY_MALFORMED';
|
|
44
41
|
clientError(code, 'sandbox control identity is missing or invalid', false);
|
|
45
42
|
}
|
|
46
|
-
if (identity.generation !== generation) {
|
|
47
|
-
clientError('SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH', 'sandbox control identity generation does not match the request', false);
|
|
48
|
-
}
|
|
49
43
|
if (env.AGENT_INFRA_CONTROL_ROOT_ID && identity.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
|
|
50
44
|
clientError('SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH', 'sandbox control identity root does not match the client configuration', false);
|
|
51
45
|
}
|
|
@@ -57,9 +51,6 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
|
|
|
57
51
|
catch {
|
|
58
52
|
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status is missing or invalid', true);
|
|
59
53
|
}
|
|
60
|
-
if (status.generation !== generation) {
|
|
61
|
-
clientError('SANDBOX_CONTROL_GENERATION_INVALID', 'broker status generation does not match this sandbox', false);
|
|
62
|
-
}
|
|
63
54
|
if (now - status.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS || status.updatedAt > now + 1_000) {
|
|
64
55
|
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status heartbeat is stale', true);
|
|
65
56
|
}
|
|
@@ -81,12 +72,11 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
|
|
|
81
72
|
clientError(access.reasonCode ?? 'SANDBOX_TASK_VIEW_DENIED', access.message ?? 'task view denied', false);
|
|
82
73
|
}
|
|
83
74
|
}
|
|
75
|
+
return status.generation;
|
|
84
76
|
}
|
|
85
77
|
function taskViewEffectForRequest(request) {
|
|
86
78
|
if (request.family === 'task-lifecycle')
|
|
87
79
|
return 'progress';
|
|
88
|
-
if (request.family === 'task-finalization')
|
|
89
|
-
return 'recovery';
|
|
90
80
|
return null;
|
|
91
81
|
}
|
|
92
82
|
function parseResponse(raw, id, accepted = false) {
|
|
@@ -169,7 +159,7 @@ function exchangeSandboxControl(request, params) {
|
|
|
169
159
|
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
170
160
|
const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
|
|
171
161
|
const taskViewEffect = taskViewEffectForRequest(request);
|
|
172
|
-
preflight(statusDir,
|
|
162
|
+
const hostGeneration = preflight(statusDir, taskViewEffect);
|
|
173
163
|
const identityPath = path.join(statusDir, 'identity.json');
|
|
174
164
|
if (fs.existsSync(statusDir)) {
|
|
175
165
|
const identity = readSandboxControlIdentitySentinel(statusDir);
|
|
@@ -180,9 +170,6 @@ function exchangeSandboxControl(request, params) {
|
|
|
180
170
|
|| (requestTask.kind === 'task' && requestTask.taskId !== identity.taskId))) {
|
|
181
171
|
clientError('SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH', 'request task does not match the sandbox identity', false);
|
|
182
172
|
}
|
|
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
173
|
}
|
|
187
174
|
const encoded = `${JSON.stringify(request)}\n`;
|
|
188
175
|
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
@@ -220,7 +207,7 @@ function exchangeSandboxControl(request, params) {
|
|
|
220
207
|
try {
|
|
221
208
|
// The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
|
|
222
209
|
accepted = true;
|
|
223
|
-
response = readPublishedResponse(raw, request.id, channelDir, accepted,
|
|
210
|
+
response = readPublishedResponse(raw, request.id, channelDir, accepted, hostGeneration);
|
|
224
211
|
malformedResponseRaw = null;
|
|
225
212
|
}
|
|
226
213
|
catch (error) {
|
|
@@ -246,7 +233,7 @@ function exchangeSandboxControl(request, params) {
|
|
|
246
233
|
}
|
|
247
234
|
if (!accepted) {
|
|
248
235
|
try {
|
|
249
|
-
preflight(statusDir,
|
|
236
|
+
preflight(statusDir, taskViewEffect);
|
|
250
237
|
}
|
|
251
238
|
catch (error) {
|
|
252
239
|
if (cancelPendingRequest(requestPath))
|
|
@@ -273,6 +260,9 @@ export function recoverSandboxControl(requestId, params = {}) {
|
|
|
273
260
|
const config = loadConfig();
|
|
274
261
|
const response = recoverSandboxControlFromHost(requestId, {
|
|
275
262
|
managedRoot: path.join(config.controlBase, config.project),
|
|
263
|
+
controlBase: config.controlBase,
|
|
264
|
+
project: config.project,
|
|
265
|
+
repoRoot: config.repoRoot,
|
|
276
266
|
timeoutMs: params.timeoutMs
|
|
277
267
|
});
|
|
278
268
|
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
|
|
@@ -359,109 +349,24 @@ export function recoverSandboxControlFromChannel(requestId, params) {
|
|
|
359
349
|
}
|
|
360
350
|
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'request did not produce a final result; inspect domain state before retrying', false, accepted, requestId);
|
|
361
351
|
}
|
|
362
|
-
function authority(params) {
|
|
363
|
-
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
364
|
-
const generation = params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
365
|
-
if (!token || !generation)
|
|
366
|
-
clientError('SANDBOX_CONTROL_UNAVAILABLE', 'control authority is missing', false);
|
|
367
|
-
return { token, generation };
|
|
368
|
-
}
|
|
369
352
|
export function requestSandboxControl(params) {
|
|
370
353
|
if (!isSandboxControlFamily(params.family))
|
|
371
354
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
|
|
372
|
-
if (params.family === 'task-create'
|
|
355
|
+
if (params.family === 'task-create') {
|
|
373
356
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
|
|
374
357
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const request = {
|
|
378
|
-
version: 4, id: randomUUID(), ...auth, issuedAt,
|
|
379
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
380
|
-
family: params.family, args: params.args
|
|
381
|
-
};
|
|
382
|
-
return exchangeSandboxControl(request, params);
|
|
383
|
-
}
|
|
384
|
-
export function requestSandboxAgentOperation(params) {
|
|
385
|
-
if (!isAgentClientId(params.agentClient)) {
|
|
386
|
-
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client id is invalid', false);
|
|
358
|
+
if (params.args[1] === 'complete') {
|
|
359
|
+
clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
|
|
387
360
|
}
|
|
388
|
-
const auth = authority(params);
|
|
389
|
-
const issuedAt = Date.now();
|
|
390
361
|
const request = {
|
|
391
|
-
version:
|
|
392
|
-
|
|
393
|
-
...auth,
|
|
394
|
-
issuedAt,
|
|
395
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
396
|
-
family: 'agent-client',
|
|
397
|
-
agentClient: params.agentClient,
|
|
398
|
-
operation: params.operation,
|
|
399
|
-
payload: params.payload
|
|
362
|
+
version: 5, id: randomUUID(),
|
|
363
|
+
family: params.family, args: params.args
|
|
400
364
|
};
|
|
401
365
|
return exchangeSandboxControl(request, params);
|
|
402
366
|
}
|
|
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
367
|
export function requestSandboxTaskCreate(params) {
|
|
460
|
-
const auth = authority(params);
|
|
461
|
-
const issuedAt = Date.now();
|
|
462
368
|
const request = {
|
|
463
|
-
version:
|
|
464
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
369
|
+
version: 5, id: randomUUID(),
|
|
465
370
|
family: 'task-create', candidate: params.candidate,
|
|
466
371
|
};
|
|
467
372
|
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
@@ -4,15 +4,12 @@ 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';
|
|
12
10
|
import { validateSandboxControlIdentity } from './identity-sentinel.js';
|
|
13
11
|
import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../../task/control-authority.js';
|
|
14
12
|
import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
|
|
15
|
-
import { executeAgentClientOperation, validateAgentClientOperation } from './agent-operation.js';
|
|
16
13
|
function sameBrokerOwner(left, right) {
|
|
17
14
|
return left.version === right.version
|
|
18
15
|
&& left.pid === right.pid
|
|
@@ -192,21 +189,6 @@ function taskCreateExecutionFailure(requestId, code, message) {
|
|
|
192
189
|
recovery: 'inspect-domain-state'
|
|
193
190
|
}));
|
|
194
191
|
}
|
|
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
192
|
async function executeRequestInner(manifest, manifestPath, request, options = {}) {
|
|
211
193
|
if (request.family === 'task-create') {
|
|
212
194
|
try {
|
|
@@ -223,26 +205,6 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
223
205
|
return taskCreateExecutionFailure(request.id, code, detail);
|
|
224
206
|
}
|
|
225
207
|
}
|
|
226
|
-
if (request.family === 'agent-client') {
|
|
227
|
-
validateAgentClientOperation(request, manifest);
|
|
228
|
-
return executeAgentClientOperation(request, manifest, manifestPath, options.agentClientOperation);
|
|
229
|
-
}
|
|
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
208
|
const boundArgs = bindSandboxControlTask(request, manifest.taskId);
|
|
247
209
|
let operation;
|
|
248
210
|
try {
|
|
@@ -302,10 +264,11 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
|
302
264
|
const gate = await waitForGate(nonce);
|
|
303
265
|
const gateOwner = gate.owner;
|
|
304
266
|
process.disconnect?.();
|
|
305
|
-
const
|
|
306
|
-
if (!
|
|
267
|
+
const requestedManifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
|
|
268
|
+
if (!requestedManifestPath)
|
|
307
269
|
throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
|
|
308
|
-
const manifest = readSandboxControlManifest(
|
|
270
|
+
const manifest = readSandboxControlManifest(requestedManifestPath);
|
|
271
|
+
const manifestPath = fs.realpathSync.native(requestedManifestPath);
|
|
309
272
|
const identity = validateSandboxControlIdentity({
|
|
310
273
|
publicStatusDir: manifest.publicStatusDir,
|
|
311
274
|
root: path.dirname(path.resolve(manifestPath)),
|
|
@@ -357,13 +320,13 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
|
357
320
|
return null;
|
|
358
321
|
}
|
|
359
322
|
})()
|
|
360
|
-
:
|
|
323
|
+
: null;
|
|
361
324
|
const context = createSandboxControlAuditContext(manifest, {
|
|
362
325
|
requestId: request.id,
|
|
363
326
|
family: request.family,
|
|
364
327
|
operation: operation && typeof operation === 'object' && 'family' in operation && operation.family === 'task-lifecycle'
|
|
365
328
|
? operation.request.intent
|
|
366
|
-
:
|
|
329
|
+
: null,
|
|
367
330
|
phase: 'started-committed',
|
|
368
331
|
outcome: 'in-progress'
|
|
369
332
|
});
|
|
@@ -380,22 +343,9 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
|
380
343
|
});
|
|
381
344
|
const detail = error instanceof Error ? error.message : String(error);
|
|
382
345
|
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);
|
|
346
|
+
result = request.family === 'task-create'
|
|
347
|
+
? taskCreateExecutionFailure(request.id, code, detail)
|
|
348
|
+
: lifecycleFailure(code, detail);
|
|
399
349
|
}
|
|
400
350
|
process.stdout.write(result.stdout);
|
|
401
351
|
process.stderr.write(result.stderr);
|