@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
|
@@ -3,12 +3,13 @@ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, san
|
|
|
3
3
|
import { detectEngine } from '../engine.js';
|
|
4
4
|
import { ensureSandboxReady } from '../recovery.js';
|
|
5
5
|
import { createSandboxCapabilityPlan, runBoundedSandboxHookCommand, runSandboxHooks } from '../agent-client-reconciler.js';
|
|
6
|
-
import { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
|
|
7
6
|
import { runInteractiveEngine } from '../shell.js';
|
|
8
7
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
|
|
8
|
+
import { redactCommandError } from '../redaction.js';
|
|
9
9
|
import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.js';
|
|
10
10
|
import { detectHostTimezone } from '../host-timezone.js';
|
|
11
|
-
import { fetchSandboxRows,
|
|
11
|
+
import { fetchSandboxRows, selectSandboxContainer, } from './list-running.js';
|
|
12
|
+
import { resolveSandboxTarget } from '../workspace-identity.js';
|
|
12
13
|
const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
|
|
13
14
|
|
|
14
15
|
N references an active task short id from
|
|
@@ -53,32 +54,6 @@ export function runSandboxInteractive(params) {
|
|
|
53
54
|
}
|
|
54
55
|
return runBridge({ engine, dockerArgs, container, home });
|
|
55
56
|
}
|
|
56
|
-
export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY, providerAuthAvailable = false) {
|
|
57
|
-
if (providerAuthAvailable && (result.status === 'STALE_ACCESS' || result.status === 'MISSING')) {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
if (result.status === 'STALE_ACCESS') {
|
|
61
|
-
return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
|
|
62
|
-
}
|
|
63
|
-
if (result.status === 'MISSING') {
|
|
64
|
-
return 'Warning: Claude Code credentials missing on host. Run "claude /login" to authenticate.\n';
|
|
65
|
-
}
|
|
66
|
-
if (result.status === 'KEYCHAIN_WRITE_FAILED') {
|
|
67
|
-
return `Warning: A sandbox refresh produced newer credentials but host Keychain write failed (${formatCredentialWarnings(result.warnings)}). Run "ai sandbox refresh" again or "claude /status" on the host to retry.\n`;
|
|
68
|
-
}
|
|
69
|
-
if (result.status === 'KEYCHAIN_LOCKED' || result.status === 'KEYCHAIN_ERROR') {
|
|
70
|
-
return 'Warning: Host keychain is unavailable; Claude credential sync skipped. Run "ai sandbox refresh" for details.\n';
|
|
71
|
-
}
|
|
72
|
-
if (result.status === 'OK' && result.authoritative !== 'host') {
|
|
73
|
-
const message = `Synced Claude Code credentials from sandbox refresh back to host (expires in ${formatRemaining(result.expiresAt)})`;
|
|
74
|
-
return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
|
|
75
|
-
}
|
|
76
|
-
if (result.status === 'OK' && result.filesWritten.length > 0) {
|
|
77
|
-
const message = `Synced Claude Code credentials from host Keychain (expires in ${formatRemaining(result.expiresAt)})`;
|
|
78
|
-
return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
57
|
export function parseEnterArgs(args) {
|
|
83
58
|
let recreate = false;
|
|
84
59
|
let index = 0;
|
|
@@ -101,10 +76,10 @@ export async function enter(args) {
|
|
|
101
76
|
return 0;
|
|
102
77
|
}
|
|
103
78
|
const config = loadConfig();
|
|
104
|
-
validateClaudeCredentialsEnvOverride();
|
|
105
79
|
const engine = detectEngine(config);
|
|
106
80
|
const parsed = parseEnterArgs(args);
|
|
107
|
-
const
|
|
81
|
+
const target = resolveSandboxTarget(parsed.target, config.repoRoot);
|
|
82
|
+
const branch = target.branch;
|
|
108
83
|
assertValidBranchName(branch);
|
|
109
84
|
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
110
85
|
const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
|
|
@@ -115,11 +90,12 @@ export async function enter(args) {
|
|
|
115
90
|
config,
|
|
116
91
|
engine,
|
|
117
92
|
branch,
|
|
93
|
+
workspace: target.workspace,
|
|
118
94
|
row: found,
|
|
119
95
|
allowRecreate: parsed.recreate,
|
|
120
|
-
recreate: async (
|
|
96
|
+
recreate: async () => {
|
|
121
97
|
const { create } = await import('./create.js');
|
|
122
|
-
await create([
|
|
98
|
+
await create([target.requestedRef, '--no-refresh']);
|
|
123
99
|
}
|
|
124
100
|
});
|
|
125
101
|
const container = ready.container;
|
|
@@ -128,7 +104,11 @@ export async function enter(args) {
|
|
|
128
104
|
const enterHookResults = await runSandboxHooks({
|
|
129
105
|
hooks: capabilityPlan.hooksByPhase['before-enter'],
|
|
130
106
|
phase: 'before-enter',
|
|
131
|
-
context: {
|
|
107
|
+
context: {
|
|
108
|
+
config,
|
|
109
|
+
plan: capabilityPlan,
|
|
110
|
+
enter: { hostHome: config.home, hostEnv: { ...process.env } }
|
|
111
|
+
},
|
|
132
112
|
runCommand: runBoundedSandboxHookCommand
|
|
133
113
|
});
|
|
134
114
|
for (const result of enterHookResults) {
|
|
@@ -138,20 +118,8 @@ export async function enter(args) {
|
|
|
138
118
|
if (result.status === 'warning') {
|
|
139
119
|
process.stderr.write(`Warning: ${result.message ?? `Sandbox hook '${result.hookId}' did not complete.`}\n`);
|
|
140
120
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
// Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
|
|
145
|
-
const providerAuthAvailable = hasClaudeProviderAuth(config.home);
|
|
146
|
-
const result = reconcileClaudeCredentials(config.home);
|
|
147
|
-
const message = formatCredentialSyncStatus(result, process.stderr.isTTY, providerAuthAvailable);
|
|
148
|
-
if (message) {
|
|
149
|
-
process.stderr.write(message);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
process.stderr.write(`Warning: Failed to sync Claude Code credentials: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
154
|
-
}
|
|
121
|
+
if (result.message && result.status !== 'warning')
|
|
122
|
+
process.stderr.write(result.message);
|
|
155
123
|
}
|
|
156
124
|
const envFlags = [...terminalEnvFlags(), ...hostTimezoneEnvFlags()];
|
|
157
125
|
if (cmd.length === 0) {
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { runSafeEngine, runVerboseEngine } from '../shell.js';
|
|
5
5
|
import { resolveTaskBranch } from '../task-resolver.js';
|
|
6
6
|
import { isRemovedHashShortIdInput } from '../../task/short-id.js';
|
|
7
|
+
import { parseSandboxWorkspaceIdentity } from '../workspace-identity.js';
|
|
7
8
|
export function containerListFormat() {
|
|
8
9
|
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
9
10
|
}
|
|
@@ -24,19 +25,28 @@ export function parseLabels(csv) {
|
|
|
24
25
|
}
|
|
25
26
|
return labels;
|
|
26
27
|
}
|
|
27
|
-
export function parseSandboxRows(rawOutput, branchKey) {
|
|
28
|
+
export function parseSandboxRows(rawOutput, branchKey, workspaceKeys) {
|
|
28
29
|
if (!rawOutput) {
|
|
29
30
|
return [];
|
|
30
31
|
}
|
|
31
32
|
return rawOutput.split('\n').map((line) => {
|
|
32
33
|
const [name = '', status = '', labelsCsv = ''] = line.split('\t');
|
|
33
|
-
const
|
|
34
|
+
const labels = parseLabels(labelsCsv);
|
|
35
|
+
const workspace = workspaceKeys
|
|
36
|
+
? parseSandboxWorkspaceIdentity(labels, workspaceKeys)
|
|
37
|
+
: null;
|
|
34
38
|
return {
|
|
35
39
|
name,
|
|
36
40
|
status,
|
|
37
|
-
branch,
|
|
41
|
+
branch: labels[branchKey] ?? '',
|
|
38
42
|
running: status.startsWith('Up '),
|
|
39
|
-
index: null
|
|
43
|
+
index: null,
|
|
44
|
+
...(workspace
|
|
45
|
+
? {
|
|
46
|
+
workspaceMode: workspace.mode,
|
|
47
|
+
taskId: workspace.mode === 'task-bound' ? workspace.taskId : null
|
|
48
|
+
}
|
|
49
|
+
: {})
|
|
40
50
|
};
|
|
41
51
|
});
|
|
42
52
|
}
|
|
@@ -58,7 +68,7 @@ export function sortAndIndexSandboxRows(rows) {
|
|
|
58
68
|
}));
|
|
59
69
|
return { running, nonRunning };
|
|
60
70
|
}
|
|
61
|
-
export function fetchSandboxRows(engine, label, branchKey) {
|
|
71
|
+
export function fetchSandboxRows(engine, label, branchKey, workspaceKeys) {
|
|
62
72
|
const raw = runSafeEngine(engine, 'docker', [
|
|
63
73
|
'ps',
|
|
64
74
|
'-a',
|
|
@@ -67,7 +77,7 @@ export function fetchSandboxRows(engine, label, branchKey) {
|
|
|
67
77
|
'--format',
|
|
68
78
|
containerListFormat()
|
|
69
79
|
]);
|
|
70
|
-
return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
|
|
80
|
+
return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey, workspaceKeys));
|
|
71
81
|
}
|
|
72
82
|
/**
|
|
73
83
|
* Returns true iff `arg` is a syntactically valid task short reference.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { loadConfig } from '../config.js';
|
|
4
|
-
import { sandboxBranchLabel, sandboxLabel } from '../constants.js';
|
|
4
|
+
import { sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel } from '../constants.js';
|
|
5
5
|
import { detectEngine } from '../engine.js';
|
|
6
6
|
import { formatTable } from '../../table.js';
|
|
7
|
-
import {
|
|
7
|
+
import { loadShortIdByTaskId } from '../../task/short-id.js';
|
|
8
8
|
import { fetchSandboxRows } from './list-running.js';
|
|
9
9
|
export { containerListFormat, parseLabels } from './list-running.js';
|
|
10
10
|
const USAGE = `Usage: ai sandbox ls
|
|
@@ -19,9 +19,9 @@ to remove with "ai sandbox rm <branch>".
|
|
|
19
19
|
|
|
20
20
|
Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
|
|
21
21
|
state paths.`;
|
|
22
|
-
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'];
|
|
22
|
+
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'WORKSPACE', 'TASK', 'BRANCH'];
|
|
23
23
|
export function formatContainerTable(rows, zebra = false) {
|
|
24
|
-
return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]), { zebra });
|
|
24
|
+
return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.workspace, r.taskId, r.branch]), { zebra });
|
|
25
25
|
}
|
|
26
26
|
export function ls(args = []) {
|
|
27
27
|
if (args.length > 0 && (args[0] === '--help' || args[0] === '-h')) {
|
|
@@ -31,7 +31,7 @@ export function ls(args = []) {
|
|
|
31
31
|
const config = loadConfig();
|
|
32
32
|
const engine = detectEngine(config);
|
|
33
33
|
const label = sandboxLabel(config);
|
|
34
|
-
const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
|
|
34
|
+
const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
|
|
35
35
|
p.intro(pc.cyan(`Sandbox status for ${config.project}`));
|
|
36
36
|
p.log.step('Containers');
|
|
37
37
|
const ordered = [...running, ...nonRunning];
|
|
@@ -40,12 +40,16 @@ export function ls(args = []) {
|
|
|
40
40
|
}
|
|
41
41
|
else {
|
|
42
42
|
const tableRows = ordered.map((container, i) => {
|
|
43
|
-
const shortId = container.
|
|
43
|
+
const shortId = container.taskId
|
|
44
|
+
? loadShortIdByTaskId(config.repoRoot).get(container.taskId) ?? null
|
|
45
|
+
: null;
|
|
44
46
|
return {
|
|
45
47
|
row: String(i + 1),
|
|
46
48
|
shortId: shortId ?? '-',
|
|
47
49
|
name: container.name,
|
|
48
50
|
status: container.status,
|
|
51
|
+
workspace: container.workspaceMode ?? 'legacy-invalid',
|
|
52
|
+
taskId: container.taskId ?? '-',
|
|
49
53
|
branch: container.branch
|
|
50
54
|
};
|
|
51
55
|
});
|
|
@@ -7,7 +7,7 @@ import { sandboxImageRefreshLabel } from '../constants.js';
|
|
|
7
7
|
import { detectEngine, ensureDocker } from '../engine.js';
|
|
8
8
|
import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.js';
|
|
9
9
|
import { pruneSandboxDanglingImages } from '../image-prune.js';
|
|
10
|
-
import {
|
|
10
|
+
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
|
|
11
11
|
import { buildImageSignature, buildSandboxImageArgs, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
|
|
12
12
|
import { assertBuildProxyCompatibility, buildProxyFailureHint, prepareBuildProxy, redactBuildProxyValues } from '../build-proxy.js';
|
|
13
13
|
const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh] [--inherit-build-proxy|-B]`;
|
|
@@ -52,8 +52,9 @@ export async function rebuild(args) {
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
const config = loadConfig();
|
|
55
|
-
const
|
|
56
|
-
const
|
|
55
|
+
const capabilityPlan = createSandboxCapabilityPlan(config);
|
|
56
|
+
const tools = [...capabilityPlan.tools];
|
|
57
|
+
const preparedDockerfile = prepareDockerfile(config, capabilityPlan.image);
|
|
57
58
|
const imageSignature = buildImageSignature(preparedDockerfile, tools);
|
|
58
59
|
const quiet = values.quiet ?? false;
|
|
59
60
|
const refresh = values.refresh ?? false;
|
|
@@ -1,85 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const USAGE = 'Usage: ai sandbox refresh';
|
|
6
|
-
export function probeClaudeStatus(spawnFn = runProbe) {
|
|
7
|
-
const result = spawnFn('claude', ['/status'], {
|
|
8
|
-
encoding: 'utf8',
|
|
9
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
10
|
-
timeout: 30_000
|
|
11
|
-
});
|
|
12
|
-
return {
|
|
13
|
-
ok: result.status === 0,
|
|
14
|
-
stderr: result.stderr?.toString() ?? '',
|
|
15
|
-
error: result.error?.message ?? null
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
export async function refresh(args, deps = {}) {
|
|
19
|
-
const { spawnFn = runProbe, execFn, readFn, existsFn, writeFn, writeHostFn, discoverFn = discoverProjects, writeStdout = (chunk) => process.stdout.write(chunk), writeStderr = (chunk) => process.stderr.write(chunk) } = deps;
|
|
20
|
-
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
21
|
-
writeStdout(`${USAGE}\n`);
|
|
22
|
-
return 0;
|
|
23
|
-
}
|
|
24
|
-
const { positionals } = parseArgs({ args, allowPositionals: true, strict: true });
|
|
25
|
-
if (positionals.length > 0) {
|
|
26
|
-
throw new Error(USAGE);
|
|
27
|
-
}
|
|
28
|
-
validateClaudeCredentialsEnvOverride();
|
|
29
|
-
const home = homedir();
|
|
30
|
-
if (!home) {
|
|
31
|
-
throw new Error('sandbox: home directory is required');
|
|
32
|
-
}
|
|
33
|
-
const projects = discoverFn(home);
|
|
34
|
-
if (projects.length === 0) {
|
|
35
|
-
writeStdout('No project credentials to refresh.\n');
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
const reconcileOptions = { execFn, readFn, existsFn, writeFn, writeHostFn, projects };
|
|
39
|
-
let result = reconcileClaudeCredentials(home, reconcileOptions);
|
|
40
|
-
if (result.status === 'STALE_ACCESS' && result.authoritative === null) {
|
|
41
|
-
writeStdout('Host credentials appear stale; probing claude /status to trigger refresh...\n');
|
|
42
|
-
const probe = probeClaudeStatus(spawnFn);
|
|
43
|
-
if (!probe.ok) {
|
|
44
|
-
writeStderr(`Probe failed: ${redactCommandError(probe.stderr || probe.error || 'unknown error')}\n`);
|
|
45
|
-
writeStderr('Run "claude /login" on the host to renew credentials.\n');
|
|
46
|
-
return 1;
|
|
47
|
-
}
|
|
48
|
-
writeStdout('Probe succeeded; re-inspecting host credentials.\n');
|
|
49
|
-
result = reconcileClaudeCredentials(home, reconcileOptions);
|
|
50
|
-
}
|
|
51
|
-
if (result.status === 'MISSING') {
|
|
52
|
-
writeStderr('No Claude Code credentials found on host.\n');
|
|
53
|
-
writeStderr('Run "claude /login" on the host to authenticate.\n');
|
|
54
|
-
return 1;
|
|
55
|
-
}
|
|
56
|
-
if (result.status === 'KEYCHAIN_LOCKED') {
|
|
57
|
-
writeStderr(`${buildLockedGuidance()}\n`);
|
|
58
|
-
return 1;
|
|
59
|
-
}
|
|
60
|
-
if (result.status === 'KEYCHAIN_ERROR') {
|
|
61
|
-
writeStderr(`Host keychain error: ${redactCommandError(result.detail || 'unknown error')}\n`);
|
|
62
|
-
writeStderr(`${buildLockedGuidance()}\n`);
|
|
63
|
-
return 1;
|
|
64
|
-
}
|
|
65
|
-
if (result.status === 'KEYCHAIN_WRITE_FAILED') {
|
|
66
|
-
writeStderr(`[host] keychain write failed: ${formatCredentialWarnings(result.warnings) || 'unknown error'}\n`);
|
|
67
|
-
return 1;
|
|
68
|
-
}
|
|
69
|
-
if (result.status !== 'OK') {
|
|
70
|
-
writeStderr('Host credentials still invalid after probe; run "claude /login".\n');
|
|
71
|
-
return 1;
|
|
72
|
-
}
|
|
73
|
-
if (result.authoritative && result.authoritative !== 'host' && result.hostWritten) {
|
|
74
|
-
writeStdout(`[host] reconciled from ${result.authoritative}\n`);
|
|
75
|
-
}
|
|
76
|
-
for (const project of projects) {
|
|
77
|
-
const action = result.filesWritten.includes(project) ? 'updated' : 'unchanged';
|
|
78
|
-
writeStdout(`[${project}] ${action}; expires in ${formatRemaining(result.expiresAt)}\n`);
|
|
79
|
-
}
|
|
80
|
-
for (const failure of result.fileErrors) {
|
|
81
|
-
writeStderr(`[${failure.project}] sync failed: ${failure.error}\n`);
|
|
82
|
-
}
|
|
83
|
-
return result.fileErrors.length > 0 ? 1 : 0;
|
|
84
|
-
}
|
|
1
|
+
import { runClaudeCodeRefresh } from '../../agent-clients/adapters/claude-code-refresh.js';
|
|
2
|
+
const refresh = runClaudeCodeRefresh;
|
|
3
|
+
export { probeClaudeStatus, runClaudeCodeRefresh } from '../../agent-clients/adapters/claude-code-refresh.js';
|
|
4
|
+
export { refresh };
|
|
85
5
|
//# sourceMappingURL=refresh.js.map
|
|
@@ -9,7 +9,7 @@ import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedV
|
|
|
9
9
|
import { pruneSandboxDanglingImages } from '../image-prune.js';
|
|
10
10
|
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
|
|
11
11
|
import { runOk, runSafe, runSafeEngine } from '../shell.js';
|
|
12
|
-
import {
|
|
12
|
+
import { resolveSandboxTarget } from '../workspace-identity.js';
|
|
13
13
|
import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
|
|
14
14
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
|
|
15
15
|
import { fetchSandboxRows } from './list-running.js';
|
|
@@ -57,6 +57,17 @@ async function rmOne(config, tools, branch, options = {}) {
|
|
|
57
57
|
for (const name of matchedContainers) {
|
|
58
58
|
runSafeEngine(engine, 'docker', ['stop', name]);
|
|
59
59
|
runSafeEngine(engine, 'docker', ['rm', name]);
|
|
60
|
+
for (const [base, label] of [
|
|
61
|
+
[path.join(config.workspaceViewBase, config.project), 'Workspace view'],
|
|
62
|
+
[path.join(config.controlBase, config.project), 'Control channel']
|
|
63
|
+
]) {
|
|
64
|
+
const directory = path.join(base, name);
|
|
65
|
+
if (fs.existsSync(directory)) {
|
|
66
|
+
removeManagedDir(base, directory);
|
|
67
|
+
if (!options.quiet)
|
|
68
|
+
p.log.success(`${label} removed: ${directory}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
60
71
|
}
|
|
61
72
|
spinner.stop(pc.green(`Removed container(s): ${matchedContainers.join(', ')}`));
|
|
62
73
|
}
|
|
@@ -184,6 +195,16 @@ async function rmPurge(config, tools) {
|
|
|
184
195
|
p.log.success(`Project share dirs removed: ${config.shareBase}`);
|
|
185
196
|
}
|
|
186
197
|
}
|
|
198
|
+
for (const [base, label] of [
|
|
199
|
+
[config.workspaceViewBase, 'workspace views'],
|
|
200
|
+
[config.controlBase, 'control channels']
|
|
201
|
+
]) {
|
|
202
|
+
const projectDir = path.join(base, config.project);
|
|
203
|
+
if (fs.existsSync(projectDir)) {
|
|
204
|
+
removeManagedDir(base, projectDir);
|
|
205
|
+
p.log.success(`Removed project ${label}: ${projectDir}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
187
208
|
const shouldRemoveImage = await p.confirm({
|
|
188
209
|
message: `Remove image ${config.imageName}?`,
|
|
189
210
|
initialValue: false
|
|
@@ -297,7 +318,7 @@ export async function rm(args) {
|
|
|
297
318
|
});
|
|
298
319
|
return;
|
|
299
320
|
}
|
|
300
|
-
const
|
|
301
|
-
await rmOne(config, tools, branch);
|
|
321
|
+
const target = resolveSandboxTarget(positionals[0] ?? '', config.repoRoot);
|
|
322
|
+
await rmOne(config, tools, target.branch);
|
|
302
323
|
}
|
|
303
324
|
//# sourceMappingURL=rm.js.map
|
|
@@ -2,9 +2,11 @@ import fs from 'node:fs';
|
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
|
-
import { assertValidBranchName, worktreeDirCandidates } from '../constants.js';
|
|
6
|
-
import {
|
|
5
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel, worktreeDirCandidates } from '../constants.js';
|
|
6
|
+
import { fetchSandboxRows, selectSandboxContainer } from './list-running.js';
|
|
7
7
|
import { resolveTools, toolConfigDirCandidates } from '../tools.js';
|
|
8
|
+
import { detectEngine } from '../engine.js';
|
|
9
|
+
import { resolveSandboxTarget } from '../workspace-identity.js';
|
|
8
10
|
const USAGE = `Usage: ai sandbox show <branch | TASK-id | N>
|
|
9
11
|
|
|
10
12
|
Shows one sandbox's worktree path and per-tool state paths (Claude Code,
|
|
@@ -40,9 +42,13 @@ export function show(args = []) {
|
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
const config = loadConfig();
|
|
43
|
-
const
|
|
45
|
+
const target = resolveSandboxTarget(args[0], config.repoRoot);
|
|
46
|
+
const branch = target.branch;
|
|
44
47
|
assertValidBranchName(branch);
|
|
45
48
|
const detail = collectSandboxDetail(config, branch);
|
|
49
|
+
const engine = detectEngine(config);
|
|
50
|
+
const rows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
|
|
51
|
+
const container = selectSandboxContainer([...rows.running, ...rows.nonRunning], containerNameCandidates(config, branch));
|
|
46
52
|
p.intro(pc.cyan(`Sandbox detail for ${config.project} · ${branch}`));
|
|
47
53
|
p.log.step('Worktree');
|
|
48
54
|
if (detail.worktrees.length === 0) {
|
|
@@ -53,6 +59,9 @@ export function show(args = []) {
|
|
|
53
59
|
process.stdout.write(` ${worktree}\n`);
|
|
54
60
|
}
|
|
55
61
|
}
|
|
62
|
+
p.log.step('Workspace identity');
|
|
63
|
+
process.stdout.write(` Mode: ${container?.workspaceMode ?? 'legacy-invalid'}\n`);
|
|
64
|
+
process.stdout.write(` Task: ${container?.taskId ?? '-'}\n`);
|
|
56
65
|
for (const tool of detail.toolStates) {
|
|
57
66
|
p.log.step(`${tool.name} state`);
|
|
58
67
|
if (tool.entries.length === 0) {
|
|
@@ -2,7 +2,8 @@ import { loadConfig } from '../config.js';
|
|
|
2
2
|
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
|
|
3
3
|
import { detectEngine } from '../engine.js';
|
|
4
4
|
import { ensureSandboxReady } from '../recovery.js';
|
|
5
|
-
import { fetchSandboxRows,
|
|
5
|
+
import { fetchSandboxRows, selectSandboxContainer, } from './list-running.js';
|
|
6
|
+
import { resolveSandboxTarget } from '../workspace-identity.js';
|
|
6
7
|
const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
|
|
7
8
|
|
|
8
9
|
Start an existing sandbox container that has stopped (for example after the
|
|
@@ -46,7 +47,8 @@ export async function start(args) {
|
|
|
46
47
|
}
|
|
47
48
|
const config = loadConfig();
|
|
48
49
|
const engine = detectEngine(config);
|
|
49
|
-
const
|
|
50
|
+
const target = resolveSandboxTarget(parsed.target, config.repoRoot);
|
|
51
|
+
const branch = target.branch;
|
|
50
52
|
assertValidBranchName(branch);
|
|
51
53
|
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
52
54
|
const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
|
|
@@ -57,11 +59,12 @@ export async function start(args) {
|
|
|
57
59
|
config,
|
|
58
60
|
engine,
|
|
59
61
|
branch,
|
|
62
|
+
workspace: target.workspace,
|
|
60
63
|
row: found,
|
|
61
64
|
allowRecreate: parsed.recreate,
|
|
62
|
-
recreate: async (
|
|
65
|
+
recreate: async () => {
|
|
63
66
|
const { create } = await import('./create.js');
|
|
64
|
-
await create([
|
|
67
|
+
await create([target.requestedRef, '--no-refresh']);
|
|
65
68
|
}
|
|
66
69
|
});
|
|
67
70
|
const message = ready.path === 'healthy'
|
|
@@ -111,6 +111,8 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
|
|
|
111
111
|
worktreeBase: hostJoin(home, '.agent-infra', 'worktrees', project),
|
|
112
112
|
shareBase: hostJoin(home, '.agent-infra', 'share', project),
|
|
113
113
|
shellConfigBase: hostJoin(home, '.agent-infra', 'config', project),
|
|
114
|
+
workspaceViewBase: hostJoin(home, '.agent-infra', 'workspace-views'),
|
|
115
|
+
controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'),
|
|
114
116
|
dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
|
|
115
117
|
engine,
|
|
116
118
|
runtimes,
|
|
@@ -69,6 +69,12 @@ export function sandboxLabel(config) {
|
|
|
69
69
|
export function sandboxBranchLabel(config) {
|
|
70
70
|
return `${sandboxLabel(config)}.branch`;
|
|
71
71
|
}
|
|
72
|
+
export function sandboxWorkspaceModeLabel(config) {
|
|
73
|
+
return `${sandboxLabel(config)}.workspace-mode`;
|
|
74
|
+
}
|
|
75
|
+
export function sandboxTaskIdLabel(config) {
|
|
76
|
+
return `${sandboxLabel(config)}.task-id`;
|
|
77
|
+
}
|
|
72
78
|
export function sandboxImageConfigLabel(config) {
|
|
73
79
|
return `${sandboxLabel(config)}.image-config`;
|
|
74
80
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { SANDBOX_CONTROL_MAX_BYTES, isSandboxControlFamily } from './protocol.js';
|
|
5
|
+
function sleep(milliseconds) {
|
|
6
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
7
|
+
}
|
|
8
|
+
function exchangeSandboxControl(request, params) {
|
|
9
|
+
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
10
|
+
const encoded = `${JSON.stringify(request)}\n`;
|
|
11
|
+
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
12
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
13
|
+
}
|
|
14
|
+
const requestsDir = path.join(channelDir, 'requests');
|
|
15
|
+
const responsesDir = path.join(channelDir, 'responses');
|
|
16
|
+
const temporary = path.join(requestsDir, `.${request.id}.tmp`);
|
|
17
|
+
const requestPath = path.join(requestsDir, `${request.id}.json`);
|
|
18
|
+
const responsePath = path.join(responsesDir, `${request.id}.json`);
|
|
19
|
+
fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
|
|
20
|
+
fs.renameSync(temporary, requestPath);
|
|
21
|
+
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
22
|
+
while (Date.now() < deadline) {
|
|
23
|
+
if (fs.existsSync(responsePath)) {
|
|
24
|
+
const raw = fs.readFileSync(responsePath, 'utf8');
|
|
25
|
+
fs.unlinkSync(responsePath);
|
|
26
|
+
const response = JSON.parse(raw);
|
|
27
|
+
if (response.version !== 1 || response.id !== request.id || !Number.isInteger(response.exitCode)) {
|
|
28
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
|
|
29
|
+
}
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
sleep(25);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
|
|
35
|
+
}
|
|
36
|
+
export function requestSandboxControl(params) {
|
|
37
|
+
if (!isSandboxControlFamily(params.family)) {
|
|
38
|
+
throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
|
|
39
|
+
}
|
|
40
|
+
if (params.family === 'task-create')
|
|
41
|
+
throw new Error("SANDBOX_CONTROL_COMMAND_DENIED: 'task-create' requires a typed candidate");
|
|
42
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
43
|
+
if (!token)
|
|
44
|
+
throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
45
|
+
const id = randomUUID();
|
|
46
|
+
const request = { version: 1, id, token, family: params.family, args: params.args };
|
|
47
|
+
return exchangeSandboxControl(request, params);
|
|
48
|
+
}
|
|
49
|
+
export function requestSandboxTaskCreate(params) {
|
|
50
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
51
|
+
if (!token)
|
|
52
|
+
throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
53
|
+
const request = {
|
|
54
|
+
version: 1, id: randomUUID(), token, family: 'task-create', candidate: params.candidate
|
|
55
|
+
};
|
|
56
|
+
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { validateTaskCreateCandidate } from '../../task/create.js';
|
|
2
|
+
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
3
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
|
|
4
|
+
export function isSandboxControlFamily(value) {
|
|
5
|
+
return SANDBOX_CONTROL_FAMILIES.includes(value);
|
|
6
|
+
}
|
|
7
|
+
export function validateSandboxControlRequest(value, manifest) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
9
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request must be an object');
|
|
10
|
+
}
|
|
11
|
+
const request = value;
|
|
12
|
+
if (request.version !== 1
|
|
13
|
+
|| typeof request.id !== 'string'
|
|
14
|
+
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
15
|
+
|| request.token !== manifest.token
|
|
16
|
+
|| typeof request.family !== 'string'
|
|
17
|
+
|| !isSandboxControlFamily(request.family)) {
|
|
18
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
19
|
+
}
|
|
20
|
+
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
21
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
22
|
+
}
|
|
23
|
+
if (request.family === 'task-create') {
|
|
24
|
+
const keys = Object.keys(request).sort().join(',');
|
|
25
|
+
if (keys !== ['candidate', 'family', 'id', 'token', 'version'].sort().join(',')) {
|
|
26
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
27
|
+
}
|
|
28
|
+
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
|
|
29
|
+
}
|
|
30
|
+
const keys = Object.keys(request).sort().join(',');
|
|
31
|
+
if (keys !== ['args', 'family', 'id', 'token', 'version'].sort().join(',')
|
|
32
|
+
|| !Array.isArray(request.args)
|
|
33
|
+
|| !request.args.every((arg) => typeof arg === 'string')) {
|
|
34
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
35
|
+
}
|
|
36
|
+
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
37
|
+
throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
|
|
38
|
+
}
|
|
39
|
+
if (request.family === 'task-orchestration'
|
|
40
|
+
&& request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
|
|
41
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: worktree binding is reserved for the control broker');
|
|
42
|
+
}
|
|
43
|
+
return request;
|
|
44
|
+
}
|
|
45
|
+
export function bindSandboxControlTask(request, taskId) {
|
|
46
|
+
if (request.family === 'task-create') {
|
|
47
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: task-create requests do not bind a current task');
|
|
48
|
+
}
|
|
49
|
+
if (request.args.length === 0) {
|
|
50
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
|
|
51
|
+
}
|
|
52
|
+
return [taskId, ...request.args.slice(1)];
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=protocol.js.map
|