@fitlab-ai/agent-infra 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +144 -123
- package/dist/lib/agent-clients/adapter.js +94 -4
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +15 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/registry.js +9 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +19 -1
- package/dist/lib/internal/task-orchestration.js +170 -27
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/pr-checks.js +1 -1
- package/dist/lib/platform/pull-requests.js +47 -1
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +7 -8
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +110 -228
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +12 -3
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +58 -0
- package/dist/lib/sandbox/control/protocol.js +54 -0
- package/dist/lib/sandbox/control/server.js +144 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +109 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +278 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/orchestration.js +505 -34
- package/dist/lib/task/resolve-ref.js +41 -1
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +130 -6
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +21 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +18 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/registry.ts +9 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +117 -21
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/pr-checks.ts +1 -1
- package/lib/platform/pull-requests.ts +49 -1
- package/lib/platform/releases.ts +16 -4
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +11 -8
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +127 -260
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +29 -3
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +79 -0
- package/lib/sandbox/control/protocol.ts +110 -0
- package/lib/sandbox/control/server.ts +155 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +150 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +376 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/orchestration.ts +673 -41
- package/lib/task/resolve-ref.ts +46 -1
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +3 -0
- package/templates/.agents/rules/task-management.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +21 -17
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { runSafeEngine, runVerboseEngine } from '../shell.ts';
|
|
5
5
|
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
6
6
|
import { isRemovedHashShortIdInput } from '../../task/short-id.ts';
|
|
7
|
+
import { parseSandboxWorkspaceIdentity } from '../workspace-identity.ts';
|
|
7
8
|
|
|
8
9
|
export type SandboxRow = {
|
|
9
10
|
name: string;
|
|
@@ -11,6 +12,8 @@ export type SandboxRow = {
|
|
|
11
12
|
branch: string;
|
|
12
13
|
running: boolean;
|
|
13
14
|
index: number | null;
|
|
15
|
+
workspaceMode?: 'task-bound' | 'branch-only' | 'legacy-invalid';
|
|
16
|
+
taskId?: string | null;
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
export function containerListFormat(): string {
|
|
@@ -36,19 +39,32 @@ export function parseLabels(csv: string): Record<string, string> {
|
|
|
36
39
|
return labels;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
export function parseSandboxRows(
|
|
42
|
+
export function parseSandboxRows(
|
|
43
|
+
rawOutput: string,
|
|
44
|
+
branchKey: string,
|
|
45
|
+
workspaceKeys?: Readonly<{ mode: string; taskId: string }>
|
|
46
|
+
): SandboxRow[] {
|
|
40
47
|
if (!rawOutput) {
|
|
41
48
|
return [];
|
|
42
49
|
}
|
|
43
50
|
return rawOutput.split('\n').map((line) => {
|
|
44
51
|
const [name = '', status = '', labelsCsv = ''] = line.split('\t');
|
|
45
|
-
const
|
|
52
|
+
const labels = parseLabels(labelsCsv);
|
|
53
|
+
const workspace = workspaceKeys
|
|
54
|
+
? parseSandboxWorkspaceIdentity(labels, workspaceKeys)
|
|
55
|
+
: null;
|
|
46
56
|
return {
|
|
47
57
|
name,
|
|
48
58
|
status,
|
|
49
|
-
branch,
|
|
59
|
+
branch: labels[branchKey] ?? '',
|
|
50
60
|
running: status.startsWith('Up '),
|
|
51
|
-
index: null
|
|
61
|
+
index: null,
|
|
62
|
+
...(workspace
|
|
63
|
+
? {
|
|
64
|
+
workspaceMode: workspace.mode,
|
|
65
|
+
taskId: workspace.mode === 'task-bound' ? workspace.taskId : null
|
|
66
|
+
}
|
|
67
|
+
: {})
|
|
52
68
|
};
|
|
53
69
|
});
|
|
54
70
|
}
|
|
@@ -76,7 +92,8 @@ export function sortAndIndexSandboxRows(rows: SandboxRow[]): {
|
|
|
76
92
|
export function fetchSandboxRows(
|
|
77
93
|
engine: string,
|
|
78
94
|
label: string,
|
|
79
|
-
branchKey: string
|
|
95
|
+
branchKey: string,
|
|
96
|
+
workspaceKeys?: Readonly<{ mode: string; taskId: string }>
|
|
80
97
|
): { running: SandboxRow[]; nonRunning: SandboxRow[] } {
|
|
81
98
|
const raw = runSafeEngine(engine, 'docker', [
|
|
82
99
|
'ps',
|
|
@@ -86,7 +103,7 @@ export function fetchSandboxRows(
|
|
|
86
103
|
'--format',
|
|
87
104
|
containerListFormat()
|
|
88
105
|
]);
|
|
89
|
-
return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
|
|
106
|
+
return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey, workspaceKeys));
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
/**
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { loadConfig } from '../config.ts';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
sandboxBranchLabel,
|
|
6
|
+
sandboxLabel,
|
|
7
|
+
sandboxTaskIdLabel,
|
|
8
|
+
sandboxWorkspaceModeLabel
|
|
9
|
+
} from '../constants.ts';
|
|
5
10
|
import { detectEngine } from '../engine.ts';
|
|
6
11
|
import { formatTable } from '../../table.ts';
|
|
7
|
-
import {
|
|
12
|
+
import { loadShortIdByTaskId } from '../../task/short-id.ts';
|
|
8
13
|
import { fetchSandboxRows } from './list-running.ts';
|
|
9
14
|
|
|
10
15
|
export { containerListFormat, parseLabels } from './list-running.ts';
|
|
@@ -22,20 +27,22 @@ to remove with "ai sandbox rm <branch>".
|
|
|
22
27
|
Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
|
|
23
28
|
state paths.`;
|
|
24
29
|
|
|
25
|
-
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'] as const;
|
|
30
|
+
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'WORKSPACE', 'TASK', 'BRANCH'] as const;
|
|
26
31
|
|
|
27
32
|
type ContainerTableRow = {
|
|
28
33
|
row: string;
|
|
29
34
|
shortId: string;
|
|
30
35
|
name: string;
|
|
31
36
|
status: string;
|
|
37
|
+
workspace: string;
|
|
38
|
+
taskId: string;
|
|
32
39
|
branch: string;
|
|
33
40
|
};
|
|
34
41
|
|
|
35
42
|
export function formatContainerTable(rows: ContainerTableRow[], zebra = false): string[] {
|
|
36
43
|
return formatTable(
|
|
37
44
|
CONTAINER_TABLE_HEADERS,
|
|
38
|
-
rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]),
|
|
45
|
+
rows.map((r) => [r.row, r.shortId, r.name, r.status, r.workspace, r.taskId, r.branch]),
|
|
39
46
|
{ zebra }
|
|
40
47
|
);
|
|
41
48
|
}
|
|
@@ -49,7 +56,12 @@ export function ls(args: string[] = []): void {
|
|
|
49
56
|
const config = loadConfig();
|
|
50
57
|
const engine = detectEngine(config);
|
|
51
58
|
const label = sandboxLabel(config);
|
|
52
|
-
const { running, nonRunning } = fetchSandboxRows(
|
|
59
|
+
const { running, nonRunning } = fetchSandboxRows(
|
|
60
|
+
engine,
|
|
61
|
+
label,
|
|
62
|
+
sandboxBranchLabel(config),
|
|
63
|
+
{ mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
|
|
64
|
+
);
|
|
53
65
|
|
|
54
66
|
p.intro(pc.cyan(`Sandbox status for ${config.project}`));
|
|
55
67
|
|
|
@@ -59,12 +71,16 @@ export function ls(args: string[] = []): void {
|
|
|
59
71
|
p.log.warn(' No sandbox containers');
|
|
60
72
|
} else {
|
|
61
73
|
const tableRows: ContainerTableRow[] = ordered.map((container, i) => {
|
|
62
|
-
const shortId = container.
|
|
74
|
+
const shortId = container.taskId
|
|
75
|
+
? loadShortIdByTaskId(config.repoRoot).get(container.taskId) ?? null
|
|
76
|
+
: null;
|
|
63
77
|
return {
|
|
64
78
|
row: String(i + 1),
|
|
65
79
|
shortId: shortId ?? '-',
|
|
66
80
|
name: container.name,
|
|
67
81
|
status: container.status,
|
|
82
|
+
workspace: container.workspaceMode ?? 'legacy-invalid',
|
|
83
|
+
taskId: container.taskId ?? '-',
|
|
68
84
|
branch: container.branch
|
|
69
85
|
};
|
|
70
86
|
});
|
|
@@ -8,8 +8,8 @@ import { sandboxImageRefreshLabel } from '../constants.ts';
|
|
|
8
8
|
import { detectEngine, ensureDocker } from '../engine.ts';
|
|
9
9
|
import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
|
|
10
10
|
import { pruneSandboxDanglingImages } from '../image-prune.ts';
|
|
11
|
-
import { resolveTools } from '../tools.ts';
|
|
12
11
|
import type { SandboxTool } from '../tools.ts';
|
|
12
|
+
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
|
|
13
13
|
import {
|
|
14
14
|
buildImageSignature,
|
|
15
15
|
buildSandboxImageArgs,
|
|
@@ -97,8 +97,9 @@ export async function rebuild(args: string[]): Promise<void> {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const config = loadConfig();
|
|
100
|
-
const
|
|
101
|
-
const
|
|
100
|
+
const capabilityPlan = createSandboxCapabilityPlan(config);
|
|
101
|
+
const tools = [...capabilityPlan.tools];
|
|
102
|
+
const preparedDockerfile = prepareDockerfile(config, capabilityPlan.image);
|
|
102
103
|
const imageSignature = buildImageSignature(preparedDockerfile, tools);
|
|
103
104
|
const quiet = values.quiet ?? false;
|
|
104
105
|
const refresh = values.refresh ?? false;
|
|
@@ -1,128 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { parseArgs } from 'node:util';
|
|
3
|
-
import {
|
|
4
|
-
buildLockedGuidance,
|
|
5
|
-
discoverProjects,
|
|
6
|
-
formatCredentialWarnings,
|
|
7
|
-
formatRemaining,
|
|
8
|
-
reconcileClaudeCredentials,
|
|
9
|
-
redactCommandError,
|
|
10
|
-
validateClaudeCredentialsEnvOverride
|
|
11
|
-
} from '../credentials.ts';
|
|
12
|
-
import { runProbe } from '../shell.ts';
|
|
1
|
+
import { runClaudeCodeRefresh } from '../../agent-clients/adapters/claude-code-refresh.ts';
|
|
13
2
|
|
|
14
|
-
const
|
|
3
|
+
const refresh = runClaudeCodeRefresh;
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type RefreshDeps = Partial<ReconcileOptions> & {
|
|
19
|
-
spawnFn?: typeof runProbe;
|
|
20
|
-
discoverFn?: typeof discoverProjects;
|
|
21
|
-
writeStdout?: (chunk: string) => unknown;
|
|
22
|
-
writeStderr?: (chunk: string) => unknown;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export function probeClaudeStatus(spawnFn: typeof runProbe = runProbe): { ok: boolean; stderr: string; error: string | null } {
|
|
26
|
-
const result = spawnFn('claude', ['/status'], {
|
|
27
|
-
encoding: 'utf8',
|
|
28
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
29
|
-
timeout: 30_000
|
|
30
|
-
});
|
|
31
|
-
return {
|
|
32
|
-
ok: result.status === 0,
|
|
33
|
-
stderr: result.stderr?.toString() ?? '',
|
|
34
|
-
error: result.error?.message ?? null
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function refresh(args: string[], deps: RefreshDeps = {}): Promise<number> {
|
|
39
|
-
const {
|
|
40
|
-
spawnFn = runProbe,
|
|
41
|
-
execFn,
|
|
42
|
-
readFn,
|
|
43
|
-
existsFn,
|
|
44
|
-
writeFn,
|
|
45
|
-
writeHostFn,
|
|
46
|
-
discoverFn = discoverProjects,
|
|
47
|
-
writeStdout = (chunk: string) => process.stdout.write(chunk),
|
|
48
|
-
writeStderr = (chunk: string) => process.stderr.write(chunk)
|
|
49
|
-
} = deps;
|
|
50
|
-
|
|
51
|
-
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
52
|
-
writeStdout(`${USAGE}\n`);
|
|
53
|
-
return 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const { positionals } = parseArgs({ args, allowPositionals: true, strict: true });
|
|
57
|
-
if (positionals.length > 0) {
|
|
58
|
-
throw new Error(USAGE);
|
|
59
|
-
}
|
|
60
|
-
validateClaudeCredentialsEnvOverride();
|
|
61
|
-
|
|
62
|
-
const home = homedir();
|
|
63
|
-
if (!home) {
|
|
64
|
-
throw new Error('sandbox: home directory is required');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const projects = discoverFn(home);
|
|
68
|
-
if (projects.length === 0) {
|
|
69
|
-
writeStdout('No project credentials to refresh.\n');
|
|
70
|
-
return 0;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const reconcileOptions = { execFn, readFn, existsFn, writeFn, writeHostFn, projects };
|
|
74
|
-
let result = reconcileClaudeCredentials(home, reconcileOptions);
|
|
75
|
-
if (result.status === 'STALE_ACCESS' && result.authoritative === null) {
|
|
76
|
-
writeStdout('Host credentials appear stale; probing claude /status to trigger refresh...\n');
|
|
77
|
-
const probe = probeClaudeStatus(spawnFn);
|
|
78
|
-
if (!probe.ok) {
|
|
79
|
-
writeStderr(`Probe failed: ${redactCommandError(probe.stderr || probe.error || 'unknown error')}\n`);
|
|
80
|
-
writeStderr('Run "claude /login" on the host to renew credentials.\n');
|
|
81
|
-
return 1;
|
|
82
|
-
}
|
|
83
|
-
writeStdout('Probe succeeded; re-inspecting host credentials.\n');
|
|
84
|
-
result = reconcileClaudeCredentials(home, reconcileOptions);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (result.status === 'MISSING') {
|
|
88
|
-
writeStderr('No Claude Code credentials found on host.\n');
|
|
89
|
-
writeStderr('Run "claude /login" on the host to authenticate.\n');
|
|
90
|
-
return 1;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (result.status === 'KEYCHAIN_LOCKED') {
|
|
94
|
-
writeStderr(`${buildLockedGuidance()}\n`);
|
|
95
|
-
return 1;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (result.status === 'KEYCHAIN_ERROR') {
|
|
99
|
-
writeStderr(`Host keychain error: ${redactCommandError(result.detail || 'unknown error')}\n`);
|
|
100
|
-
writeStderr(`${buildLockedGuidance()}\n`);
|
|
101
|
-
return 1;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (result.status === 'KEYCHAIN_WRITE_FAILED') {
|
|
105
|
-
writeStderr(`[host] keychain write failed: ${formatCredentialWarnings(result.warnings) || 'unknown error'}\n`);
|
|
106
|
-
return 1;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (result.status !== 'OK') {
|
|
110
|
-
writeStderr('Host credentials still invalid after probe; run "claude /login".\n');
|
|
111
|
-
return 1;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (result.authoritative && result.authoritative !== 'host' && result.hostWritten) {
|
|
115
|
-
writeStdout(`[host] reconciled from ${result.authoritative}\n`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
for (const project of projects) {
|
|
119
|
-
const action = result.filesWritten.includes(project) ? 'updated' : 'unchanged';
|
|
120
|
-
writeStdout(`[${project}] ${action}; expires in ${formatRemaining(result.expiresAt)}\n`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
for (const failure of result.fileErrors) {
|
|
124
|
-
writeStderr(`[${failure.project}] sync failed: ${failure.error}\n`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return result.fileErrors.length > 0 ? 1 : 0;
|
|
128
|
-
}
|
|
5
|
+
export { probeClaudeStatus, runClaudeCodeRefresh } from '../../agent-clients/adapters/claude-code-refresh.ts';
|
|
6
|
+
export { refresh };
|
|
@@ -18,7 +18,7 @@ import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedV
|
|
|
18
18
|
import { pruneSandboxDanglingImages } from '../image-prune.ts';
|
|
19
19
|
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
|
|
20
20
|
import { runOk, runSafe, runSafeEngine } from '../shell.ts';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveSandboxTarget } from '../workspace-identity.ts';
|
|
22
22
|
import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
|
|
23
23
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
|
|
24
24
|
import type { SandboxTool } from '../tools.ts';
|
|
@@ -81,6 +81,16 @@ async function rmOne(
|
|
|
81
81
|
for (const name of matchedContainers) {
|
|
82
82
|
runSafeEngine(engine, 'docker', ['stop', name]);
|
|
83
83
|
runSafeEngine(engine, 'docker', ['rm', name]);
|
|
84
|
+
for (const [base, label] of [
|
|
85
|
+
[path.join(config.workspaceViewBase, config.project), 'Workspace view'],
|
|
86
|
+
[path.join(config.controlBase, config.project), 'Control channel']
|
|
87
|
+
] as const) {
|
|
88
|
+
const directory = path.join(base, name);
|
|
89
|
+
if (fs.existsSync(directory)) {
|
|
90
|
+
removeManagedDir(base, directory);
|
|
91
|
+
if (!options.quiet) p.log.success(`${label} removed: ${directory}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
84
94
|
}
|
|
85
95
|
spinner.stop(pc.green(`Removed container(s): ${matchedContainers.join(', ')}`));
|
|
86
96
|
} else {
|
|
@@ -224,6 +234,17 @@ async function rmPurge(config: SandboxConfig, tools: SandboxTool[]): Promise<voi
|
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
for (const [base, label] of [
|
|
238
|
+
[config.workspaceViewBase, 'workspace views'],
|
|
239
|
+
[config.controlBase, 'control channels']
|
|
240
|
+
] as const) {
|
|
241
|
+
const projectDir = path.join(base, config.project);
|
|
242
|
+
if (fs.existsSync(projectDir)) {
|
|
243
|
+
removeManagedDir(base, projectDir);
|
|
244
|
+
p.log.success(`Removed project ${label}: ${projectDir}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
227
248
|
const shouldRemoveImage = await p.confirm({
|
|
228
249
|
message: `Remove image ${config.imageName}?`,
|
|
229
250
|
initialValue: false
|
|
@@ -369,6 +390,6 @@ export async function rm(args: string[]): Promise<void> {
|
|
|
369
390
|
return;
|
|
370
391
|
}
|
|
371
392
|
|
|
372
|
-
const
|
|
373
|
-
await rmOne(config, tools, branch);
|
|
393
|
+
const target = resolveSandboxTarget(positionals[0] ?? '', config.repoRoot);
|
|
394
|
+
await rmOne(config, tools, target.branch);
|
|
374
395
|
}
|
|
@@ -3,9 +3,19 @@ import * as p from '@clack/prompts';
|
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import type { SandboxConfig } from '../config.ts';
|
|
5
5
|
import { loadConfig } from '../config.ts';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
assertValidBranchName,
|
|
8
|
+
containerNameCandidates,
|
|
9
|
+
sandboxBranchLabel,
|
|
10
|
+
sandboxLabel,
|
|
11
|
+
sandboxTaskIdLabel,
|
|
12
|
+
sandboxWorkspaceModeLabel,
|
|
13
|
+
worktreeDirCandidates
|
|
14
|
+
} from '../constants.ts';
|
|
15
|
+
import { fetchSandboxRows, selectSandboxContainer } from './list-running.ts';
|
|
8
16
|
import { resolveTools, toolConfigDirCandidates } from '../tools.ts';
|
|
17
|
+
import { detectEngine } from '../engine.ts';
|
|
18
|
+
import { resolveSandboxTarget } from '../workspace-identity.ts';
|
|
9
19
|
|
|
10
20
|
const USAGE = `Usage: ai sandbox show <branch | TASK-id | N>
|
|
11
21
|
|
|
@@ -51,10 +61,22 @@ export function show(args: string[] = []): void {
|
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
const config = loadConfig();
|
|
54
|
-
const
|
|
64
|
+
const target = resolveSandboxTarget(args[0]!, config.repoRoot);
|
|
65
|
+
const branch = target.branch;
|
|
55
66
|
assertValidBranchName(branch);
|
|
56
67
|
|
|
57
68
|
const detail = collectSandboxDetail(config, branch);
|
|
69
|
+
const engine = detectEngine(config);
|
|
70
|
+
const rows = fetchSandboxRows(
|
|
71
|
+
engine,
|
|
72
|
+
sandboxLabel(config),
|
|
73
|
+
sandboxBranchLabel(config),
|
|
74
|
+
{ mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
|
|
75
|
+
);
|
|
76
|
+
const container = selectSandboxContainer(
|
|
77
|
+
[...rows.running, ...rows.nonRunning],
|
|
78
|
+
containerNameCandidates(config, branch)
|
|
79
|
+
);
|
|
58
80
|
|
|
59
81
|
p.intro(pc.cyan(`Sandbox detail for ${config.project} · ${branch}`));
|
|
60
82
|
|
|
@@ -67,6 +89,10 @@ export function show(args: string[] = []): void {
|
|
|
67
89
|
}
|
|
68
90
|
}
|
|
69
91
|
|
|
92
|
+
p.log.step('Workspace identity');
|
|
93
|
+
process.stdout.write(` Mode: ${container?.workspaceMode ?? 'legacy-invalid'}\n`);
|
|
94
|
+
process.stdout.write(` Task: ${container?.taskId ?? '-'}\n`);
|
|
95
|
+
|
|
70
96
|
for (const tool of detail.toolStates) {
|
|
71
97
|
p.log.step(`${tool.name} state`);
|
|
72
98
|
if (tool.entries.length === 0) {
|
|
@@ -9,9 +9,9 @@ import { detectEngine } from '../engine.ts';
|
|
|
9
9
|
import { ensureSandboxReady } from '../recovery.ts';
|
|
10
10
|
import {
|
|
11
11
|
fetchSandboxRows,
|
|
12
|
-
resolveBranchArg,
|
|
13
12
|
selectSandboxContainer,
|
|
14
13
|
} from './list-running.ts';
|
|
14
|
+
import { resolveSandboxTarget } from '../workspace-identity.ts';
|
|
15
15
|
|
|
16
16
|
const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
|
|
17
17
|
|
|
@@ -56,7 +56,8 @@ export async function start(args: string[]): Promise<void> {
|
|
|
56
56
|
|
|
57
57
|
const config = loadConfig();
|
|
58
58
|
const engine = detectEngine(config);
|
|
59
|
-
const
|
|
59
|
+
const target = resolveSandboxTarget(parsed.target, config.repoRoot);
|
|
60
|
+
const branch = target.branch;
|
|
60
61
|
assertValidBranchName(branch);
|
|
61
62
|
|
|
62
63
|
const { running, nonRunning } = fetchSandboxRows(
|
|
@@ -79,11 +80,12 @@ export async function start(args: string[]): Promise<void> {
|
|
|
79
80
|
config,
|
|
80
81
|
engine,
|
|
81
82
|
branch,
|
|
83
|
+
workspace: target.workspace,
|
|
82
84
|
row: found,
|
|
83
85
|
allowRecreate: parsed.recreate,
|
|
84
|
-
recreate: async (
|
|
86
|
+
recreate: async () => {
|
|
85
87
|
const { create } = await import('./create.ts');
|
|
86
|
-
await create([
|
|
88
|
+
await create([target.requestedRef, '--no-refresh']);
|
|
87
89
|
}
|
|
88
90
|
});
|
|
89
91
|
const message = ready.path === 'healthy'
|
package/lib/sandbox/config.ts
CHANGED
|
@@ -55,6 +55,8 @@ export type SandboxConfig = {
|
|
|
55
55
|
worktreeBase: string;
|
|
56
56
|
shareBase: string;
|
|
57
57
|
shellConfigBase: string;
|
|
58
|
+
workspaceViewBase: string;
|
|
59
|
+
controlBase: string;
|
|
58
60
|
dotfilesDir: string;
|
|
59
61
|
engine: string | null;
|
|
60
62
|
runtimes: string[];
|
|
@@ -188,6 +190,8 @@ export function loadConfig({
|
|
|
188
190
|
worktreeBase: hostJoin(home, '.agent-infra', 'worktrees', project),
|
|
189
191
|
shareBase: hostJoin(home, '.agent-infra', 'share', project),
|
|
190
192
|
shellConfigBase: hostJoin(home, '.agent-infra', 'config', project),
|
|
193
|
+
workspaceViewBase: hostJoin(home, '.agent-infra', 'workspace-views'),
|
|
194
|
+
controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'),
|
|
191
195
|
dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
|
|
192
196
|
engine,
|
|
193
197
|
runtimes,
|
package/lib/sandbox/constants.ts
CHANGED
|
@@ -103,6 +103,14 @@ export function sandboxBranchLabel(config: Pick<SandboxPathConfig, 'project'>):
|
|
|
103
103
|
return `${sandboxLabel(config)}.branch`;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
export function sandboxWorkspaceModeLabel(config: Pick<SandboxPathConfig, 'project'>): string {
|
|
107
|
+
return `${sandboxLabel(config)}.workspace-mode`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function sandboxTaskIdLabel(config: Pick<SandboxPathConfig, 'project'>): string {
|
|
111
|
+
return `${sandboxLabel(config)}.task-id`;
|
|
112
|
+
}
|
|
113
|
+
|
|
106
114
|
export function sandboxImageConfigLabel(config: Pick<SandboxPathConfig, 'project'>): string {
|
|
107
115
|
return `${sandboxLabel(config)}.image-config`;
|
|
108
116
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
SANDBOX_CONTROL_MAX_BYTES,
|
|
6
|
+
isSandboxControlFamily,
|
|
7
|
+
type SandboxControlRequest,
|
|
8
|
+
type SandboxControlResponse,
|
|
9
|
+
type SandboxTaskCreateRequest
|
|
10
|
+
} from './protocol.ts';
|
|
11
|
+
import type { TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
12
|
+
|
|
13
|
+
function sleep(milliseconds: number): void {
|
|
14
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly<{
|
|
18
|
+
channelDir?: string;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}>): SandboxControlResponse {
|
|
21
|
+
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
22
|
+
const encoded = `${JSON.stringify(request)}\n`;
|
|
23
|
+
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
24
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
25
|
+
}
|
|
26
|
+
const requestsDir = path.join(channelDir, 'requests');
|
|
27
|
+
const responsesDir = path.join(channelDir, 'responses');
|
|
28
|
+
const temporary = path.join(requestsDir, `.${request.id}.tmp`);
|
|
29
|
+
const requestPath = path.join(requestsDir, `${request.id}.json`);
|
|
30
|
+
const responsePath = path.join(responsesDir, `${request.id}.json`);
|
|
31
|
+
fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
|
|
32
|
+
fs.renameSync(temporary, requestPath);
|
|
33
|
+
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
34
|
+
while (Date.now() < deadline) {
|
|
35
|
+
if (fs.existsSync(responsePath)) {
|
|
36
|
+
const raw = fs.readFileSync(responsePath, 'utf8');
|
|
37
|
+
fs.unlinkSync(responsePath);
|
|
38
|
+
const response = JSON.parse(raw) as SandboxControlResponse;
|
|
39
|
+
if (response.version !== 1 || response.id !== request.id || !Number.isInteger(response.exitCode)) {
|
|
40
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
|
|
41
|
+
}
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
sleep(25);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function requestSandboxControl(params: Readonly<{
|
|
50
|
+
family: string;
|
|
51
|
+
args: string[];
|
|
52
|
+
channelDir?: string;
|
|
53
|
+
token?: string;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
}>): SandboxControlResponse {
|
|
56
|
+
if (!isSandboxControlFamily(params.family)) {
|
|
57
|
+
throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
|
|
58
|
+
}
|
|
59
|
+
if (params.family === 'task-create') throw new Error("SANDBOX_CONTROL_COMMAND_DENIED: 'task-create' requires a typed candidate");
|
|
60
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
61
|
+
if (!token) throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
62
|
+
const id = randomUUID();
|
|
63
|
+
const request: SandboxControlRequest = { version: 1, id, token, family: params.family, args: params.args };
|
|
64
|
+
return exchangeSandboxControl(request, params);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function requestSandboxTaskCreate(params: Readonly<{
|
|
68
|
+
candidate: TaskCreateCandidateV1;
|
|
69
|
+
channelDir?: string;
|
|
70
|
+
token?: string;
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
}>): SandboxControlResponse {
|
|
73
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
74
|
+
if (!token) throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
75
|
+
const request: SandboxTaskCreateRequest = {
|
|
76
|
+
version: 1, id: randomUUID(), token, family: 'task-create', candidate: params.candidate
|
|
77
|
+
};
|
|
78
|
+
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
79
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
2
|
+
|
|
3
|
+
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
4
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'] as const;
|
|
5
|
+
|
|
6
|
+
export type SandboxControlFamily = typeof SANDBOX_CONTROL_FAMILIES[number];
|
|
7
|
+
|
|
8
|
+
export type SandboxControlManifest = Readonly<{
|
|
9
|
+
version: 2;
|
|
10
|
+
repoRoot: string;
|
|
11
|
+
worktreeRoot: string;
|
|
12
|
+
project: string;
|
|
13
|
+
container: string;
|
|
14
|
+
branch: string;
|
|
15
|
+
mode: 'task-bound' | 'branch-only';
|
|
16
|
+
taskId: string | null;
|
|
17
|
+
token: string;
|
|
18
|
+
channelDir: string;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export type SandboxTaskCommandRequest = Readonly<{
|
|
22
|
+
version: 1;
|
|
23
|
+
id: string;
|
|
24
|
+
token: string;
|
|
25
|
+
family: 'task-lifecycle' | 'task-orchestration';
|
|
26
|
+
args: string[];
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type SandboxTaskCreateRequest = Readonly<{
|
|
30
|
+
version: 1;
|
|
31
|
+
id: string;
|
|
32
|
+
token: string;
|
|
33
|
+
family: 'task-create';
|
|
34
|
+
candidate: TaskCreateCandidateV1;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskCreateRequest;
|
|
38
|
+
|
|
39
|
+
export type SandboxControlResponse = Readonly<{
|
|
40
|
+
version: 1;
|
|
41
|
+
id: string;
|
|
42
|
+
exitCode: number;
|
|
43
|
+
stdout: string;
|
|
44
|
+
stderr: string;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
export function isSandboxControlFamily(value: string): value is SandboxControlFamily {
|
|
48
|
+
return SANDBOX_CONTROL_FAMILIES.includes(value as SandboxControlFamily);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function validateSandboxControlRequest(
|
|
52
|
+
value: unknown,
|
|
53
|
+
manifest: SandboxControlManifest
|
|
54
|
+
): SandboxControlRequest {
|
|
55
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
56
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request must be an object');
|
|
57
|
+
}
|
|
58
|
+
const request = value as Record<string, unknown>;
|
|
59
|
+
if (
|
|
60
|
+
request.version !== 1
|
|
61
|
+
|| typeof request.id !== 'string'
|
|
62
|
+
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
63
|
+
|| request.token !== manifest.token
|
|
64
|
+
|| typeof request.family !== 'string'
|
|
65
|
+
|| !isSandboxControlFamily(request.family)
|
|
66
|
+
) {
|
|
67
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
68
|
+
}
|
|
69
|
+
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
70
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
71
|
+
}
|
|
72
|
+
if (request.family === 'task-create') {
|
|
73
|
+
const keys = Object.keys(request).sort().join(',');
|
|
74
|
+
if (keys !== ['candidate', 'family', 'id', 'token', 'version'].sort().join(',')) {
|
|
75
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
76
|
+
}
|
|
77
|
+
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) } as SandboxTaskCreateRequest;
|
|
78
|
+
}
|
|
79
|
+
const keys = Object.keys(request).sort().join(',');
|
|
80
|
+
if (
|
|
81
|
+
keys !== ['args', 'family', 'id', 'token', 'version'].sort().join(',')
|
|
82
|
+
|| !Array.isArray(request.args)
|
|
83
|
+
|| !request.args.every((arg) => typeof arg === 'string')
|
|
84
|
+
) {
|
|
85
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
86
|
+
}
|
|
87
|
+
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
88
|
+
throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
request.family === 'task-orchestration'
|
|
92
|
+
&& request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))
|
|
93
|
+
) {
|
|
94
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: worktree binding is reserved for the control broker');
|
|
95
|
+
}
|
|
96
|
+
return request as SandboxTaskCommandRequest;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function bindSandboxControlTask(
|
|
100
|
+
request: SandboxControlRequest,
|
|
101
|
+
taskId: string
|
|
102
|
+
): string[] {
|
|
103
|
+
if (request.family === 'task-create') {
|
|
104
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: task-create requests do not bind a current task');
|
|
105
|
+
}
|
|
106
|
+
if (request.args.length === 0) {
|
|
107
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
|
|
108
|
+
}
|
|
109
|
+
return [taskId, ...request.args.slice(1)];
|
|
110
|
+
}
|