@fitlab-ai/agent-infra 0.8.3 → 0.8.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.zh-CN.md +10 -0
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/lib/cp.js +56 -15
- package/dist/lib/defaults.json +9 -0
- package/dist/lib/init.js +5 -0
- package/dist/lib/sandbox/capture.js +5 -3
- package/dist/lib/sandbox/clipboard/bridge.js +12 -9
- package/dist/lib/sandbox/clipboard/inbox.js +82 -0
- package/dist/lib/sandbox/commands/create.js +68 -26
- package/dist/lib/sandbox/engine.js +33 -3
- package/dist/lib/sandbox/engines/colima.js +2 -2
- package/dist/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/dist/lib/server/daemon.js +9 -0
- package/dist/lib/server/process-control.js +66 -73
- package/dist/lib/server/process-state.js +168 -0
- package/dist/lib/update.js +2 -0
- package/lib/cp.ts +56 -15
- package/lib/defaults.json +9 -0
- package/lib/init.ts +9 -0
- package/lib/sandbox/capture.ts +5 -3
- package/lib/sandbox/clipboard/bridge.ts +14 -12
- package/lib/sandbox/clipboard/inbox.ts +83 -0
- package/lib/sandbox/commands/create.ts +80 -27
- package/lib/sandbox/engine.ts +37 -3
- package/lib/sandbox/engines/colima.ts +2 -2
- package/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- package/lib/server/daemon.ts +9 -0
- package/lib/server/process-control.ts +70 -71
- package/lib/server/process-state.ts +183 -0
- package/lib/update.ts +6 -0
- package/package.json +2 -1
- package/runtime/platform-adapters/platform-sync.github.js +1501 -0
- package/templates/.agents/README.en.md +5 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +5 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -4
- package/templates/.agents/rules/testing-discipline.en.md +3 -41
- package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
- package/templates/.agents/scripts/lib/agent-infra-package.js +149 -0
- package/templates/.agents/scripts/lib/post-review-commit.js +5 -0
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +25 -1459
- package/templates/.agents/scripts/review-diff-fingerprint.js +95 -24
- package/templates/.agents/scripts/validate-artifact.js +144 -38
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- 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/code-task/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +6 -4
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +35 -8
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +35 -8
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +27 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +27 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- 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-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +5 -4
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +6 -5
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +170 -45
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +203 -142
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
pruneClipboardDir,
|
|
10
10
|
writeClipboardPngAtomic
|
|
11
11
|
} from './paths.ts';
|
|
12
|
+
import { isClipboardInboxReadable, readPendingClipboardPath } from './inbox.ts';
|
|
12
13
|
import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from '../shell.ts';
|
|
13
14
|
import { loadNodePty, type NodePty, type PtyProcess } from './node-pty.ts';
|
|
14
15
|
|
|
@@ -85,13 +86,9 @@ export async function runInteractiveWithClipboardBridge(options: BridgeOptions):
|
|
|
85
86
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
86
87
|
return fallback('host stdin/stdout is not a TTY');
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const available = adapter.available();
|
|
92
|
-
if (!available.ok) {
|
|
93
|
-
return fallback(available.reason);
|
|
94
|
-
}
|
|
89
|
+
const available = adapter?.available() ?? { ok: false as const, reason: 'no clipboard adapter available on this platform' };
|
|
90
|
+
const inboxReady = isClipboardInboxReadable(home);
|
|
91
|
+
if (!available.ok && !inboxReady) return fallback(available.reason);
|
|
95
92
|
if (!runOk(engine, 'docker', ['exec', container, 'sh', '-c', '[ -d /clipboard ] && [ -r /clipboard ]'])) {
|
|
96
93
|
return fallback('container /clipboard mount is missing; rebuild the sandbox to enable image paste');
|
|
97
94
|
}
|
|
@@ -119,7 +116,7 @@ export async function runInteractiveWithClipboardBridge(options: BridgeOptions):
|
|
|
119
116
|
return runBridge({
|
|
120
117
|
child,
|
|
121
118
|
home,
|
|
122
|
-
adapter,
|
|
119
|
+
adapter: available.ok ? adapter : null,
|
|
123
120
|
writeStderr,
|
|
124
121
|
stdin,
|
|
125
122
|
stdout,
|
|
@@ -138,7 +135,7 @@ async function runBridge({
|
|
|
138
135
|
}: {
|
|
139
136
|
child: PtyProcess;
|
|
140
137
|
home: string;
|
|
141
|
-
adapter: ClipboardAdapter;
|
|
138
|
+
adapter: ClipboardAdapter | null;
|
|
142
139
|
writeStderr: (chunk: string) => unknown;
|
|
143
140
|
stdin: NodeJS.ReadStream;
|
|
144
141
|
stdout: NodeJS.WriteStream;
|
|
@@ -182,9 +179,9 @@ async function runBridge({
|
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
function handleText(raw: string, target: PtyProcess): void {
|
|
185
|
-
const textAdapter = adapter as TextImageClipboardAdapter;
|
|
182
|
+
const textAdapter = adapter as TextImageClipboardAdapter | null;
|
|
186
183
|
const pastedText = extractSinglePathPaste(raw);
|
|
187
|
-
if (!textAdapter.readImageFromText || pastedText === null) {
|
|
184
|
+
if (!textAdapter || !textAdapter.readImageFromText || pastedText === null) {
|
|
188
185
|
target.write(raw);
|
|
189
186
|
return;
|
|
190
187
|
}
|
|
@@ -207,12 +204,17 @@ async function runBridge({
|
|
|
207
204
|
|
|
208
205
|
function handleCtrlV(match: CtrlVMatch, target: PtyProcess): void {
|
|
209
206
|
try {
|
|
207
|
+
const pendingPath = readPendingClipboardPath(home);
|
|
208
|
+
if (pendingPath) {
|
|
209
|
+
target.write(buildBracketedPaste(pendingPath));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
210
212
|
// readImagePng returns null both for "no image on clipboard" and for
|
|
211
213
|
// unexpected read failures; both cases forward the original Ctrl+V so
|
|
212
214
|
// the container app handles it as a regular keystroke. The throw branch
|
|
213
215
|
// below only fires on truly unexpected exceptions (e.g. fs write
|
|
214
216
|
// errors writing to the host clipboard dir).
|
|
215
|
-
const png = adapter
|
|
217
|
+
const png = adapter?.readImagePng() ?? null;
|
|
216
218
|
if (!png) {
|
|
217
219
|
target.write(match.raw);
|
|
218
220
|
return;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
clipboardHostDir,
|
|
5
|
+
containerClipboardPath,
|
|
6
|
+
pngClipboardFilename,
|
|
7
|
+
pruneClipboardDir,
|
|
8
|
+
writeClipboardPngAtomic
|
|
9
|
+
} from './paths.ts';
|
|
10
|
+
|
|
11
|
+
export const RECEIVER_CAPABILITY = 'AGENT_INFRA_CLIPBOARD_RECEIVER:v1';
|
|
12
|
+
const MARKER = '.pending.json';
|
|
13
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
14
|
+
const HASHED_PNG = /^[a-f0-9]{16}\.png$/u;
|
|
15
|
+
const REMOTE_TEMP = /^agent-infra-cp-[0-9a-f-]{36}\.png$/u;
|
|
16
|
+
|
|
17
|
+
export function isClipboardInboxReadable(home: string): boolean {
|
|
18
|
+
const dir = clipboardHostDir(home);
|
|
19
|
+
try {
|
|
20
|
+
fs.accessSync(dir, fs.constants.R_OK);
|
|
21
|
+
return fs.statSync(dir).isDirectory();
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function receiveRemoteClipboardPng(home: string, tempPath: string, platformName: NodeJS.Platform = process.platform): string {
|
|
28
|
+
if (platformName !== 'linux') throw new Error('remote clipboard receiver requires Linux');
|
|
29
|
+
if (path.dirname(tempPath) !== '/tmp' || !REMOTE_TEMP.test(path.basename(tempPath))) {
|
|
30
|
+
throw new Error('invalid clipboard upload path');
|
|
31
|
+
}
|
|
32
|
+
const signature = Buffer.alloc(PNG_SIGNATURE.length);
|
|
33
|
+
const fd = fs.openSync(tempPath, 'r');
|
|
34
|
+
let signatureBytes: number;
|
|
35
|
+
try {
|
|
36
|
+
signatureBytes = fs.readSync(fd, signature, 0, signature.length, 0);
|
|
37
|
+
} finally {
|
|
38
|
+
fs.closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
if (signatureBytes !== PNG_SIGNATURE.length || !signature.equals(PNG_SIGNATURE)) {
|
|
41
|
+
throw new Error('uploaded clipboard file is not a PNG');
|
|
42
|
+
}
|
|
43
|
+
const png = fs.readFileSync(tempPath);
|
|
44
|
+
const dir = clipboardHostDir(home);
|
|
45
|
+
if (!fs.existsSync(path.dirname(dir))) throw new Error('agent-infra is not installed on this host');
|
|
46
|
+
const filename = pngClipboardFilename(png);
|
|
47
|
+
writeClipboardPngAtomic(dir, filename, png);
|
|
48
|
+
pruneClipboardDir(dir);
|
|
49
|
+
const markerTmp = path.join(dir, `.${MARKER}.${process.pid}.tmp`);
|
|
50
|
+
fs.writeFileSync(markerTmp, JSON.stringify({ version: 1, filename, createdAt: new Date().toISOString() }) + '\n', { mode: 0o600 });
|
|
51
|
+
fs.renameSync(markerTmp, path.join(dir, MARKER));
|
|
52
|
+
return containerClipboardPath(filename);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readPendingClipboardPath(home: string): string | null {
|
|
56
|
+
const dir = clipboardHostDir(home);
|
|
57
|
+
const markerPath = path.join(dir, MARKER);
|
|
58
|
+
let markerText: string;
|
|
59
|
+
try {
|
|
60
|
+
markerText = fs.readFileSync(markerPath, 'utf8');
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
63
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let marker: { version?: unknown; filename?: unknown };
|
|
67
|
+
try {
|
|
68
|
+
marker = JSON.parse(markerText) as { version?: unknown; filename?: unknown };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
71
|
+
}
|
|
72
|
+
if (marker.version !== 1 || typeof marker.filename !== 'string' || !HASHED_PNG.test(marker.filename)) {
|
|
73
|
+
throw new Error('invalid clipboard pending marker');
|
|
74
|
+
}
|
|
75
|
+
const filePath = path.join(dir, marker.filename);
|
|
76
|
+
try {
|
|
77
|
+
const stat = fs.lstatSync(filePath);
|
|
78
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('pending clipboard image is not a regular file');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
81
|
+
}
|
|
82
|
+
return containerClipboardPath(marker.filename);
|
|
83
|
+
}
|
|
@@ -87,17 +87,36 @@ alias gy='gemini --yolo; tput ed'
|
|
|
87
87
|
`;
|
|
88
88
|
const CONTAINER_HOME = '/home/devuser';
|
|
89
89
|
const CONTAINER_SHELL_CONFIG_MOUNT = `${CONTAINER_HOME}/.host-shell-config`;
|
|
90
|
-
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh]
|
|
90
|
+
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh] [--inherit-proxy|-P]
|
|
91
91
|
|
|
92
92
|
Host aliases:
|
|
93
93
|
${'~'}/.agent-infra/aliases/sandbox.sh is auto-created on first run and exposed
|
|
94
94
|
as ${CONTAINER_HOME}/.bash_aliases inside the sandbox container (the host
|
|
95
95
|
shell-config directory is bind-mounted at ${CONTAINER_SHELL_CONFIG_MOUNT} and
|
|
96
|
-
symlinked into $HOME)
|
|
96
|
+
symlinked into $HOME).
|
|
97
|
+
|
|
98
|
+
Proxy:
|
|
99
|
+
--inherit-proxy, -P Copy non-empty standard host proxy variables into the
|
|
100
|
+
container environment through the private env file.`;
|
|
101
|
+
const HOST_PROXY_ENV_KEYS = [
|
|
102
|
+
'http_proxy',
|
|
103
|
+
'HTTP_PROXY',
|
|
104
|
+
'https_proxy',
|
|
105
|
+
'HTTPS_PROXY',
|
|
106
|
+
'all_proxy',
|
|
107
|
+
'ALL_PROXY',
|
|
108
|
+
'no_proxy',
|
|
109
|
+
'NO_PROXY'
|
|
110
|
+
] as const;
|
|
97
111
|
|
|
98
112
|
type SandboxCreateConfig = ReturnType<typeof loadConfig>;
|
|
99
113
|
type PreparedDockerfile = ReturnType<typeof prepareDockerfile>;
|
|
100
114
|
type ResolvedTool = { tool: SandboxTool; dir: string };
|
|
115
|
+
type TmpfsSeedPlanEntry = {
|
|
116
|
+
stagingPath: string;
|
|
117
|
+
targetPath: string;
|
|
118
|
+
volumeArgs: string[];
|
|
119
|
+
};
|
|
101
120
|
type RuntimeCheck = { name: string; cmd: string[] };
|
|
102
121
|
type JsonObject = Record<string, unknown>;
|
|
103
122
|
type GpgCache = { pub: Buffer; sec: Buffer } | null;
|
|
@@ -322,12 +341,14 @@ export function writeSanitizedGitconfig({
|
|
|
322
341
|
const SHELL_CONFIG_SYMLINKS = ['.gitconfig', '.gitignore_global', '.stCommitMsg', '.bash_aliases'];
|
|
323
342
|
|
|
324
343
|
export function ensureShellConfigSymlinks(engine: string, container: string, execFn: EngineExecFn = execEngine): void {
|
|
325
|
-
// Idempotent symlink setup.
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
344
|
+
// Idempotent symlink setup. Avoid a shell command here because Windows .cmd
|
|
345
|
+
// engine shims would interpret metacharacters in a `bash -lc` script before
|
|
346
|
+
// forwarding it to Docker.
|
|
347
|
+
for (const file of SHELL_CONFIG_SYMLINKS) {
|
|
348
|
+
execFn(engine, 'docker', [
|
|
349
|
+
'exec', container, 'ln', '-sf', `.host-shell-config/${file}`, `${CONTAINER_HOME}/${file}`
|
|
350
|
+
], { stdio: 'ignore' });
|
|
351
|
+
}
|
|
331
352
|
}
|
|
332
353
|
|
|
333
354
|
export function prepareHostShellConfig({
|
|
@@ -619,28 +640,44 @@ function formatEnvFileEntry(key: string, value: string): string {
|
|
|
619
640
|
return `${key}=${value}`;
|
|
620
641
|
}
|
|
621
642
|
|
|
643
|
+
export function collectHostProxyEntries(env: NodeJS.ProcessEnv): Array<[string, string]> {
|
|
644
|
+
const entriesByExactKey = new Map(Object.entries(env));
|
|
645
|
+
return HOST_PROXY_ENV_KEYS.flatMap((key) => {
|
|
646
|
+
const value = entriesByExactKey.get(key);
|
|
647
|
+
return typeof value === 'string' && value !== '' ? [[key, value]] : [];
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
622
651
|
export function buildContainerEnvFile(
|
|
623
652
|
resolvedTools: ResolvedTool[],
|
|
624
653
|
engine: string,
|
|
625
654
|
runSafeEngineFn: EngineRunSafeFn = runSafeEngine,
|
|
626
655
|
options: {
|
|
656
|
+
additionalEntries?: Array<[string, string]>;
|
|
627
657
|
mkdtempFn?: typeof fs.mkdtempSync;
|
|
628
658
|
writeFileFn?: typeof fs.writeFileSync;
|
|
629
659
|
chmodFn?: typeof fs.chmodSync;
|
|
630
660
|
rmFn?: typeof fs.rmSync;
|
|
631
661
|
tmpDir?: string;
|
|
662
|
+
runSafeFn?: DirectRunSafeFn;
|
|
632
663
|
} = {}
|
|
633
664
|
): { dockerArgs: string[]; cleanup: () => void } {
|
|
634
665
|
const {
|
|
666
|
+
additionalEntries = [],
|
|
635
667
|
mkdtempFn = fs.mkdtempSync,
|
|
636
668
|
writeFileFn = fs.writeFileSync,
|
|
637
669
|
chmodFn = fs.chmodSync,
|
|
638
670
|
rmFn = fs.rmSync,
|
|
639
|
-
tmpDir = os.tmpdir()
|
|
671
|
+
tmpDir = os.tmpdir(),
|
|
672
|
+
runSafeFn = runSafe
|
|
640
673
|
} = options;
|
|
641
674
|
|
|
642
675
|
const entries: Array<[string, string]> = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
|
|
643
|
-
|
|
676
|
+
entries.push(...additionalEntries);
|
|
677
|
+
let ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
|
|
678
|
+
if (!ghToken && engine === 'wsl2') {
|
|
679
|
+
ghToken = runSafeFn('gh', ['auth', 'token']);
|
|
680
|
+
}
|
|
644
681
|
if (ghToken) {
|
|
645
682
|
entries.push(['GH_TOKEN', ghToken]);
|
|
646
683
|
}
|
|
@@ -1234,6 +1271,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1234
1271
|
cpu: { type: 'string' },
|
|
1235
1272
|
memory: { type: 'string' },
|
|
1236
1273
|
'no-refresh': { type: 'boolean' },
|
|
1274
|
+
'inherit-proxy': { type: 'boolean', short: 'P' },
|
|
1237
1275
|
help: { type: 'boolean', short: 'h' }
|
|
1238
1276
|
}
|
|
1239
1277
|
});
|
|
@@ -1491,8 +1529,12 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1491
1529
|
signingKey
|
|
1492
1530
|
)
|
|
1493
1531
|
: null;
|
|
1494
|
-
const
|
|
1532
|
+
const proxyEntries = values['inherit-proxy'] ? collectHostProxyEntries(process.env) : [];
|
|
1533
|
+
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine, undefined, {
|
|
1534
|
+
additionalEntries: proxyEntries
|
|
1535
|
+
});
|
|
1495
1536
|
let hostShellConfig: HostShellConfig;
|
|
1537
|
+
let tmpfsSeedPlan: TmpfsSeedPlanEntry[] = [];
|
|
1496
1538
|
try {
|
|
1497
1539
|
const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
1498
1540
|
if (claudeCodeEntry) {
|
|
@@ -1533,22 +1575,24 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1533
1575
|
'-v',
|
|
1534
1576
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1535
1577
|
]);
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
// the
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
// sessions/ from a previous bind-mount era) must NOT be re-mounted,
|
|
1545
|
-
// or the high-churn writes would land on the host SSD again.
|
|
1546
|
-
const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1547
|
-
(tool.tmpfs?.seed ?? []).flatMap((entry) => {
|
|
1578
|
+
// Mount each declared seed read-only at an isolated staging path.
|
|
1579
|
+
// After docker run mounts the empty tmpfs, the container's default
|
|
1580
|
+
// user copies these entries into it so the runtime targets are normal,
|
|
1581
|
+
// writable files rather than nested mount points. The allowlist keeps
|
|
1582
|
+
// stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
|
|
1583
|
+
// being exposed or written through.
|
|
1584
|
+
tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1585
|
+
(tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
|
|
1548
1586
|
const hostPath = path.join(dir, entry);
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1587
|
+
if (!fs.existsSync(hostPath)) {
|
|
1588
|
+
return [];
|
|
1589
|
+
}
|
|
1590
|
+
const stagingPath = path.posix.join('/run/agent-infra/tmpfs-seeds', tool.id, String(index));
|
|
1591
|
+
return [{
|
|
1592
|
+
stagingPath,
|
|
1593
|
+
targetPath: path.posix.join(tool.containerMount, entry),
|
|
1594
|
+
volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
|
|
1595
|
+
}];
|
|
1552
1596
|
})
|
|
1553
1597
|
);
|
|
1554
1598
|
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
@@ -1604,7 +1648,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1604
1648
|
...dotfilesMount,
|
|
1605
1649
|
...toolVolumes,
|
|
1606
1650
|
...tmpfsArgs,
|
|
1607
|
-
...
|
|
1651
|
+
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1608
1652
|
...liveMountVolumes,
|
|
1609
1653
|
...shellConfigVolumes,
|
|
1610
1654
|
...envFile.dockerArgs,
|
|
@@ -1617,6 +1661,15 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1617
1661
|
envFile.cleanup();
|
|
1618
1662
|
}
|
|
1619
1663
|
|
|
1664
|
+
for (const { stagingPath, targetPath } of tmpfsSeedPlan) {
|
|
1665
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1666
|
+
'exec', container, 'mkdir', '-p', path.posix.dirname(targetPath)
|
|
1667
|
+
]);
|
|
1668
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1669
|
+
'exec', container, 'cp', '-R', '--', stagingPath, targetPath
|
|
1670
|
+
]);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1620
1673
|
// Belt-and-suspenders: re-create the four shell-config symlinks at
|
|
1621
1674
|
// runtime so users with a custom `sandbox.dockerfile` (which won't
|
|
1622
1675
|
// include the ai-tools.dockerfile symlink fragment) still get
|
package/lib/sandbox/engine.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { platform } from 'node:os';
|
|
|
2
2
|
import { detectHostResources } from './constants.ts';
|
|
3
3
|
import { ADAPTERS, enginesForPlatform, getAdapter } from './engines/index.ts';
|
|
4
4
|
import type { EffectiveSandboxConfig, OnMessage, RunFns, SandboxAdapter, SandboxVmConfig } from './engines/index.ts';
|
|
5
|
-
import { run, runOk, runSafe, runVerbose } from './shell.ts';
|
|
5
|
+
import { run, runOk, runOkEngine, runSafe, runVerbose } from './shell.ts';
|
|
6
6
|
|
|
7
7
|
export const ENGINES = Object.freeze({
|
|
8
8
|
COLIMA: 'colima',
|
|
@@ -26,6 +26,7 @@ type EngineDependencies = {
|
|
|
26
26
|
platformFn?: typeof platform;
|
|
27
27
|
runFn?: RunFns['run'];
|
|
28
28
|
runOkFn?: RunFns['runOk'];
|
|
29
|
+
runOkEngineFn?: typeof runOkEngine;
|
|
29
30
|
runSafeFn?: RunFns['runSafe'];
|
|
30
31
|
runVerboseFn?: RunFns['runVerbose'];
|
|
31
32
|
};
|
|
@@ -50,6 +51,37 @@ function applyDockerContext(adapter: SandboxAdapter): void {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function buildKitError(engine: string): Error {
|
|
55
|
+
let repair: string;
|
|
56
|
+
switch (engine) {
|
|
57
|
+
case ENGINES.COLIMA:
|
|
58
|
+
repair = 'Install the Docker Buildx plugin with: brew install docker-buildx';
|
|
59
|
+
break;
|
|
60
|
+
case ENGINES.DOCKER_DESKTOP:
|
|
61
|
+
case ENGINES.WSL2:
|
|
62
|
+
repair = 'Upgrade or repair Docker Desktop so Docker Buildx and BuildKit are available.';
|
|
63
|
+
break;
|
|
64
|
+
case ENGINES.NATIVE:
|
|
65
|
+
repair = 'Install the Docker Buildx plugin for your Docker Engine installation.';
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
repair = `Upgrade or repair ${engineDisplayName(engine)} so Docker Buildx and BuildKit are available.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return new Error([
|
|
72
|
+
`Docker BuildKit is not available for ${engineDisplayName(engine)}.`,
|
|
73
|
+
repair,
|
|
74
|
+
'Verify the builder with: docker buildx inspect --bootstrap',
|
|
75
|
+
'Then retry the sandbox create or rebuild command.'
|
|
76
|
+
].join('\n'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ensureBuildKit(engine: string, runOkEngineFn: typeof runOkEngine): void {
|
|
80
|
+
if (!runOkEngineFn(engine, 'docker', ['buildx', 'inspect', '--bootstrap'])) {
|
|
81
|
+
throw buildKitError(engine);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
53
85
|
export function validateSandboxEngine(
|
|
54
86
|
engine: string | null | undefined,
|
|
55
87
|
{ platformFn = platform }: Pick<EngineDependencies, 'platformFn'> = {}
|
|
@@ -144,10 +176,12 @@ export async function ensureDocker(
|
|
|
144
176
|
const engine = detectEngine(config, dependencies);
|
|
145
177
|
const adapter = getAdapter(engine);
|
|
146
178
|
const effectiveConfig = effectiveConfigFor(adapter, config);
|
|
179
|
+
const fns = runFns(dependencies);
|
|
147
180
|
|
|
148
181
|
applyDockerContext(adapter);
|
|
149
|
-
const vmJustStarted = await adapter.ensure(effectiveConfig, onMessage,
|
|
150
|
-
|
|
182
|
+
const vmJustStarted = await adapter.ensure(effectiveConfig, onMessage, fns);
|
|
183
|
+
ensureBuildKit(engine, dependencies.runOkEngineFn ?? runOkEngine);
|
|
184
|
+
adapter.syncResources(effectiveConfig, onMessage, fns, { vmJustStarted });
|
|
151
185
|
}
|
|
152
186
|
|
|
153
187
|
export function isVmManaged(config: EngineConfig = {}, dependencies: EngineDependencies = {}): boolean {
|
|
@@ -35,8 +35,8 @@ export const colimaAdapter: SandboxAdapter = {
|
|
|
35
35
|
let started = false;
|
|
36
36
|
|
|
37
37
|
if (!runOk('which', ['colima'])) {
|
|
38
|
-
onMessage?.('Installing colima + docker via Homebrew...');
|
|
39
|
-
runVerbose('brew', ['install', 'colima', 'docker']);
|
|
38
|
+
onMessage?.('Installing colima + docker + docker-buildx via Homebrew...');
|
|
39
|
+
runVerbose('brew', ['install', 'colima', 'docker', 'docker-buildx']);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (!runOk('colima', ['status'])) {
|
|
@@ -12,6 +12,9 @@ RUN if [ "${HOST_UID}" = "0" ]; then \
|
|
|
12
12
|
(groupadd -o -g ${HOST_GID} devuser || true) && \
|
|
13
13
|
useradd -o -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
|
|
14
14
|
else \
|
|
15
|
+
if ubuntu_uid="$(id -u ubuntu 2>/dev/null)" && [ "${ubuntu_uid}" = "${HOST_UID}" ]; then \
|
|
16
|
+
userdel -r ubuntu || exit 1; \
|
|
17
|
+
fi; \
|
|
15
18
|
(groupadd -g ${HOST_GID} devuser || true) && \
|
|
16
19
|
useradd -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
|
|
17
20
|
fi
|
|
@@ -188,6 +191,10 @@ fi
|
|
|
188
191
|
exec tmux new-session -s "$SESSION"
|
|
189
192
|
SCRIPT
|
|
190
193
|
|
|
194
|
+
RUN test -s /usr/local/bin/cc-token-status && test -x /usr/local/bin/cc-token-status && \
|
|
195
|
+
test -s /usr/local/bin/sandbox-dotfiles-link && test -x /usr/local/bin/sandbox-dotfiles-link && \
|
|
196
|
+
test -s /usr/local/bin/sandbox-tmux-entry && test -x /usr/local/bin/sandbox-tmux-entry
|
|
197
|
+
|
|
191
198
|
ENV LANG=en_US.UTF-8
|
|
192
199
|
ENV LC_ALL=en_US.UTF-8
|
|
193
200
|
ENV TERM=xterm-256color
|
package/lib/sandbox/shell.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import type { ExecFileSyncOptions, StdioOptions, SpawnSyncOptions, SpawnSyncReturns } from 'node:child_process';
|
|
5
|
+
import { getAdapter } from './engines/index.ts';
|
|
5
6
|
|
|
6
7
|
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
7
8
|
|
|
@@ -52,7 +53,7 @@ function resolveCommand(cmd: string): string {
|
|
|
52
53
|
return cmd;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function commandOptions<T extends
|
|
56
|
+
function commandOptions<T extends object>(cmd: string, opts: T): T | (T & { shell: true }) {
|
|
56
57
|
if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
|
|
57
58
|
return { ...opts, shell: true };
|
|
58
59
|
}
|
|
@@ -143,6 +144,13 @@ export function commandForEngine(engine: string, cmd: string, args: string[] = [
|
|
|
143
144
|
return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
if (cmd === 'docker') {
|
|
148
|
+
const context = getAdapter(engine).dockerContext;
|
|
149
|
+
if (context) {
|
|
150
|
+
return { cmd, args: ['--context', context, ...args] };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
146
154
|
return { cmd, args };
|
|
147
155
|
}
|
|
148
156
|
|
|
@@ -153,7 +161,8 @@ export function runEngine(engine: string, cmd: string, args: string[], opts: Com
|
|
|
153
161
|
|
|
154
162
|
export function execEngine(engine: string, cmd: string, args: string[], opts: ExecFileSyncOptions = {}) {
|
|
155
163
|
const command = commandForEngine(engine, cmd, args);
|
|
156
|
-
|
|
164
|
+
const resolved = resolveCommand(command.cmd);
|
|
165
|
+
return execFileSync(resolved, command.args, commandOptions(resolved, opts));
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
export function runOkEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): boolean {
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -22,9 +22,9 @@ export type SandboxTool = {
|
|
|
22
22
|
// When set, containerMount is mounted as an in-container tmpfs (RAM) instead
|
|
23
23
|
// of bind-mounting the host config dir, keeping high-churn tool logs off the
|
|
24
24
|
// host disk. `seed` lists the host-dir entries (relative to the tool's config
|
|
25
|
-
// dir) to
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// dir) to copy into the tmpfs when the container starts. It is an explicit
|
|
26
|
+
// allowlist so runtime files (e.g. logs_2.sqlite, sessions) left in the host
|
|
27
|
+
// dir stay out of the container and seeded config cannot write back.
|
|
28
28
|
tmpfs?: { size?: string; seed?: string[] };
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -80,8 +80,8 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
80
80
|
// codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
|
|
81
81
|
// a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
|
|
82
82
|
// codex home as tmpfs so those logs stay in RAM and die with the container.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
83
|
+
// Seeded config (config.toml, model-catalogs) is copied into the tmpfs at
|
|
84
|
+
// startup; runtime files like logs_2.sqlite stay in RAM.
|
|
85
85
|
tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
|
|
86
86
|
hostLiveMounts: [
|
|
87
87
|
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
package/lib/server/daemon.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { RunnerOptions, RunnerResult } from './runner.ts';
|
|
|
12
12
|
import { streamCommand } from './streamer.ts';
|
|
13
13
|
import { markdownMessage, replyOutbound, textMessage } from './display.ts';
|
|
14
14
|
import { buildStatusModel, statusModelToDisplay, type StatusModel } from '../task/commands/status.ts';
|
|
15
|
+
import { getProcessStartTime, removePidRecordIfMatches } from './process-state.ts';
|
|
15
16
|
|
|
16
17
|
function errorMessage(error: unknown): string {
|
|
17
18
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -110,6 +111,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
110
111
|
|
|
111
112
|
const logger = createLogger(config.log);
|
|
112
113
|
logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
|
|
114
|
+
const ownStartTime = getProcessStartTime(process.pid);
|
|
113
115
|
|
|
114
116
|
const abortController = new AbortController();
|
|
115
117
|
let resolveShutdown: () => void = () => {};
|
|
@@ -135,6 +137,13 @@ export async function runDaemon(): Promise<void> {
|
|
|
135
137
|
await unloadAdapters(adapters);
|
|
136
138
|
clearInterval(heartbeat);
|
|
137
139
|
logger.close();
|
|
140
|
+
if (ownStartTime !== null) {
|
|
141
|
+
removePidRecordIfMatches(config.pidFile, {
|
|
142
|
+
version: 1,
|
|
143
|
+
pid: process.pid,
|
|
144
|
+
startTime: ownStartTime
|
|
145
|
+
});
|
|
146
|
+
}
|
|
138
147
|
resolveShutdown();
|
|
139
148
|
})();
|
|
140
149
|
};
|