@fitlab-ai/agent-infra 0.8.3 → 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 +1 -1
- package/dist/lib/cp.js +56 -15
- package/dist/lib/defaults.json +1 -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 +39 -23
- package/dist/lib/sandbox/runtimes/base.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/lib/cp.ts +56 -15
- package/lib/defaults.json +1 -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 +48 -24
- package/lib/sandbox/runtimes/base.dockerfile +3 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- package/package.json +1 -1
- 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 +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- 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/testing-discipline.en.md +3 -41
- package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
- package/templates/.agents/scripts/validate-artifact.js +93 -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 +27 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +27 -3
- 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-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 +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 +29 -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/README.zh-CN.md
CHANGED
|
@@ -207,3 +207,13 @@ my-project/
|
|
|
207
207
|
## 许可协议
|
|
208
208
|
|
|
209
209
|
[MIT](License.txt)
|
|
210
|
+
|
|
211
|
+
## 社区
|
|
212
|
+
|
|
213
|
+
- 微信公众号:`FitFramework`
|
|
214
|
+
- QQ 技术交流群:`1029802553`
|
|
215
|
+
|
|
216
|
+
<p align="center">
|
|
217
|
+
<img src="./assets/qrcode_for_wechat_gh.png" alt="FitFramework 微信公众号二维码" width="250">
|
|
218
|
+
<img src="./assets/qrcode_for_qq_01.png" alt="FitFramework QQ 技术交流群二维码" width="250">
|
|
219
|
+
</p>
|
package/bin/cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructur
|
|
|
15
15
|
Usage: ai <command> [options]
|
|
16
16
|
|
|
17
17
|
Commands:
|
|
18
|
-
cp <ssh-alias> Copy local clipboard image to a remote macOS
|
|
18
|
+
cp <ssh-alias> Copy local clipboard image to a remote macOS clipboard or Linux sandbox over SSH
|
|
19
19
|
decide Record a human decision for a task HD item
|
|
20
20
|
help Show this help message
|
|
21
21
|
init Initialize a new project with update-agent-infra seed command
|
package/dist/bin/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructur
|
|
|
19
19
|
Usage: ai <command> [options]
|
|
20
20
|
|
|
21
21
|
Commands:
|
|
22
|
-
cp <ssh-alias> Copy local clipboard image to a remote macOS
|
|
22
|
+
cp <ssh-alias> Copy local clipboard image to a remote macOS clipboard or Linux sandbox over SSH
|
|
23
23
|
decide Record a human decision for a task HD item
|
|
24
24
|
help Show this help message
|
|
25
25
|
init Initialize a new project with update-agent-infra seed command
|
package/dist/lib/cp.js
CHANGED
|
@@ -5,7 +5,8 @@ import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { createClipboardAdapter } from './sandbox/clipboard/index.js';
|
|
8
|
-
|
|
8
|
+
import { RECEIVER_CAPABILITY, receiveRemoteClipboardPng } from './sandbox/clipboard/inbox.js';
|
|
9
|
+
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS clipboard or Linux sandbox over SSH.\n';
|
|
9
10
|
const COMMAND_TIMEOUT_MS = 30_000;
|
|
10
11
|
export function runCommand(cmd, args, input) {
|
|
11
12
|
const result = spawnSync(cmd, args, {
|
|
@@ -23,6 +24,22 @@ export function runCommand(cmd, args, input) {
|
|
|
23
24
|
}
|
|
24
25
|
export async function cmdCp(args, deps = {}) {
|
|
25
26
|
const { platform = currentPlatform(), createAdapter = createClipboardAdapter, spawnFn = runCommand, randomId = randomUUID, mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, rmFn = fs.rmSync, tmpdir = defaultTmpdir, writeStdout = (chunk) => process.stdout.write(chunk), writeStderr = (chunk) => process.stderr.write(chunk) } = deps;
|
|
27
|
+
if (args.length === 1 && args[0] === '--remote-receive-capability') {
|
|
28
|
+
if (platform === 'linux')
|
|
29
|
+
writeStdout(`${RECEIVER_CAPABILITY}\n`);
|
|
30
|
+
return platform === 'linux' ? 0 : 1;
|
|
31
|
+
}
|
|
32
|
+
if (args.length === 2 && args[0] === '--remote-receive-v1') {
|
|
33
|
+
try {
|
|
34
|
+
const result = receiveRemoteClipboardPng(process.env.HOME || process.env.USERPROFILE || '', args[1], platform);
|
|
35
|
+
writeStdout(`${result}\n`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
writeStderr(`${error instanceof Error ? error.message : 'clipboard receive failed'}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
26
43
|
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
27
44
|
writeStdout(USAGE);
|
|
28
45
|
return 0;
|
|
@@ -62,6 +79,34 @@ export async function cmdCp(args, deps = {}) {
|
|
|
62
79
|
const localPng = path.join(localTmpDir, 'clipboard.png');
|
|
63
80
|
writeFileFn(localPng, png);
|
|
64
81
|
remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
|
|
82
|
+
const remoteSystem = spawnFn('ssh', [
|
|
83
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'uname', '-s'
|
|
84
|
+
]);
|
|
85
|
+
if (remoteSystem.status !== 0) {
|
|
86
|
+
writeStderr(`failed to detect remote platform on ${alias}:\n${commandDetail(remoteSystem)}\n`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const remotePlatform = remoteSystem.stdout.trim();
|
|
90
|
+
if (remotePlatform !== 'Darwin' && remotePlatform !== 'Linux') {
|
|
91
|
+
writeStderr(`unsupported remote platform on ${alias}: ${remotePlatform || 'unknown'}\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
if (remotePlatform === 'Linux') {
|
|
95
|
+
const homeCheck = spawnFn('ssh', [
|
|
96
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'test', '-d', '$HOME/.agent-infra'
|
|
97
|
+
]);
|
|
98
|
+
if (homeCheck.status !== 0) {
|
|
99
|
+
writeStderr(`remote agent-infra installation is missing on ${alias}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const capability = spawnFn('ssh', [
|
|
103
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-capability'
|
|
104
|
+
]);
|
|
105
|
+
if (capability.status !== 0 || capability.stderr !== '' || capability.stdout.replace(/\n$/u, '') !== RECEIVER_CAPABILITY) {
|
|
106
|
+
writeStderr(`remote agent-infra clipboard receiver is unavailable or incompatible on ${alias}; update agent-infra\n`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
65
110
|
const upload = spawnFn('scp', [
|
|
66
111
|
'-o',
|
|
67
112
|
'BatchMode=yes',
|
|
@@ -75,24 +120,20 @@ export async function cmdCp(args, deps = {}) {
|
|
|
75
120
|
return 1;
|
|
76
121
|
}
|
|
77
122
|
uploaded = true;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'-o',
|
|
86
|
-
'ConnectTimeout=10',
|
|
87
|
-
alias,
|
|
88
|
-
'osascript',
|
|
89
|
-
'-'
|
|
90
|
-
], remoteSetScript(remotePath));
|
|
123
|
+
const setRemote = remotePlatform === 'Darwin'
|
|
124
|
+
? spawnFn('ssh', [
|
|
125
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'osascript', '-'
|
|
126
|
+
], remoteSetScript(remotePath))
|
|
127
|
+
: spawnFn('ssh', [
|
|
128
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-v1', remotePath
|
|
129
|
+
]);
|
|
91
130
|
if (setRemote.status !== 0) {
|
|
92
131
|
writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
|
|
93
132
|
return 1;
|
|
94
133
|
}
|
|
95
|
-
writeStdout(
|
|
134
|
+
writeStdout(remotePlatform === 'Linux'
|
|
135
|
+
? `uploaded clipboard image to ${alias}; return to the SSH session and press Ctrl+V\n`
|
|
136
|
+
: `copied clipboard image to ${alias}\n`);
|
|
96
137
|
return 0;
|
|
97
138
|
}
|
|
98
139
|
finally {
|
package/dist/lib/defaults.json
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { loadConfig } from './config.js';
|
|
3
3
|
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from './constants.js';
|
|
4
4
|
import { detectEngine } from './engine.js';
|
|
5
|
+
import { commandForEngine } from './shell.js';
|
|
5
6
|
import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.js';
|
|
6
7
|
import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
|
|
7
8
|
async function spawnCapture(file, args) {
|
|
@@ -49,12 +50,12 @@ function buildRunScript(params) {
|
|
|
49
50
|
return `#!/bin/sh
|
|
50
51
|
set -u
|
|
51
52
|
cd /workspace
|
|
52
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
53
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/started_at
|
|
53
54
|
printf '%s\\n' running > ${runDir}/status
|
|
54
55
|
${command}
|
|
55
56
|
code=$?
|
|
56
57
|
printf '%s\\n' "$code" > ${runDir}/exit_code
|
|
57
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
58
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/finished_at
|
|
58
59
|
if [ "$code" -eq 0 ]; then
|
|
59
60
|
printf '%s\\n' completed > ${runDir}/status
|
|
60
61
|
else
|
|
@@ -144,7 +145,8 @@ export async function runInSandbox(request, options = {}) {
|
|
|
144
145
|
'-lc',
|
|
145
146
|
buildTmuxLauncher({ request, runId })
|
|
146
147
|
];
|
|
147
|
-
const
|
|
148
|
+
const command = commandForEngine(engine, 'docker', dockerArgs);
|
|
149
|
+
const result = await (options.spawn ?? spawnCapture)(command.cmd, command.args);
|
|
148
150
|
return {
|
|
149
151
|
...result,
|
|
150
152
|
run: {
|
|
@@ -3,6 +3,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { createClipboardAdapter } from './index.js';
|
|
4
4
|
import { buildBracketedPaste, CtrlVDetector } from './keys.js';
|
|
5
5
|
import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from './paths.js';
|
|
6
|
+
import { isClipboardInboxReadable, readPendingClipboardPath } from './inbox.js';
|
|
6
7
|
import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from '../shell.js';
|
|
7
8
|
import { loadNodePty } from './node-pty.js';
|
|
8
9
|
const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
|
|
@@ -37,13 +38,10 @@ export async function runInteractiveWithClipboardBridge(options) {
|
|
|
37
38
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
38
39
|
return fallback('host stdin/stdout is not a TTY');
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const available = adapter.available();
|
|
44
|
-
if (!available.ok) {
|
|
41
|
+
const available = adapter?.available() ?? { ok: false, reason: 'no clipboard adapter available on this platform' };
|
|
42
|
+
const inboxReady = isClipboardInboxReadable(home);
|
|
43
|
+
if (!available.ok && !inboxReady)
|
|
45
44
|
return fallback(available.reason);
|
|
46
|
-
}
|
|
47
45
|
if (!runOk(engine, 'docker', ['exec', container, 'sh', '-c', '[ -d /clipboard ] && [ -r /clipboard ]'])) {
|
|
48
46
|
return fallback('container /clipboard mount is missing; rebuild the sandbox to enable image paste');
|
|
49
47
|
}
|
|
@@ -69,7 +67,7 @@ export async function runInteractiveWithClipboardBridge(options) {
|
|
|
69
67
|
return runBridge({
|
|
70
68
|
child,
|
|
71
69
|
home,
|
|
72
|
-
adapter,
|
|
70
|
+
adapter: available.ok ? adapter : null,
|
|
73
71
|
writeStderr,
|
|
74
72
|
stdin,
|
|
75
73
|
stdout,
|
|
@@ -116,7 +114,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
116
114
|
function handleText(raw, target) {
|
|
117
115
|
const textAdapter = adapter;
|
|
118
116
|
const pastedText = extractSinglePathPaste(raw);
|
|
119
|
-
if (!textAdapter.readImageFromText || pastedText === null) {
|
|
117
|
+
if (!textAdapter || !textAdapter.readImageFromText || pastedText === null) {
|
|
120
118
|
target.write(raw);
|
|
121
119
|
return;
|
|
122
120
|
}
|
|
@@ -138,12 +136,17 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
138
136
|
}
|
|
139
137
|
function handleCtrlV(match, target) {
|
|
140
138
|
try {
|
|
139
|
+
const pendingPath = readPendingClipboardPath(home);
|
|
140
|
+
if (pendingPath) {
|
|
141
|
+
target.write(buildBracketedPaste(pendingPath));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
141
144
|
// readImagePng returns null both for "no image on clipboard" and for
|
|
142
145
|
// unexpected read failures; both cases forward the original Ctrl+V so
|
|
143
146
|
// the container app handles it as a regular keystroke. The throw branch
|
|
144
147
|
// below only fires on truly unexpected exceptions (e.g. fs write
|
|
145
148
|
// errors writing to the host clipboard dir).
|
|
146
|
-
const png = adapter
|
|
149
|
+
const png = adapter?.readImagePng() ?? null;
|
|
147
150
|
if (!png) {
|
|
148
151
|
target.write(match.raw);
|
|
149
152
|
return;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from './paths.js';
|
|
4
|
+
export const RECEIVER_CAPABILITY = 'AGENT_INFRA_CLIPBOARD_RECEIVER:v1';
|
|
5
|
+
const MARKER = '.pending.json';
|
|
6
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
7
|
+
const HASHED_PNG = /^[a-f0-9]{16}\.png$/u;
|
|
8
|
+
const REMOTE_TEMP = /^agent-infra-cp-[0-9a-f-]{36}\.png$/u;
|
|
9
|
+
export function isClipboardInboxReadable(home) {
|
|
10
|
+
const dir = clipboardHostDir(home);
|
|
11
|
+
try {
|
|
12
|
+
fs.accessSync(dir, fs.constants.R_OK);
|
|
13
|
+
return fs.statSync(dir).isDirectory();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function receiveRemoteClipboardPng(home, tempPath, platformName = process.platform) {
|
|
20
|
+
if (platformName !== 'linux')
|
|
21
|
+
throw new Error('remote clipboard receiver requires Linux');
|
|
22
|
+
if (path.dirname(tempPath) !== '/tmp' || !REMOTE_TEMP.test(path.basename(tempPath))) {
|
|
23
|
+
throw new Error('invalid clipboard upload path');
|
|
24
|
+
}
|
|
25
|
+
const signature = Buffer.alloc(PNG_SIGNATURE.length);
|
|
26
|
+
const fd = fs.openSync(tempPath, 'r');
|
|
27
|
+
let signatureBytes;
|
|
28
|
+
try {
|
|
29
|
+
signatureBytes = fs.readSync(fd, signature, 0, signature.length, 0);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
fs.closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
if (signatureBytes !== PNG_SIGNATURE.length || !signature.equals(PNG_SIGNATURE)) {
|
|
35
|
+
throw new Error('uploaded clipboard file is not a PNG');
|
|
36
|
+
}
|
|
37
|
+
const png = fs.readFileSync(tempPath);
|
|
38
|
+
const dir = clipboardHostDir(home);
|
|
39
|
+
if (!fs.existsSync(path.dirname(dir)))
|
|
40
|
+
throw new Error('agent-infra is not installed on this host');
|
|
41
|
+
const filename = pngClipboardFilename(png);
|
|
42
|
+
writeClipboardPngAtomic(dir, filename, png);
|
|
43
|
+
pruneClipboardDir(dir);
|
|
44
|
+
const markerTmp = path.join(dir, `.${MARKER}.${process.pid}.tmp`);
|
|
45
|
+
fs.writeFileSync(markerTmp, JSON.stringify({ version: 1, filename, createdAt: new Date().toISOString() }) + '\n', { mode: 0o600 });
|
|
46
|
+
fs.renameSync(markerTmp, path.join(dir, MARKER));
|
|
47
|
+
return containerClipboardPath(filename);
|
|
48
|
+
}
|
|
49
|
+
export function readPendingClipboardPath(home) {
|
|
50
|
+
const dir = clipboardHostDir(home);
|
|
51
|
+
const markerPath = path.join(dir, MARKER);
|
|
52
|
+
let markerText;
|
|
53
|
+
try {
|
|
54
|
+
markerText = fs.readFileSync(markerPath, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === 'ENOENT')
|
|
58
|
+
return null;
|
|
59
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
60
|
+
}
|
|
61
|
+
let marker;
|
|
62
|
+
try {
|
|
63
|
+
marker = JSON.parse(markerText);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
67
|
+
}
|
|
68
|
+
if (marker.version !== 1 || typeof marker.filename !== 'string' || !HASHED_PNG.test(marker.filename)) {
|
|
69
|
+
throw new Error('invalid clipboard pending marker');
|
|
70
|
+
}
|
|
71
|
+
const filePath = path.join(dir, marker.filename);
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.lstatSync(filePath);
|
|
74
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
75
|
+
throw new Error('pending clipboard image is not a regular file');
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
79
|
+
}
|
|
80
|
+
return containerClipboardPath(marker.filename);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -209,12 +209,14 @@ export function writeSanitizedGitconfig({ home, hostConfigDir, stripGpg, repoRoo
|
|
|
209
209
|
// Keep in sync with the symlink block in lib/sandbox/runtimes/ai-tools.dockerfile.
|
|
210
210
|
const SHELL_CONFIG_SYMLINKS = ['.gitconfig', '.gitignore_global', '.stCommitMsg', '.bash_aliases'];
|
|
211
211
|
export function ensureShellConfigSymlinks(engine, container, execFn = execEngine) {
|
|
212
|
-
// Idempotent symlink setup.
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
// Idempotent symlink setup. Avoid a shell command here because Windows .cmd
|
|
213
|
+
// engine shims would interpret metacharacters in a `bash -lc` script before
|
|
214
|
+
// forwarding it to Docker.
|
|
215
|
+
for (const file of SHELL_CONFIG_SYMLINKS) {
|
|
216
|
+
execFn(engine, 'docker', [
|
|
217
|
+
'exec', container, 'ln', '-sf', `.host-shell-config/${file}`, `${CONTAINER_HOME}/${file}`
|
|
218
|
+
], { stdio: 'ignore' });
|
|
219
|
+
}
|
|
218
220
|
}
|
|
219
221
|
export function prepareHostShellConfig({ home, project, branch, repoRoot }) {
|
|
220
222
|
const hostDir = hostShellConfigDir(home, project, branch);
|
|
@@ -424,9 +426,12 @@ function formatEnvFileEntry(key, value) {
|
|
|
424
426
|
return `${key}=${value}`;
|
|
425
427
|
}
|
|
426
428
|
export function buildContainerEnvFile(resolvedTools, engine, runSafeEngineFn = runSafeEngine, options = {}) {
|
|
427
|
-
const { mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, chmodFn = fs.chmodSync, rmFn = fs.rmSync, tmpDir = os.tmpdir() } = options;
|
|
429
|
+
const { mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, chmodFn = fs.chmodSync, rmFn = fs.rmSync, tmpDir = os.tmpdir(), runSafeFn = runSafe } = options;
|
|
428
430
|
const entries = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
|
|
429
|
-
|
|
431
|
+
let ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
|
|
432
|
+
if (!ghToken && engine === 'wsl2') {
|
|
433
|
+
ghToken = runSafeFn('gh', ['auth', 'token']);
|
|
434
|
+
}
|
|
430
435
|
if (ghToken) {
|
|
431
436
|
entries.push(['GH_TOKEN', ghToken]);
|
|
432
437
|
}
|
|
@@ -1145,6 +1150,7 @@ export async function create(args) {
|
|
|
1145
1150
|
: null;
|
|
1146
1151
|
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine);
|
|
1147
1152
|
let hostShellConfig;
|
|
1153
|
+
let tmpfsSeedPlan = [];
|
|
1148
1154
|
try {
|
|
1149
1155
|
const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
1150
1156
|
if (claudeCodeEntry) {
|
|
@@ -1181,21 +1187,23 @@ export async function create(args) {
|
|
|
1181
1187
|
'-v',
|
|
1182
1188
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1183
1189
|
]);
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
// the
|
|
1187
|
-
//
|
|
1188
|
-
//
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
// runtime files left in the host dir (e.g. a stale logs_2.sqlite or
|
|
1192
|
-
// sessions/ from a previous bind-mount era) must NOT be re-mounted,
|
|
1193
|
-
// or the high-churn writes would land on the host SSD again.
|
|
1194
|
-
const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry) => {
|
|
1190
|
+
// Mount each declared seed read-only at an isolated staging path.
|
|
1191
|
+
// After docker run mounts the empty tmpfs, the container's default
|
|
1192
|
+
// user copies these entries into it so the runtime targets are normal,
|
|
1193
|
+
// writable files rather than nested mount points. The allowlist keeps
|
|
1194
|
+
// stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
|
|
1195
|
+
// being exposed or written through.
|
|
1196
|
+
tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
|
|
1195
1197
|
const hostPath = path.join(dir, entry);
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1198
|
+
if (!fs.existsSync(hostPath)) {
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1201
|
+
const stagingPath = path.posix.join('/run/agent-infra/tmpfs-seeds', tool.id, String(index));
|
|
1202
|
+
return [{
|
|
1203
|
+
stagingPath,
|
|
1204
|
+
targetPath: path.posix.join(tool.containerMount, entry),
|
|
1205
|
+
volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
|
|
1206
|
+
}];
|
|
1199
1207
|
}));
|
|
1200
1208
|
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
|
|
1201
1209
|
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
@@ -1238,7 +1246,7 @@ export async function create(args) {
|
|
|
1238
1246
|
...dotfilesMount,
|
|
1239
1247
|
...toolVolumes,
|
|
1240
1248
|
...tmpfsArgs,
|
|
1241
|
-
...
|
|
1249
|
+
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1242
1250
|
...liveMountVolumes,
|
|
1243
1251
|
...shellConfigVolumes,
|
|
1244
1252
|
...envFile.dockerArgs,
|
|
@@ -1251,6 +1259,14 @@ export async function create(args) {
|
|
|
1251
1259
|
finally {
|
|
1252
1260
|
envFile.cleanup();
|
|
1253
1261
|
}
|
|
1262
|
+
for (const { stagingPath, targetPath } of tmpfsSeedPlan) {
|
|
1263
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1264
|
+
'exec', container, 'mkdir', '-p', path.posix.dirname(targetPath)
|
|
1265
|
+
]);
|
|
1266
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1267
|
+
'exec', container, 'cp', '-R', '--', stagingPath, targetPath
|
|
1268
|
+
]);
|
|
1269
|
+
}
|
|
1254
1270
|
// Belt-and-suspenders: re-create the four shell-config symlinks at
|
|
1255
1271
|
// runtime so users with a custom `sandbox.dockerfile` (which won't
|
|
1256
1272
|
// include the ai-tools.dockerfile symlink fragment) still get
|
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { getAdapter } from './engines/index.js';
|
|
4
5
|
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
5
6
|
function normalizeOptions(opts = {}, stdio) {
|
|
6
7
|
return {
|
|
@@ -114,6 +115,12 @@ export function commandForEngine(engine, cmd, args = []) {
|
|
|
114
115
|
const resolvedWrapper = resolveCommand('wsl.exe');
|
|
115
116
|
return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
|
|
116
117
|
}
|
|
118
|
+
if (cmd === 'docker') {
|
|
119
|
+
const context = getAdapter(engine).dockerContext;
|
|
120
|
+
if (context) {
|
|
121
|
+
return { cmd, args: ['--context', context, ...args] };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
117
124
|
return { cmd, args };
|
|
118
125
|
}
|
|
119
126
|
export function runEngine(engine, cmd, args, opts = {}) {
|
|
@@ -122,7 +129,8 @@ export function runEngine(engine, cmd, args, opts = {}) {
|
|
|
122
129
|
}
|
|
123
130
|
export function execEngine(engine, cmd, args, opts = {}) {
|
|
124
131
|
const command = commandForEngine(engine, cmd, args);
|
|
125
|
-
|
|
132
|
+
const resolved = resolveCommand(command.cmd);
|
|
133
|
+
return execFileSync(resolved, command.args, commandOptions(resolved, opts));
|
|
126
134
|
}
|
|
127
135
|
export function runOkEngine(engine, cmd, args, opts = {}) {
|
|
128
136
|
const command = commandForEngine(engine, cmd, args);
|
|
@@ -44,8 +44,8 @@ function createBuiltinTools(home, project) {
|
|
|
44
44
|
// codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
|
|
45
45
|
// a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
|
|
46
46
|
// codex home as tmpfs so those logs stay in RAM and die with the container.
|
|
47
|
-
//
|
|
48
|
-
//
|
|
47
|
+
// Seeded config (config.toml, model-catalogs) is copied into the tmpfs at
|
|
48
|
+
// startup; runtime files like logs_2.sqlite stay in RAM.
|
|
49
49
|
tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
|
|
50
50
|
hostLiveMounts: [
|
|
51
51
|
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
package/lib/cp.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { createClipboardAdapter, type ClipboardAdapter } from './sandbox/clipboard/index.ts';
|
|
8
|
+
import { RECEIVER_CAPABILITY, receiveRemoteClipboardPng } from './sandbox/clipboard/inbox.ts';
|
|
8
9
|
|
|
9
|
-
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS
|
|
10
|
+
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS clipboard or Linux sandbox over SSH.\n';
|
|
10
11
|
const COMMAND_TIMEOUT_MS = 30_000;
|
|
11
12
|
|
|
12
13
|
export type SpawnResult = {
|
|
@@ -65,6 +66,21 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
|
|
|
65
66
|
writeStderr = (chunk: string) => process.stderr.write(chunk)
|
|
66
67
|
} = deps;
|
|
67
68
|
|
|
69
|
+
if (args.length === 1 && args[0] === '--remote-receive-capability') {
|
|
70
|
+
if (platform === 'linux') writeStdout(`${RECEIVER_CAPABILITY}\n`);
|
|
71
|
+
return platform === 'linux' ? 0 : 1;
|
|
72
|
+
}
|
|
73
|
+
if (args.length === 2 && args[0] === '--remote-receive-v1') {
|
|
74
|
+
try {
|
|
75
|
+
const result = receiveRemoteClipboardPng(process.env.HOME || process.env.USERPROFILE || '', args[1]!, platform);
|
|
76
|
+
writeStdout(`${result}\n`);
|
|
77
|
+
return 0;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
writeStderr(`${error instanceof Error ? error.message : 'clipboard receive failed'}\n`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
69
85
|
writeStdout(USAGE);
|
|
70
86
|
return 0;
|
|
@@ -110,6 +126,35 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
|
|
|
110
126
|
writeFileFn(localPng, png);
|
|
111
127
|
|
|
112
128
|
remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
|
|
129
|
+
const remoteSystem = spawnFn('ssh', [
|
|
130
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'uname', '-s'
|
|
131
|
+
]);
|
|
132
|
+
if (remoteSystem.status !== 0) {
|
|
133
|
+
writeStderr(`failed to detect remote platform on ${alias}:\n${commandDetail(remoteSystem)}\n`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const remotePlatform = remoteSystem.stdout.trim();
|
|
137
|
+
if (remotePlatform !== 'Darwin' && remotePlatform !== 'Linux') {
|
|
138
|
+
writeStderr(`unsupported remote platform on ${alias}: ${remotePlatform || 'unknown'}\n`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
if (remotePlatform === 'Linux') {
|
|
142
|
+
const homeCheck = spawnFn('ssh', [
|
|
143
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'test', '-d', '$HOME/.agent-infra'
|
|
144
|
+
]);
|
|
145
|
+
if (homeCheck.status !== 0) {
|
|
146
|
+
writeStderr(`remote agent-infra installation is missing on ${alias}\n`);
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
const capability = spawnFn('ssh', [
|
|
150
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-capability'
|
|
151
|
+
]);
|
|
152
|
+
if (capability.status !== 0 || capability.stderr !== '' || capability.stdout.replace(/\n$/u, '') !== RECEIVER_CAPABILITY) {
|
|
153
|
+
writeStderr(`remote agent-infra clipboard receiver is unavailable or incompatible on ${alias}; update agent-infra\n`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
113
158
|
const upload = spawnFn('scp', [
|
|
114
159
|
'-o',
|
|
115
160
|
'BatchMode=yes',
|
|
@@ -124,25 +169,21 @@ export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number>
|
|
|
124
169
|
}
|
|
125
170
|
uploaded = true;
|
|
126
171
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
'-o',
|
|
135
|
-
'ConnectTimeout=10',
|
|
136
|
-
alias,
|
|
137
|
-
'osascript',
|
|
138
|
-
'-'
|
|
139
|
-
], remoteSetScript(remotePath));
|
|
172
|
+
const setRemote = remotePlatform === 'Darwin'
|
|
173
|
+
? spawnFn('ssh', [
|
|
174
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'osascript', '-'
|
|
175
|
+
], remoteSetScript(remotePath))
|
|
176
|
+
: spawnFn('ssh', [
|
|
177
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-v1', remotePath
|
|
178
|
+
]);
|
|
140
179
|
if (setRemote.status !== 0) {
|
|
141
180
|
writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
|
|
142
181
|
return 1;
|
|
143
182
|
}
|
|
144
183
|
|
|
145
|
-
writeStdout(
|
|
184
|
+
writeStdout(remotePlatform === 'Linux'
|
|
185
|
+
? `uploaded clipboard image to ${alias}; return to the SSH session and press Ctrl+V\n`
|
|
186
|
+
: `copied clipboard image to ${alias}\n`);
|
|
146
187
|
return 0;
|
|
147
188
|
} finally {
|
|
148
189
|
if (uploaded && remotePath) {
|
package/lib/defaults.json
CHANGED
package/lib/sandbox/capture.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { loadConfig } from './config.ts';
|
|
3
3
|
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from './constants.ts';
|
|
4
4
|
import { detectEngine } from './engine.ts';
|
|
5
|
+
import { commandForEngine } from './shell.ts';
|
|
5
6
|
import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.ts';
|
|
6
7
|
import {
|
|
7
8
|
fetchSandboxRows,
|
|
@@ -95,12 +96,12 @@ function buildRunScript(params: {
|
|
|
95
96
|
return `#!/bin/sh
|
|
96
97
|
set -u
|
|
97
98
|
cd /workspace
|
|
98
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
99
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/started_at
|
|
99
100
|
printf '%s\\n' running > ${runDir}/status
|
|
100
101
|
${command}
|
|
101
102
|
code=$?
|
|
102
103
|
printf '%s\\n' "$code" > ${runDir}/exit_code
|
|
103
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
104
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/finished_at
|
|
104
105
|
if [ "$code" -eq 0 ]; then
|
|
105
106
|
printf '%s\\n' completed > ${runDir}/status
|
|
106
107
|
else
|
|
@@ -202,7 +203,8 @@ export async function runInSandbox(
|
|
|
202
203
|
'-lc',
|
|
203
204
|
buildTmuxLauncher({ request, runId })
|
|
204
205
|
];
|
|
205
|
-
const
|
|
206
|
+
const command = commandForEngine(engine, 'docker', dockerArgs);
|
|
207
|
+
const result = await (options.spawn ?? spawnCapture)(command.cmd, command.args);
|
|
206
208
|
return {
|
|
207
209
|
...result,
|
|
208
210
|
run: {
|