@fitlab-ai/agent-infra 0.8.2 → 0.8.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.zh-CN.md +10 -0
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +2 -2
- package/dist/lib/cp.js +57 -16
- package/dist/lib/decide.js +2 -2
- package/dist/lib/defaults.json +2 -0
- package/dist/lib/init.js +7 -7
- package/dist/lib/merge.js +1 -1
- package/dist/lib/prompt.js +1 -1
- package/dist/lib/run/index.js +7 -7
- package/dist/lib/run/prompt.js +1 -1
- package/dist/lib/sandbox/capture.js +10 -8
- package/dist/lib/sandbox/clipboard/bridge.js +59 -21
- package/dist/lib/sandbox/clipboard/inbox.js +82 -0
- package/dist/lib/sandbox/clipboard/index.js +5 -6
- package/dist/lib/sandbox/clipboard/linux.js +91 -0
- package/dist/lib/sandbox/clipboard/paths.js +1 -1
- package/dist/lib/sandbox/clipboard/win32.js +144 -0
- package/dist/lib/sandbox/commands/create.js +106 -88
- package/dist/lib/sandbox/commands/enter.js +9 -9
- package/dist/lib/sandbox/commands/list-running.js +2 -2
- package/dist/lib/sandbox/commands/ls.js +7 -7
- package/dist/lib/sandbox/commands/prune.js +8 -8
- package/dist/lib/sandbox/commands/rebuild.js +38 -50
- package/dist/lib/sandbox/commands/refresh.js +2 -2
- package/dist/lib/sandbox/commands/rm.js +11 -11
- package/dist/lib/sandbox/commands/show.js +4 -4
- package/dist/lib/sandbox/commands/start.js +4 -4
- package/dist/lib/sandbox/commands/vm.js +4 -4
- package/dist/lib/sandbox/config.js +10 -4
- package/dist/lib/sandbox/constants.js +4 -1
- package/dist/lib/sandbox/credentials.js +1 -1
- package/dist/lib/sandbox/dotfiles.js +1 -1
- package/dist/lib/sandbox/engine.js +3 -3
- package/dist/lib/sandbox/engines/index.js +5 -5
- package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
- package/dist/lib/sandbox/image-build.js +80 -0
- package/dist/lib/sandbox/image-prune.js +2 -2
- package/dist/lib/sandbox/index.js +10 -10
- package/dist/lib/sandbox/managed-fs.js +1 -1
- package/dist/lib/sandbox/readme-scaffold.js +1 -1
- package/dist/lib/sandbox/runtimes/base.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +4 -4
- package/dist/lib/server/adapters/feishu/index.js +10 -3
- package/dist/lib/server/adapters/feishu/renderer.js +88 -0
- package/dist/lib/server/adapters/feishu/transport.js +2 -13
- package/dist/lib/server/daemon.js +64 -43
- package/dist/lib/server/display.js +83 -0
- package/dist/lib/server/index.js +2 -2
- package/dist/lib/server/process-control.js +2 -2
- package/dist/lib/server/protocol.js +2 -2
- package/dist/lib/server/streamer.js +5 -4
- package/dist/lib/task/commands/cat.js +2 -2
- package/dist/lib/task/commands/decisions.js +4 -4
- package/dist/lib/task/commands/files.js +3 -3
- package/dist/lib/task/commands/grep.js +3 -3
- package/dist/lib/task/commands/issue-body.js +4 -4
- package/dist/lib/task/commands/log.js +5 -5
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/commands/show.js +1 -1
- package/dist/lib/task/commands/status.js +71 -25
- package/dist/lib/task/index.js +9 -9
- package/dist/lib/task/resolve-ref.js +1 -1
- package/dist/lib/task/workflow-warnings.js +94 -0
- package/dist/lib/update.js +4 -4
- package/lib/cp.ts +56 -15
- package/lib/defaults.json +2 -0
- package/lib/sandbox/capture.ts +5 -3
- package/lib/sandbox/clipboard/bridge.ts +62 -18
- package/lib/sandbox/clipboard/inbox.ts +83 -0
- package/lib/sandbox/clipboard/index.ts +5 -6
- package/lib/sandbox/clipboard/linux.ts +124 -0
- package/lib/sandbox/clipboard/win32.ts +173 -0
- package/lib/sandbox/commands/create.ts +132 -88
- package/lib/sandbox/commands/rebuild.ts +42 -54
- package/lib/sandbox/config.ts +13 -1
- package/lib/sandbox/constants.ts +4 -0
- package/lib/sandbox/image-build.ts +134 -0
- package/lib/sandbox/runtimes/base.dockerfile +3 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- package/lib/server/adapters/_contract.ts +3 -0
- package/lib/server/adapters/feishu/index.ts +11 -3
- package/lib/server/adapters/feishu/renderer.ts +99 -0
- package/lib/server/adapters/feishu/transport.ts +5 -18
- package/lib/server/daemon.ts +74 -38
- package/lib/server/display.ts +136 -0
- package/lib/server/streamer.ts +5 -4
- package/lib/task/commands/log.ts +3 -3
- package/lib/task/commands/status.ts +102 -21
- package/lib/task/workflow-warnings.ts +121 -0
- package/package.json +2 -2
- package/templates/.agents/README.en.md +3 -0
- package/templates/.agents/README.zh-CN.md +3 -0
- package/templates/.agents/rules/create-issue.github.en.md +4 -4
- package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -4
- package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
- package/templates/.agents/rules/issue-sync.github.en.md +13 -0
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
- package/templates/.agents/rules/next-step-output.en.md +15 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
- package/templates/.agents/rules/pr-sync.github.en.md +17 -2
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
- package/templates/.agents/rules/review-handshake.en.md +2 -2
- package/templates/.agents/rules/review-handshake.zh-CN.md +2 -2
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- 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/platform-adapters/platform-sync.github.js +57 -0
- package/templates/.agents/scripts/validate-artifact.js +213 -0
- package/templates/.agents/scripts/workflow-warnings.js +290 -0
- 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 +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +6 -6
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +142 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +142 -0
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
- 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 +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -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 +6 -3
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +9 -6
- 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 +3 -2
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +165 -41
- 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 +1 -0
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
- 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 +2 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -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.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +36 -6
- 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
- package/templates/.agents/templates/task.en.md +7 -0
- package/templates/.agents/templates/task.zh-CN.md +7 -0
- package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
- package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
- package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
- package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
- package/templates/.github/workflows/metadata-sync.yml +5 -0
- package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import type { ExecFileSyncOptions } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const PROBE_TIMEOUT_MS = 2_000;
|
|
8
|
+
const READ_IMAGE_TIMEOUT_MS = 5_000;
|
|
9
|
+
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
10
|
+
|
|
11
|
+
type ExecFn = (cmd: string, args: string[], options?: ExecFileSyncOptions) => Buffer | string;
|
|
12
|
+
type ExecToFileFn = (cmd: string, args: string[], outputPath: string, timeout: number) => void;
|
|
13
|
+
|
|
14
|
+
type LinuxBackend =
|
|
15
|
+
| { kind: 'wayland'; command: 'wl-paste' }
|
|
16
|
+
| { kind: 'x11'; command: 'xclip' };
|
|
17
|
+
|
|
18
|
+
export type LinuxClipboardAdapter = {
|
|
19
|
+
available(): { ok: true } | { ok: false; reason: string };
|
|
20
|
+
readImagePng(): Buffer | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createLinuxClipboardAdapter({
|
|
24
|
+
env = process.env,
|
|
25
|
+
execFn = execFileSync,
|
|
26
|
+
execToFileFn = execToFile,
|
|
27
|
+
mkdtempFn = fs.mkdtempSync,
|
|
28
|
+
readFileFn = fs.readFileSync,
|
|
29
|
+
rmFn = fs.rmSync
|
|
30
|
+
}: {
|
|
31
|
+
env?: NodeJS.ProcessEnv;
|
|
32
|
+
execFn?: ExecFn;
|
|
33
|
+
execToFileFn?: ExecToFileFn;
|
|
34
|
+
mkdtempFn?: typeof fs.mkdtempSync;
|
|
35
|
+
readFileFn?: typeof fs.readFileSync;
|
|
36
|
+
rmFn?: typeof fs.rmSync;
|
|
37
|
+
} = {}): LinuxClipboardAdapter {
|
|
38
|
+
return {
|
|
39
|
+
available() {
|
|
40
|
+
const backend = selectBackend(env);
|
|
41
|
+
try {
|
|
42
|
+
execFn(backend.command, versionArgs(backend), {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
timeout: PROBE_TIMEOUT_MS
|
|
45
|
+
});
|
|
46
|
+
return { ok: true };
|
|
47
|
+
} catch {
|
|
48
|
+
return { ok: false, reason: unavailableReason(backend) };
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
readImagePng() {
|
|
52
|
+
const backend = selectBackend(env);
|
|
53
|
+
try {
|
|
54
|
+
const mimeTypes = String(execFn(backend.command, mimeArgs(backend), {
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
timeout: PROBE_TIMEOUT_MS
|
|
57
|
+
}));
|
|
58
|
+
if (!hasPngMime(mimeTypes)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
|
|
63
|
+
const outputPath = path.join(tmpDir, 'clipboard.png');
|
|
64
|
+
try {
|
|
65
|
+
execToFileFn(backend.command, imageArgs(backend), outputPath, READ_IMAGE_TIMEOUT_MS);
|
|
66
|
+
const png = Buffer.from(readFileFn(outputPath));
|
|
67
|
+
return isPng(png) ? png : null;
|
|
68
|
+
} finally {
|
|
69
|
+
rmFn(tmpDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function selectBackend(env: NodeJS.ProcessEnv): LinuxBackend {
|
|
79
|
+
return env.WAYLAND_DISPLAY?.trim()
|
|
80
|
+
? { kind: 'wayland', command: 'wl-paste' }
|
|
81
|
+
: { kind: 'x11', command: 'xclip' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function versionArgs(backend: LinuxBackend): string[] {
|
|
85
|
+
return backend.kind === 'wayland' ? ['--version'] : ['-version'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mimeArgs(backend: LinuxBackend): string[] {
|
|
89
|
+
return backend.kind === 'wayland'
|
|
90
|
+
? ['--list-types']
|
|
91
|
+
: ['-selection', 'clipboard', '-t', 'TARGETS', '-o'];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function imageArgs(backend: LinuxBackend): string[] {
|
|
95
|
+
return backend.kind === 'wayland'
|
|
96
|
+
? ['-t', 'image/png']
|
|
97
|
+
: ['-selection', 'clipboard', '-t', 'image/png', '-o'];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function unavailableReason(backend: LinuxBackend): string {
|
|
101
|
+
return backend.kind === 'wayland'
|
|
102
|
+
? 'Wayland clipboard tool wl-paste is unavailable; install wl-clipboard to enable image paste'
|
|
103
|
+
: 'X11 clipboard tool xclip is unavailable; install xclip to enable image paste';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasPngMime(output: string): boolean {
|
|
107
|
+
return output.split(/\s+/u).some((type) => type === 'image/png');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function execToFile(cmd: string, args: string[], outputPath: string, timeout: number): void {
|
|
111
|
+
const fd = fs.openSync(outputPath, 'w', 0o600);
|
|
112
|
+
try {
|
|
113
|
+
execFileSync(cmd, args, {
|
|
114
|
+
timeout,
|
|
115
|
+
stdio: ['ignore', fd, 'pipe']
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
fs.closeSync(fd);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isPng(buffer: Buffer): boolean {
|
|
123
|
+
return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
|
|
124
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import type { ExecFileSyncOptions } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const PROBE_TIMEOUT_MS = 2_000;
|
|
8
|
+
const READ_IMAGE_TIMEOUT_MS = 5_000;
|
|
9
|
+
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
10
|
+
const POWERSHELL_ARGS = ['-NoProfile', '-NonInteractive', '-STA', '-Command'];
|
|
11
|
+
|
|
12
|
+
type ExecFn = (cmd: string, args: string[], options?: ExecFileSyncOptions) => Buffer | string;
|
|
13
|
+
type MkdtempFn = (prefix: string) => string;
|
|
14
|
+
type ReadFileFn = (filePath: fs.PathOrFileDescriptor) => Buffer | string;
|
|
15
|
+
type RmFn = (filePath: fs.PathLike, options: { recursive?: boolean; force?: boolean }) => void;
|
|
16
|
+
type ExistsFn = (filePath: fs.PathLike) => boolean;
|
|
17
|
+
|
|
18
|
+
export type Win32ClipboardAdapter = {
|
|
19
|
+
available(): { ok: true } | { ok: false; reason: string };
|
|
20
|
+
readImagePng(): Buffer | null;
|
|
21
|
+
readImageFromPath(imagePath: string): Buffer | null;
|
|
22
|
+
readImageFromText(text: string): Buffer | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createWin32ClipboardAdapter({
|
|
26
|
+
execFn = execFileSync,
|
|
27
|
+
mkdtempFn = fs.mkdtempSync,
|
|
28
|
+
readFileFn = fs.readFileSync,
|
|
29
|
+
rmFn = fs.rmSync,
|
|
30
|
+
existsFn = fs.existsSync
|
|
31
|
+
}: {
|
|
32
|
+
execFn?: ExecFn;
|
|
33
|
+
mkdtempFn?: MkdtempFn;
|
|
34
|
+
readFileFn?: ReadFileFn;
|
|
35
|
+
rmFn?: RmFn;
|
|
36
|
+
existsFn?: ExistsFn;
|
|
37
|
+
} = {}): Win32ClipboardAdapter {
|
|
38
|
+
return {
|
|
39
|
+
available() {
|
|
40
|
+
try {
|
|
41
|
+
execFn('powershell.exe', [...POWERSHELL_ARGS, clipboardProbeScript()], {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
timeout: PROBE_TIMEOUT_MS
|
|
44
|
+
});
|
|
45
|
+
return { ok: true };
|
|
46
|
+
} catch {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
reason: 'Windows PowerShell Get-Clipboard is unavailable; install or enable powershell.exe to use image paste'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
readImagePng() {
|
|
54
|
+
const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
|
|
55
|
+
const outputPath = path.join(tmpDir, 'clipboard.png');
|
|
56
|
+
try {
|
|
57
|
+
execFn('powershell.exe', [...POWERSHELL_ARGS, pngWriteScript(outputPath)], {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
timeout: READ_IMAGE_TIMEOUT_MS
|
|
60
|
+
});
|
|
61
|
+
const png = Buffer.from(readFileFn(outputPath));
|
|
62
|
+
return isPng(png) ? png : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
} finally {
|
|
66
|
+
rmFn(tmpDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
readImageFromPath(imagePath) {
|
|
70
|
+
const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
|
|
71
|
+
const outputPath = path.join(tmpDir, 'clipboard.png');
|
|
72
|
+
try {
|
|
73
|
+
execFn('powershell.exe', [...POWERSHELL_ARGS, pngFromPathScript(imagePath, outputPath)], {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
timeout: READ_IMAGE_TIMEOUT_MS
|
|
76
|
+
});
|
|
77
|
+
const png = Buffer.from(readFileFn(outputPath));
|
|
78
|
+
return isPng(png) ? png : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
} finally {
|
|
82
|
+
rmFn(tmpDir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
readImageFromText(text) {
|
|
86
|
+
const trimmed = text.trim();
|
|
87
|
+
if (!isWindowsImagePath(trimmed)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (!existsFn(trimmed)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return this.readImageFromPath(trimmed);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function clipboardProbeScript(): string {
|
|
99
|
+
return 'if ($null -eq (Get-Command Get-Clipboard -ErrorAction SilentlyContinue)) { exit 1 }';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Embed a filesystem path in a PowerShell single-quoted string literal.
|
|
103
|
+
// Single-quoted strings in PowerShell are verbatim (no variable expansion,
|
|
104
|
+
// no escape sequences). The only special character is a single quote itself,
|
|
105
|
+
// which is escaped by doubling it.
|
|
106
|
+
function psLiteral(value: string): string {
|
|
107
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function pngWriteScript(outputPath: string): string {
|
|
111
|
+
const psOutputPath = psLiteral(outputPath);
|
|
112
|
+
return [
|
|
113
|
+
'$ErrorActionPreference = "Stop"',
|
|
114
|
+
`$outputPath = ${psOutputPath}`,
|
|
115
|
+
'$image = $null',
|
|
116
|
+
'try {',
|
|
117
|
+
' Add-Type -AssemblyName System.Drawing',
|
|
118
|
+
' $image = Get-Clipboard -Format Image -ErrorAction SilentlyContinue',
|
|
119
|
+
' if ($null -ne $image) {',
|
|
120
|
+
' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
|
|
121
|
+
' return',
|
|
122
|
+
' }',
|
|
123
|
+
' $files = Get-Clipboard -Format FileDropList -ErrorAction SilentlyContinue',
|
|
124
|
+
' if ($null -ne $files) {',
|
|
125
|
+
' $allowed = @(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".ico")',
|
|
126
|
+
' foreach ($file in $files) {',
|
|
127
|
+
' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue }',
|
|
128
|
+
' $ext = [System.IO.Path]::GetExtension($file)',
|
|
129
|
+
' if ($allowed -notcontains $ext) { continue }',
|
|
130
|
+
' try {',
|
|
131
|
+
' $image = [System.Drawing.Image]::FromFile($file)',
|
|
132
|
+
' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
|
|
133
|
+
' return',
|
|
134
|
+
' } catch {}',
|
|
135
|
+
' }',
|
|
136
|
+
' }',
|
|
137
|
+
' throw "clipboard has no image"',
|
|
138
|
+
'} finally {',
|
|
139
|
+
' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
|
|
140
|
+
'}'
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pngFromPathScript(imagePath: string, outputPath: string): string {
|
|
145
|
+
const psImagePath = psLiteral(imagePath);
|
|
146
|
+
const psOutputPath = psLiteral(outputPath);
|
|
147
|
+
return [
|
|
148
|
+
'$ErrorActionPreference = "Stop"',
|
|
149
|
+
`$imagePath = ${psImagePath}`,
|
|
150
|
+
`$outputPath = ${psOutputPath}`,
|
|
151
|
+
'$image = $null',
|
|
152
|
+
'try {',
|
|
153
|
+
' Add-Type -AssemblyName System.Drawing',
|
|
154
|
+
' if (-not (Test-Path -LiteralPath $imagePath -PathType Leaf)) { throw "file not found: $imagePath" }',
|
|
155
|
+
' $image = [System.Drawing.Image]::FromFile($imagePath)',
|
|
156
|
+
' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
|
|
157
|
+
'} finally {',
|
|
158
|
+
' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
|
|
159
|
+
'}'
|
|
160
|
+
].join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const IMAGE_EXTENSIONS_RE = /\.(png|jpg|jpeg|gif|bmp|webp|tiff?|ico)$/i;
|
|
164
|
+
|
|
165
|
+
function isWindowsImagePath(text: string): boolean {
|
|
166
|
+
// Must be a Windows absolute path: drive letter + colon + backslash/slash
|
|
167
|
+
// followed by a path ending with a known image extension.
|
|
168
|
+
return /^[A-Za-z]:[\\/].+$/.test(text) && IMAGE_EXTENSIONS_RE.test(text);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isPng(buffer: Buffer): boolean {
|
|
172
|
+
return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
|
|
173
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
parsePositiveIntegerOption,
|
|
17
17
|
sandboxBranchLabel,
|
|
18
18
|
sandboxImageConfigLabel,
|
|
19
|
+
sandboxImageRefreshLabel,
|
|
19
20
|
sandboxLabel,
|
|
20
21
|
shareBranchDir,
|
|
21
22
|
shareCommonDir,
|
|
@@ -37,17 +38,13 @@ import {
|
|
|
37
38
|
} from '../shell.ts';
|
|
38
39
|
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
39
40
|
import {
|
|
40
|
-
imageSignatureFields,
|
|
41
41
|
resolveTools,
|
|
42
|
-
toolConfigDirCandidates
|
|
43
|
-
toolNpmPackagesArg,
|
|
44
|
-
toolShellInstallScriptBase64
|
|
42
|
+
toolConfigDirCandidates
|
|
45
43
|
} from '../tools.ts';
|
|
46
44
|
import type { SandboxTool } from '../tools.ts';
|
|
47
45
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
48
46
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
49
47
|
import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
50
|
-
import { resolveBuildUid } from '../engines/native.ts';
|
|
51
48
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
52
49
|
import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.ts';
|
|
53
50
|
import {
|
|
@@ -56,6 +53,14 @@ import {
|
|
|
56
53
|
validateClaudeCredentialsEnvOverride
|
|
57
54
|
} from '../credentials.ts';
|
|
58
55
|
import { detectHostTimezone } from '../host-timezone.ts';
|
|
56
|
+
import {
|
|
57
|
+
buildImageSignature,
|
|
58
|
+
buildSandboxImageArgs,
|
|
59
|
+
isRefreshDisabled,
|
|
60
|
+
isRefreshDue,
|
|
61
|
+
parseImageLabels,
|
|
62
|
+
parseRefreshTimestamp
|
|
63
|
+
} from '../image-build.ts';
|
|
59
64
|
|
|
60
65
|
const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
|
|
61
66
|
const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
|
|
@@ -82,7 +87,7 @@ alias gy='gemini --yolo; tput ed'
|
|
|
82
87
|
`;
|
|
83
88
|
const CONTAINER_HOME = '/home/devuser';
|
|
84
89
|
const CONTAINER_SHELL_CONFIG_MOUNT = `${CONTAINER_HOME}/.host-shell-config`;
|
|
85
|
-
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>]
|
|
90
|
+
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh]
|
|
86
91
|
|
|
87
92
|
Host aliases:
|
|
88
93
|
${'~'}/.agent-infra/aliases/sandbox.sh is auto-created on first run and exposed
|
|
@@ -93,6 +98,11 @@ Host aliases:
|
|
|
93
98
|
type SandboxCreateConfig = ReturnType<typeof loadConfig>;
|
|
94
99
|
type PreparedDockerfile = ReturnType<typeof prepareDockerfile>;
|
|
95
100
|
type ResolvedTool = { tool: SandboxTool; dir: string };
|
|
101
|
+
type TmpfsSeedPlanEntry = {
|
|
102
|
+
stagingPath: string;
|
|
103
|
+
targetPath: string;
|
|
104
|
+
volumeArgs: string[];
|
|
105
|
+
};
|
|
96
106
|
type RuntimeCheck = { name: string; cmd: string[] };
|
|
97
107
|
type JsonObject = Record<string, unknown>;
|
|
98
108
|
type GpgCache = { pub: Buffer; sec: Buffer } | null;
|
|
@@ -115,16 +125,6 @@ type HostShellConfig = {
|
|
|
115
125
|
mounts: Array<{ hostPath: string; containerPath: string }>;
|
|
116
126
|
};
|
|
117
127
|
|
|
118
|
-
function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTool[]): string {
|
|
119
|
-
return createHash('sha256')
|
|
120
|
-
.update(JSON.stringify({
|
|
121
|
-
dockerfile: preparedDockerfile.signature,
|
|
122
|
-
tools: imageSignatureFields(tools)
|
|
123
|
-
}))
|
|
124
|
-
.digest('hex')
|
|
125
|
-
.slice(0, 12);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
128
|
function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
|
|
129
129
|
return tools.map((tool) => {
|
|
130
130
|
const candidates = toolConfigDirCandidates(tool, config.project, branch);
|
|
@@ -327,12 +327,14 @@ export function writeSanitizedGitconfig({
|
|
|
327
327
|
const SHELL_CONFIG_SYMLINKS = ['.gitconfig', '.gitignore_global', '.stCommitMsg', '.bash_aliases'];
|
|
328
328
|
|
|
329
329
|
export function ensureShellConfigSymlinks(engine: string, container: string, execFn: EngineExecFn = execEngine): void {
|
|
330
|
-
// Idempotent symlink setup.
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
330
|
+
// Idempotent symlink setup. Avoid a shell command here because Windows .cmd
|
|
331
|
+
// engine shims would interpret metacharacters in a `bash -lc` script before
|
|
332
|
+
// forwarding it to Docker.
|
|
333
|
+
for (const file of SHELL_CONFIG_SYMLINKS) {
|
|
334
|
+
execFn(engine, 'docker', [
|
|
335
|
+
'exec', container, 'ln', '-sf', `.host-shell-config/${file}`, `${CONTAINER_HOME}/${file}`
|
|
336
|
+
], { stdio: 'ignore' });
|
|
337
|
+
}
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
export function prepareHostShellConfig({
|
|
@@ -634,6 +636,7 @@ export function buildContainerEnvFile(
|
|
|
634
636
|
chmodFn?: typeof fs.chmodSync;
|
|
635
637
|
rmFn?: typeof fs.rmSync;
|
|
636
638
|
tmpDir?: string;
|
|
639
|
+
runSafeFn?: DirectRunSafeFn;
|
|
637
640
|
} = {}
|
|
638
641
|
): { dockerArgs: string[]; cleanup: () => void } {
|
|
639
642
|
const {
|
|
@@ -641,11 +644,15 @@ export function buildContainerEnvFile(
|
|
|
641
644
|
writeFileFn = fs.writeFileSync,
|
|
642
645
|
chmodFn = fs.chmodSync,
|
|
643
646
|
rmFn = fs.rmSync,
|
|
644
|
-
tmpDir = os.tmpdir()
|
|
647
|
+
tmpDir = os.tmpdir(),
|
|
648
|
+
runSafeFn = runSafe
|
|
645
649
|
} = options;
|
|
646
650
|
|
|
647
651
|
const entries: Array<[string, string]> = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
|
|
648
|
-
|
|
652
|
+
let ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
|
|
653
|
+
if (!ghToken && engine === 'wsl2') {
|
|
654
|
+
ghToken = runSafeFn('gh', ['auth', 'token']);
|
|
655
|
+
}
|
|
649
656
|
if (ghToken) {
|
|
650
657
|
entries.push(['GH_TOKEN', ghToken]);
|
|
651
658
|
}
|
|
@@ -1191,43 +1198,43 @@ export function buildImage(
|
|
|
1191
1198
|
runFn = runEngine,
|
|
1192
1199
|
runSafeFn = runSafeEngine,
|
|
1193
1200
|
runVerboseFn = runVerboseEngine,
|
|
1194
|
-
env = process.env
|
|
1201
|
+
env = process.env,
|
|
1202
|
+
refresh = false,
|
|
1203
|
+
lastRefresh
|
|
1195
1204
|
}: {
|
|
1196
1205
|
engine?: string;
|
|
1197
1206
|
runFn?: EngineRunFn;
|
|
1198
1207
|
runSafeFn?: EngineRunSafeFn;
|
|
1199
1208
|
runVerboseFn?: EngineRunVerboseFn;
|
|
1200
1209
|
env?: NodeJS.ProcessEnv;
|
|
1210
|
+
refresh?: boolean;
|
|
1211
|
+
lastRefresh?: number;
|
|
1201
1212
|
} = {}
|
|
1202
1213
|
): void {
|
|
1203
1214
|
const selectedEngine = engine ?? detectEngine({ engine: config.engine });
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1215
|
+
runVerboseFn(
|
|
1216
|
+
selectedEngine,
|
|
1217
|
+
'docker',
|
|
1218
|
+
buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
|
|
1219
|
+
engine: selectedEngine,
|
|
1220
|
+
runFn,
|
|
1221
|
+
runSafeFn,
|
|
1222
|
+
env,
|
|
1223
|
+
refresh,
|
|
1224
|
+
lastRefresh
|
|
1225
|
+
}),
|
|
1226
|
+
{ cwd: config.repoRoot }
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1210
1229
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
'
|
|
1214
|
-
|
|
1215
|
-
'--
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
'--build-arg',
|
|
1220
|
-
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
1221
|
-
'--build-arg',
|
|
1222
|
-
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
1223
|
-
'--label',
|
|
1224
|
-
sandboxLabel(config),
|
|
1225
|
-
'--label',
|
|
1226
|
-
`${sandboxImageConfigLabel(config)}=${imageSignature}`,
|
|
1227
|
-
'-f',
|
|
1228
|
-
toEnginePath(selectedEngine, dockerfilePath),
|
|
1229
|
-
toEnginePath(selectedEngine, config.repoRoot)
|
|
1230
|
-
], { cwd: config.repoRoot });
|
|
1230
|
+
function readImageLabels(config: Pick<SandboxCreateConfig, 'imageName'> & Pick<SandboxCreateConfig, 'project'>, engine: string): Record<string, string> {
|
|
1231
|
+
return parseImageLabels(runSafeEngine(engine, 'docker', [
|
|
1232
|
+
'image',
|
|
1233
|
+
'inspect',
|
|
1234
|
+
'--format',
|
|
1235
|
+
'{{ json .Config.Labels }}',
|
|
1236
|
+
config.imageName
|
|
1237
|
+
]));
|
|
1231
1238
|
}
|
|
1232
1239
|
|
|
1233
1240
|
export async function create(args: string[]): Promise<void> {
|
|
@@ -1238,6 +1245,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1238
1245
|
options: {
|
|
1239
1246
|
cpu: { type: 'string' },
|
|
1240
1247
|
memory: { type: 'string' },
|
|
1248
|
+
'no-refresh': { type: 'boolean' },
|
|
1241
1249
|
help: { type: 'boolean', short: 'h' }
|
|
1242
1250
|
}
|
|
1243
1251
|
});
|
|
@@ -1287,7 +1295,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1287
1295
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
1288
1296
|
const preparedDockerfile = prepareDockerfile(effectiveConfig);
|
|
1289
1297
|
const baseBranch = base ?? runSafe('git', ['-C', effectiveConfig.repoRoot, 'branch', '--show-current']);
|
|
1290
|
-
const expectedImageSignature =
|
|
1298
|
+
const expectedImageSignature = buildImageSignature(preparedDockerfile, tools);
|
|
1291
1299
|
const engine = detectEngine(effectiveConfig);
|
|
1292
1300
|
|
|
1293
1301
|
p.intro(pc.cyan('AI Sandbox'));
|
|
@@ -1310,27 +1318,53 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1310
1318
|
p.log.success('Docker is ready');
|
|
1311
1319
|
|
|
1312
1320
|
const imageExists = runOkEngine(engine, 'docker', ['image', 'inspect', effectiveConfig.imageName]);
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
])
|
|
1321
|
-
|
|
1322
|
-
const needsImageBuild =
|
|
1321
|
+
const imageLabels = imageExists ? readImageLabels(effectiveConfig, engine) : {};
|
|
1322
|
+
const currentImageSignature = imageLabels[sandboxImageConfigLabel(effectiveConfig)] ?? '';
|
|
1323
|
+
const currentLastRefresh = parseRefreshTimestamp(imageLabels[sandboxImageRefreshLabel(effectiveConfig)] ?? '');
|
|
1324
|
+
const signatureStale = !imageExists || currentImageSignature !== expectedImageSignature;
|
|
1325
|
+
const now = Date.now();
|
|
1326
|
+
const refreshDue = imageExists
|
|
1327
|
+
&& !signatureStale
|
|
1328
|
+
&& !isRefreshDisabled(process.env, values['no-refresh'] ?? false)
|
|
1329
|
+
&& isRefreshDue(currentLastRefresh, now, effectiveConfig.refreshIntervalDays);
|
|
1330
|
+
const needsImageBuild = signatureStale || refreshDue;
|
|
1323
1331
|
|
|
1324
1332
|
if (needsImageBuild) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1333
|
+
const buildRefresh = !imageExists || refreshDue;
|
|
1334
|
+
const buildLastRefresh = buildRefresh ? now : currentLastRefresh || 0;
|
|
1335
|
+
|
|
1336
|
+
p.log.step(
|
|
1337
|
+
refreshDue
|
|
1338
|
+
? 'Refreshing stale image...'
|
|
1339
|
+
: imageExists
|
|
1340
|
+
? 'Rebuilding stale image...'
|
|
1341
|
+
: 'Building image for first use...'
|
|
1332
1342
|
);
|
|
1333
|
-
|
|
1343
|
+
try {
|
|
1344
|
+
buildImage(
|
|
1345
|
+
effectiveConfig,
|
|
1346
|
+
tools,
|
|
1347
|
+
preparedDockerfile.path,
|
|
1348
|
+
expectedImageSignature,
|
|
1349
|
+
{ engine, refresh: buildRefresh, lastRefresh: buildLastRefresh }
|
|
1350
|
+
);
|
|
1351
|
+
p.log.success(
|
|
1352
|
+
refreshDue
|
|
1353
|
+
? 'Image refreshed'
|
|
1354
|
+
: imageExists
|
|
1355
|
+
? 'Image rebuilt'
|
|
1356
|
+
: 'Image built'
|
|
1357
|
+
);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if (refreshDue && !signatureStale && imageExists) {
|
|
1360
|
+
p.log.warn(
|
|
1361
|
+
'Scheduled sandbox image refresh failed; continuing with the existing image. ' +
|
|
1362
|
+
commandErrorMessage(error)
|
|
1363
|
+
);
|
|
1364
|
+
} else {
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1334
1368
|
} else {
|
|
1335
1369
|
p.log.step(`Using existing image ${effectiveConfig.imageName}`);
|
|
1336
1370
|
}
|
|
@@ -1471,6 +1505,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1471
1505
|
: null;
|
|
1472
1506
|
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine);
|
|
1473
1507
|
let hostShellConfig: HostShellConfig;
|
|
1508
|
+
let tmpfsSeedPlan: TmpfsSeedPlanEntry[] = [];
|
|
1474
1509
|
try {
|
|
1475
1510
|
const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
1476
1511
|
if (claudeCodeEntry) {
|
|
@@ -1511,22 +1546,24 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1511
1546
|
'-v',
|
|
1512
1547
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1513
1548
|
]);
|
|
1514
|
-
//
|
|
1515
|
-
//
|
|
1516
|
-
// the
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
// sessions/ from a previous bind-mount era) must NOT be re-mounted,
|
|
1523
|
-
// or the high-churn writes would land on the host SSD again.
|
|
1524
|
-
const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1525
|
-
(tool.tmpfs?.seed ?? []).flatMap((entry) => {
|
|
1549
|
+
// Mount each declared seed read-only at an isolated staging path.
|
|
1550
|
+
// After docker run mounts the empty tmpfs, the container's default
|
|
1551
|
+
// user copies these entries into it so the runtime targets are normal,
|
|
1552
|
+
// writable files rather than nested mount points. The allowlist keeps
|
|
1553
|
+
// stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
|
|
1554
|
+
// being exposed or written through.
|
|
1555
|
+
tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1556
|
+
(tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
|
|
1526
1557
|
const hostPath = path.join(dir, entry);
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1558
|
+
if (!fs.existsSync(hostPath)) {
|
|
1559
|
+
return [];
|
|
1560
|
+
}
|
|
1561
|
+
const stagingPath = path.posix.join('/run/agent-infra/tmpfs-seeds', tool.id, String(index));
|
|
1562
|
+
return [{
|
|
1563
|
+
stagingPath,
|
|
1564
|
+
targetPath: path.posix.join(tool.containerMount, entry),
|
|
1565
|
+
volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
|
|
1566
|
+
}];
|
|
1530
1567
|
})
|
|
1531
1568
|
);
|
|
1532
1569
|
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
@@ -1579,12 +1616,10 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1579
1616
|
path.join(effectiveConfig.repoRoot, '.git'),
|
|
1580
1617
|
`${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`
|
|
1581
1618
|
),
|
|
1582
|
-
'-v',
|
|
1583
|
-
volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
|
|
1584
1619
|
...dotfilesMount,
|
|
1585
1620
|
...toolVolumes,
|
|
1586
1621
|
...tmpfsArgs,
|
|
1587
|
-
...
|
|
1622
|
+
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1588
1623
|
...liveMountVolumes,
|
|
1589
1624
|
...shellConfigVolumes,
|
|
1590
1625
|
...envFile.dockerArgs,
|
|
@@ -1597,6 +1632,15 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1597
1632
|
envFile.cleanup();
|
|
1598
1633
|
}
|
|
1599
1634
|
|
|
1635
|
+
for (const { stagingPath, targetPath } of tmpfsSeedPlan) {
|
|
1636
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1637
|
+
'exec', container, 'mkdir', '-p', path.posix.dirname(targetPath)
|
|
1638
|
+
]);
|
|
1639
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1640
|
+
'exec', container, 'cp', '-R', '--', stagingPath, targetPath
|
|
1641
|
+
]);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1600
1644
|
// Belt-and-suspenders: re-create the four shell-config symlinks at
|
|
1601
1645
|
// runtime so users with a custom `sandbox.dockerfile` (which won't
|
|
1602
1646
|
// include the ai-tools.dockerfile symlink fragment) still get
|