@fitlab-ai/agent-infra 0.9.3 → 0.9.4
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 +2 -0
- package/README.zh-CN.md +2 -0
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +134 -123
- package/dist/lib/agent-clients/adapter.js +92 -2
- 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/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/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- 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 +122 -18
- 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 +109 -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 +44 -0
- package/dist/lib/sandbox/control/protocol.js +35 -0
- package/dist/lib/sandbox/control/server.js +109 -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 +103 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/commit-finalization.js +267 -0
- package/dist/lib/task/commit-intent.js +165 -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 +363 -28
- package/dist/lib/task/resolve-ref.js +41 -1
- package/lib/agent-clients/adapter.ts +126 -2
- 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/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/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +81 -11
- 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 +126 -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 +55 -0
- package/lib/sandbox/control/protocol.ts +75 -0
- package/lib/sandbox/control/server.ts +117 -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 +143 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/commit-finalization.ts +356 -0
- package/lib/task/commit-intent.ts +230 -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 +501 -33
- package/lib/task/resolve-ref.ts +46 -1
- package/package.json +1 -1
- 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 +2 -0
- package/templates/.agents/rules/task-management.zh-CN.md +2 -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 -11
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -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/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/.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,55 @@
|
|
|
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
|
+
} from './protocol.ts';
|
|
10
|
+
|
|
11
|
+
function sleep(milliseconds: number): void {
|
|
12
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function requestSandboxControl(params: Readonly<{
|
|
16
|
+
family: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
channelDir?: string;
|
|
19
|
+
token?: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}>): SandboxControlResponse {
|
|
22
|
+
if (!isSandboxControlFamily(params.family)) {
|
|
23
|
+
throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
|
|
24
|
+
}
|
|
25
|
+
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
26
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
27
|
+
if (!token) throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
28
|
+
const requestsDir = path.join(channelDir, 'requests');
|
|
29
|
+
const responsesDir = path.join(channelDir, 'responses');
|
|
30
|
+
const id = randomUUID();
|
|
31
|
+
const request: SandboxControlRequest = { version: 1, id, token, family: params.family, args: params.args };
|
|
32
|
+
const encoded = `${JSON.stringify(request)}\n`;
|
|
33
|
+
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
34
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
35
|
+
}
|
|
36
|
+
const temporary = path.join(requestsDir, `.${id}.tmp`);
|
|
37
|
+
const requestPath = path.join(requestsDir, `${id}.json`);
|
|
38
|
+
const responsePath = path.join(responsesDir, `${id}.json`);
|
|
39
|
+
fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
|
|
40
|
+
fs.renameSync(temporary, requestPath);
|
|
41
|
+
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
if (fs.existsSync(responsePath)) {
|
|
44
|
+
const raw = fs.readFileSync(responsePath, 'utf8');
|
|
45
|
+
fs.unlinkSync(responsePath);
|
|
46
|
+
const response = JSON.parse(raw) as SandboxControlResponse;
|
|
47
|
+
if (response.version !== 1 || response.id !== id || !Number.isInteger(response.exitCode)) {
|
|
48
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
|
|
49
|
+
}
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
sleep(25);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
2
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration'] as const;
|
|
3
|
+
|
|
4
|
+
export type SandboxControlFamily = typeof SANDBOX_CONTROL_FAMILIES[number];
|
|
5
|
+
|
|
6
|
+
export type SandboxControlManifest = Readonly<{
|
|
7
|
+
version: 1;
|
|
8
|
+
repoRoot: string;
|
|
9
|
+
project: string;
|
|
10
|
+
container: string;
|
|
11
|
+
branch: string;
|
|
12
|
+
mode: 'task-bound' | 'branch-only';
|
|
13
|
+
taskId: string | null;
|
|
14
|
+
token: string;
|
|
15
|
+
channelDir: string;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type SandboxControlRequest = Readonly<{
|
|
19
|
+
version: 1;
|
|
20
|
+
id: string;
|
|
21
|
+
token: string;
|
|
22
|
+
family: SandboxControlFamily;
|
|
23
|
+
args: string[];
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export type SandboxControlResponse = Readonly<{
|
|
27
|
+
version: 1;
|
|
28
|
+
id: string;
|
|
29
|
+
exitCode: number;
|
|
30
|
+
stdout: string;
|
|
31
|
+
stderr: string;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export function isSandboxControlFamily(value: string): value is SandboxControlFamily {
|
|
35
|
+
return SANDBOX_CONTROL_FAMILIES.includes(value as SandboxControlFamily);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateSandboxControlRequest(
|
|
39
|
+
value: unknown,
|
|
40
|
+
manifest: SandboxControlManifest
|
|
41
|
+
): SandboxControlRequest {
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
43
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request must be an object');
|
|
44
|
+
}
|
|
45
|
+
const request = value as Partial<SandboxControlRequest>;
|
|
46
|
+
if (
|
|
47
|
+
request.version !== 1
|
|
48
|
+
|| typeof request.id !== 'string'
|
|
49
|
+
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
50
|
+
|| request.token !== manifest.token
|
|
51
|
+
|| typeof request.family !== 'string'
|
|
52
|
+
|| !isSandboxControlFamily(request.family)
|
|
53
|
+
|| !Array.isArray(request.args)
|
|
54
|
+
|| !request.args.every((arg) => typeof arg === 'string')
|
|
55
|
+
) {
|
|
56
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
57
|
+
}
|
|
58
|
+
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
59
|
+
throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
|
|
60
|
+
}
|
|
61
|
+
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
62
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
63
|
+
}
|
|
64
|
+
return request as SandboxControlRequest;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function bindSandboxControlTask(
|
|
68
|
+
request: SandboxControlRequest,
|
|
69
|
+
taskId: string
|
|
70
|
+
): string[] {
|
|
71
|
+
if (request.args.length === 0) {
|
|
72
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
|
|
73
|
+
}
|
|
74
|
+
return [taskId, ...request.args.slice(1)];
|
|
75
|
+
}
|