@fitlab-ai/agent-infra 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/cli.ts +4 -4
- package/dist/bin/cli.js +4 -4
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/run/index.js +96 -5
- package/dist/lib/sandbox/capture.js +113 -4
- package/dist/lib/sandbox/commands/create.js +37 -4
- package/dist/lib/sandbox/commands/enter.js +7 -3
- package/dist/lib/sandbox/config.js +1 -1
- package/dist/lib/sandbox/credentials.js +42 -3
- package/dist/lib/sandbox/runtimes/base.dockerfile +13 -0
- package/dist/lib/sandbox/runtimes/node20.dockerfile +1 -1
- package/dist/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/dist/lib/sandbox/tools.js +9 -0
- package/dist/lib/server/adapters/feishu/index.js +5 -3
- package/dist/lib/server/adapters/feishu/transport.js +20 -2
- package/dist/lib/server/protocol.js +20 -4
- package/dist/lib/task/commands/log.js +36 -14
- package/dist/lib/task/commands/status.js +144 -45
- package/dist/lib/update.js +31 -0
- package/lib/defaults.json +1 -0
- package/lib/run/index.ts +119 -6
- package/lib/sandbox/capture.ts +136 -5
- package/lib/sandbox/commands/create.ts +42 -4
- package/lib/sandbox/commands/enter.ts +8 -2
- package/lib/sandbox/config.ts +1 -1
- package/lib/sandbox/credentials.ts +57 -3
- package/lib/sandbox/runtimes/base.dockerfile +13 -0
- package/lib/sandbox/runtimes/node20.dockerfile +1 -1
- package/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/lib/sandbox/tools.ts +9 -0
- package/lib/server/adapters/feishu/index.ts +5 -3
- package/lib/server/adapters/feishu/transport.ts +32 -3
- package/lib/server/protocol.ts +20 -4
- package/lib/task/commands/log.ts +36 -14
- package/lib/task/commands/status.ts +200 -58
- package/lib/update.ts +34 -0
- package/package.json +5 -4
- package/templates/.agents/README.en.md +3 -3
- package/templates/.agents/README.zh-CN.md +3 -3
- package/templates/.agents/rules/next-step-output.en.md +1 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +3 -3
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -3
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -3
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +11 -11
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +11 -11
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +16 -16
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +11 -11
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +11 -11
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +26 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/v/@fitlab-ai/agent-infra" alt="npm version"></a>
|
|
17
17
|
<a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/dm/@fitlab-ai/agent-infra" alt="npm downloads"></a>
|
|
18
18
|
<a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
19
|
-
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
|
|
19
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22.9.0-brightgreen?logo=node.js" alt="Node.js >= 22.9.0"></a>
|
|
20
20
|
<a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
|
|
21
21
|
<a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
|
|
22
22
|
<a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
|
package/README.zh-CN.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/v/@fitlab-ai/agent-infra" alt="npm version"></a>
|
|
17
17
|
<a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/dm/@fitlab-ai/agent-infra" alt="npm downloads"></a>
|
|
18
18
|
<a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
19
|
-
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
|
|
19
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22.9.0-brightgreen?logo=node.js" alt="Node.js >= 22.9.0"></a>
|
|
20
20
|
<a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
|
|
21
21
|
<a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
|
|
22
22
|
<a href="CONTRIBUTING.zh-CN.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
|
package/bin/cli.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { VERSION } from '../lib/version.ts';
|
|
3
3
|
|
|
4
4
|
// Node.js version check
|
|
5
|
-
const [major = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
|
|
6
|
-
if (major < 22) {
|
|
5
|
+
const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
|
|
6
|
+
if (major < 22 || (major === 22 && minor < 9)) {
|
|
7
7
|
process.stderr.write(
|
|
8
|
-
`agent-infra requires Node.js >= 22 (current: ${process.version})\n`
|
|
8
|
+
`agent-infra requires Node.js >= 22.9.0 (current: ${process.version})\n`
|
|
9
9
|
);
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
@@ -20,7 +20,7 @@ Commands:
|
|
|
20
20
|
help Show this help message
|
|
21
21
|
init Initialize a new project with update-agent-infra seed command
|
|
22
22
|
merge Merge tasks from another workspace directory (active/blocked/completed/archive)
|
|
23
|
-
run
|
|
23
|
+
run Schedule a lifecycle skill in the task sandbox tmux session
|
|
24
24
|
sandbox, s Manage Docker-based AI sandboxes
|
|
25
25
|
server Run the local AI collaboration daemon (start/stop/status/logs)
|
|
26
26
|
task, t Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
|
package/dist/bin/cli.js
CHANGED
|
@@ -9,9 +9,9 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
};
|
|
10
10
|
import { VERSION } from "../lib/version.js";
|
|
11
11
|
// Node.js version check
|
|
12
|
-
const [major = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
|
|
13
|
-
if (major < 22) {
|
|
14
|
-
process.stderr.write(`agent-infra requires Node.js >= 22 (current: ${process.version})\n`);
|
|
12
|
+
const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
|
|
13
|
+
if (major < 22 || (major === 22 && minor < 9)) {
|
|
14
|
+
process.stderr.write(`agent-infra requires Node.js >= 22.9.0 (current: ${process.version})\n`);
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
17
|
const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
|
|
@@ -24,7 +24,7 @@ Commands:
|
|
|
24
24
|
help Show this help message
|
|
25
25
|
init Initialize a new project with update-agent-infra seed command
|
|
26
26
|
merge Merge tasks from another workspace directory (active/blocked/completed/archive)
|
|
27
|
-
run
|
|
27
|
+
run Schedule a lifecycle skill in the task sandbox tmux session
|
|
28
28
|
sandbox, s Manage Docker-based AI sandboxes
|
|
29
29
|
server Run the local AI collaboration daemon (start/stop/status/logs)
|
|
30
30
|
task, t Read-only views over .agents/workspace tasks (cat / files / grep / log / ls / show / status)
|
package/dist/lib/defaults.json
CHANGED
package/dist/lib/run/index.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { loadServerConfig } from "../server/config.js";
|
|
2
4
|
import { resolveTaskBranch } from "../sandbox/task-resolver.js";
|
|
3
|
-
import { runInSandbox } from "../sandbox/capture.js";
|
|
5
|
+
import { createRunId, runInSandbox } from "../sandbox/capture.js";
|
|
6
|
+
import { loadShortIdByTaskId, normalizeShortIdInput } from "../task/short-id.js";
|
|
4
7
|
import { buildTuiCommand, renderPrompt, selectTui } from "./tui.js";
|
|
5
8
|
import { getSkillRunSpec } from "./skills.js";
|
|
6
9
|
import { runHostCommand } from "./host.js";
|
|
7
10
|
const USAGE = `Usage: ai run <skill> [task-ref] [args...] [--tui <name>]
|
|
8
11
|
|
|
12
|
+
Task skills are scheduled inside the sandbox tmux session; ai run returns once
|
|
13
|
+
the tmux window is created.
|
|
14
|
+
|
|
9
15
|
Examples:
|
|
10
16
|
ai run create-task "describe the task" --tui codex
|
|
11
17
|
ai run code-task #7 --tui codex`;
|
|
@@ -56,6 +62,83 @@ function assertAllowedByConfig(skill, commandConfig) {
|
|
|
56
62
|
throw new Error(`Skill '${skill}' is not allowed by command.allowedSkills`);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
66
|
+
function readShortIdLength(repoRoot) {
|
|
67
|
+
try {
|
|
68
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
69
|
+
const value = cfg?.task?.shortIdLength;
|
|
70
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 1)
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Use the project default when config is absent in lightweight tests.
|
|
75
|
+
}
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
function readActiveShortIdRegistry(repoRoot) {
|
|
79
|
+
const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', '.short-ids.json');
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
82
|
+
return data && typeof data === 'object' && data.ids && typeof data.ids === 'object' ? data.ids : {};
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function resolveActiveTaskIdentity(taskRef, repoRoot) {
|
|
89
|
+
let taskId = null;
|
|
90
|
+
let resolvedTaskRef = taskRef;
|
|
91
|
+
if (TASK_ID_RE.test(taskRef)) {
|
|
92
|
+
taskId = taskRef;
|
|
93
|
+
resolvedTaskRef = loadShortIdByTaskId(repoRoot).get(taskId) ?? taskRef;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const normalized = normalizeShortIdInput(taskRef, { shortIdLength: readShortIdLength(repoRoot) });
|
|
97
|
+
if (normalized.kind !== 'shortId')
|
|
98
|
+
return null;
|
|
99
|
+
resolvedTaskRef = normalized.value;
|
|
100
|
+
taskId = readActiveShortIdRegistry(repoRoot)[normalized.value.slice(1)] ?? null;
|
|
101
|
+
}
|
|
102
|
+
if (!taskId)
|
|
103
|
+
return null;
|
|
104
|
+
const taskDir = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
105
|
+
if (!fs.existsSync(path.join(taskDir, 'task.md')))
|
|
106
|
+
return null;
|
|
107
|
+
return { taskId, taskDir, taskRef: resolvedTaskRef };
|
|
108
|
+
}
|
|
109
|
+
function formatLocalTimestamp(date = new Date()) {
|
|
110
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
111
|
+
const year = date.getFullYear();
|
|
112
|
+
const month = pad(date.getMonth() + 1);
|
|
113
|
+
const day = pad(date.getDate());
|
|
114
|
+
const hour = pad(date.getHours());
|
|
115
|
+
const minute = pad(date.getMinutes());
|
|
116
|
+
const second = pad(date.getSeconds());
|
|
117
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
118
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
119
|
+
const offsetHour = pad(Math.floor(Math.abs(offsetMinutes) / 60));
|
|
120
|
+
const offsetMinute = pad(Math.abs(offsetMinutes) % 60);
|
|
121
|
+
return `${year}-${month}-${day} ${hour}:${minute}:${second}${sign}${offsetHour}:${offsetMinute}`;
|
|
122
|
+
}
|
|
123
|
+
function writeRunRecord(params) {
|
|
124
|
+
const runsDir = path.join(params.identity.taskDir, 'runs');
|
|
125
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
126
|
+
const record = {
|
|
127
|
+
version: 1,
|
|
128
|
+
run_id: params.run.runId,
|
|
129
|
+
task_id: params.identity.taskId,
|
|
130
|
+
task_ref: params.identity.taskRef,
|
|
131
|
+
branch: params.branch,
|
|
132
|
+
engine: params.run.engine,
|
|
133
|
+
container: params.run.container,
|
|
134
|
+
run_dir: params.run.runDir,
|
|
135
|
+
status_file: `${params.run.runDir}/status`,
|
|
136
|
+
log_file: `${params.run.runDir}/output.log`,
|
|
137
|
+
created_at: formatLocalTimestamp(),
|
|
138
|
+
command: params.command
|
|
139
|
+
};
|
|
140
|
+
fs.writeFileSync(path.join(runsDir, `${params.run.runId}.json`), `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
141
|
+
}
|
|
59
142
|
export async function runSkill(args, options = {}) {
|
|
60
143
|
const parsed = parseRunArgs(args);
|
|
61
144
|
const config = options.command ? null : loadServerConfig({ rootDir: options.repoRoot });
|
|
@@ -72,13 +155,21 @@ export async function runSkill(args, options = {}) {
|
|
|
72
155
|
}
|
|
73
156
|
const repoRoot = options.repoRoot ?? config?.repoRoot ?? process.cwd();
|
|
74
157
|
const branch = resolveTaskBranch(parsed.taskRef, repoRoot);
|
|
75
|
-
const
|
|
76
|
-
const
|
|
158
|
+
const identity = resolveActiveTaskIdentity(parsed.taskRef, repoRoot);
|
|
159
|
+
const runId = identity ? createRunId() : undefined;
|
|
160
|
+
const writeStdout = options.writeStdout ?? ((chunk) => process.stdout.write(chunk));
|
|
161
|
+
const writeStderr = options.writeStderr ?? ((chunk) => process.stderr.write(chunk));
|
|
162
|
+
const runSandbox = options.runSandbox ??
|
|
163
|
+
((request) => runInSandbox(request));
|
|
164
|
+
const result = await runSandbox({ taskRef: parsed.taskRef, branch, command, runId });
|
|
77
165
|
if (result.stdout) {
|
|
78
|
-
|
|
166
|
+
writeStdout(result.stdout);
|
|
79
167
|
}
|
|
80
168
|
if (result.stderr) {
|
|
81
|
-
|
|
169
|
+
writeStderr(result.stderr);
|
|
170
|
+
}
|
|
171
|
+
if ((result.exitCode ?? (result.signal ? 1 : 0)) === 0 && identity && result.run) {
|
|
172
|
+
writeRunRecord({ identity, run: result.run, branch, command });
|
|
82
173
|
}
|
|
83
174
|
return result.exitCode ?? (result.signal ? 1 : 0);
|
|
84
175
|
}
|
|
@@ -9,6 +9,19 @@ async function spawnCapture(file, args) {
|
|
|
9
9
|
const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
10
10
|
let stdout = '';
|
|
11
11
|
let stderr = '';
|
|
12
|
+
let settled = false;
|
|
13
|
+
const rejectOnce = (error) => {
|
|
14
|
+
if (settled)
|
|
15
|
+
return;
|
|
16
|
+
settled = true;
|
|
17
|
+
reject(error);
|
|
18
|
+
};
|
|
19
|
+
const resolveOnce = (result) => {
|
|
20
|
+
if (settled)
|
|
21
|
+
return;
|
|
22
|
+
settled = true;
|
|
23
|
+
resolve(result);
|
|
24
|
+
};
|
|
12
25
|
child.stdout.setEncoding('utf8');
|
|
13
26
|
child.stderr.setEncoding('utf8');
|
|
14
27
|
child.stdout.on('data', (chunk) => {
|
|
@@ -17,10 +30,93 @@ async function spawnCapture(file, args) {
|
|
|
17
30
|
child.stderr.on('data', (chunk) => {
|
|
18
31
|
stderr += chunk;
|
|
19
32
|
});
|
|
20
|
-
child.on('error',
|
|
21
|
-
child.on('close', (exitCode, signal) =>
|
|
33
|
+
child.on('error', rejectOnce);
|
|
34
|
+
child.on('close', (exitCode, signal) => {
|
|
35
|
+
resolveOnce({ exitCode, signal, stdout, stderr });
|
|
36
|
+
});
|
|
22
37
|
});
|
|
23
38
|
}
|
|
39
|
+
function shellQuote(value) {
|
|
40
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
41
|
+
}
|
|
42
|
+
export function createRunId(date = new Date()) {
|
|
43
|
+
return `run-${date.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[-:]/g, '').replace('T', '-').replace('Z', '')}-${process.pid}`;
|
|
44
|
+
}
|
|
45
|
+
function buildRunScript(params) {
|
|
46
|
+
const command = params.command.map(shellQuote).join(' ');
|
|
47
|
+
const runDir = shellQuote(params.runDir);
|
|
48
|
+
const runId = shellQuote(params.runId);
|
|
49
|
+
return `#!/bin/sh
|
|
50
|
+
set -u
|
|
51
|
+
cd /workspace
|
|
52
|
+
date "+%Y-%m-%d %H:%M:%S%:z" > ${runDir}/started_at
|
|
53
|
+
printf '%s\\n' running > ${runDir}/status
|
|
54
|
+
${command}
|
|
55
|
+
code=$?
|
|
56
|
+
printf '%s\\n' "$code" > ${runDir}/exit_code
|
|
57
|
+
date "+%Y-%m-%d %H:%M:%S%:z" > ${runDir}/finished_at
|
|
58
|
+
if [ "$code" -eq 0 ]; then
|
|
59
|
+
printf '%s\\n' completed > ${runDir}/status
|
|
60
|
+
else
|
|
61
|
+
printf '%s\\n' failed > ${runDir}/status
|
|
62
|
+
fi
|
|
63
|
+
printf '\\n[agent-infra] run %s finished with exit code %s\\n' ${runId} "$code"
|
|
64
|
+
exec bash -l
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
function buildTmuxLauncher(params) {
|
|
68
|
+
const session = 'work';
|
|
69
|
+
const window = `ai-${params.runId.replace(/^run-/, '').slice(0, 18)}`;
|
|
70
|
+
const runRoot = '/tmp/agent-infra-runs';
|
|
71
|
+
const runDir = `${runRoot}/${params.runId}`;
|
|
72
|
+
const runScript = buildRunScript({ command: params.request.command, runDir, runId: params.runId });
|
|
73
|
+
const runScriptBase64 = Buffer.from(runScript, 'utf8').toString('base64');
|
|
74
|
+
const paneCommand = `cd /workspace && ${shellQuote(`${runDir}/run.sh`)}`;
|
|
75
|
+
return `set -eu
|
|
76
|
+
session=${shellQuote(session)}
|
|
77
|
+
window=${shellQuote(window)}
|
|
78
|
+
run_id=${shellQuote(params.runId)}
|
|
79
|
+
run_dir=${shellQuote(runDir)}
|
|
80
|
+
task_ref=${shellQuote(params.request.taskRef)}
|
|
81
|
+
branch=${shellQuote(params.request.branch)}
|
|
82
|
+
|
|
83
|
+
sandbox-dotfiles-link >/dev/null 2>&1 || true
|
|
84
|
+
mkdir -p "$run_dir"
|
|
85
|
+
printf '%s\\n' pending > "$run_dir/status"
|
|
86
|
+
printf '%s\\n' "$task_ref" > "$run_dir/task_ref"
|
|
87
|
+
printf '%s\\n' "$branch" > "$run_dir/branch"
|
|
88
|
+
printf '%s\\n' ${shellQuote(params.request.command.join(' '))} > "$run_dir/command"
|
|
89
|
+
printf '%s' ${shellQuote(runScriptBase64)} | base64 -d > "$run_dir/run.sh"
|
|
90
|
+
chmod +x "$run_dir/run.sh"
|
|
91
|
+
|
|
92
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
93
|
+
printf '%s\\n' "tmux is not installed in this sandbox" >&2
|
|
94
|
+
exit 127
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
if ! tmux has-session -t "$session" 2>/dev/null; then
|
|
98
|
+
tmux new-session -d -s "$session" -n shell
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [ -n "\${TZ:-}" ]; then
|
|
102
|
+
tmux set-environment -t "$session" TZ "$TZ" 2>/dev/null || true
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
pane=$(tmux new-window -d -P -F '#{pane_id}' -t "$session" -n "$window")
|
|
106
|
+
printf '%s\\n' "$session" > "$run_dir/session"
|
|
107
|
+
printf '%s\\n' "$window" > "$run_dir/window"
|
|
108
|
+
printf '%s\\n' "$pane" > "$run_dir/pane"
|
|
109
|
+
tmux pipe-pane -o -t "$pane" "cat > $run_dir/output.log"
|
|
110
|
+
tmux send-keys -t "$pane" ${shellQuote(paneCommand)} Enter
|
|
111
|
+
|
|
112
|
+
cat <<EOF
|
|
113
|
+
Started sandbox run $run_id in tmux session '$session', window '$window', pane '$pane'.
|
|
114
|
+
Attach with: ai sandbox enter $task_ref
|
|
115
|
+
Status file: $run_dir/status
|
|
116
|
+
Output log: $run_dir/output.log
|
|
117
|
+
EOF
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
24
120
|
export async function runInSandbox(request, options = {}) {
|
|
25
121
|
const config = options.engine ? null : loadConfig();
|
|
26
122
|
const engine = options.engine ?? detectEngine(config);
|
|
@@ -37,13 +133,26 @@ export async function runInSandbox(request, options = {}) {
|
|
|
37
133
|
if (!found.running) {
|
|
38
134
|
(options.startContainer ?? ((name) => startSandboxContainer(engine, name)))(found.name);
|
|
39
135
|
}
|
|
136
|
+
const runId = options.runId ?? createRunId();
|
|
137
|
+
const runDir = `/tmp/agent-infra-runs/${runId}`;
|
|
40
138
|
const dockerArgs = [
|
|
41
139
|
'exec',
|
|
42
140
|
...terminalEnvFlags(),
|
|
43
141
|
...hostTimezoneEnvFlags(),
|
|
44
142
|
found.name,
|
|
45
|
-
|
|
143
|
+
'bash',
|
|
144
|
+
'-lc',
|
|
145
|
+
buildTmuxLauncher({ request, runId })
|
|
46
146
|
];
|
|
47
|
-
|
|
147
|
+
const result = await (options.spawn ?? spawnCapture)('docker', dockerArgs);
|
|
148
|
+
return {
|
|
149
|
+
...result,
|
|
150
|
+
run: {
|
|
151
|
+
runId,
|
|
152
|
+
engine,
|
|
153
|
+
container: found.name,
|
|
154
|
+
runDir
|
|
155
|
+
}
|
|
156
|
+
};
|
|
48
157
|
}
|
|
49
158
|
//# sourceMappingURL=capture.js.map
|
|
@@ -678,6 +678,38 @@ function resolveHostCatalogPath(value, hostHomeDir) {
|
|
|
678
678
|
return null;
|
|
679
679
|
}
|
|
680
680
|
}
|
|
681
|
+
const CODEX_DISABLED_FEATURE_FLAGS = ['apps', 'enable_mcp_apps'];
|
|
682
|
+
function removeCodexMcpServers(sandboxParsed) {
|
|
683
|
+
if (!Object.hasOwn(sandboxParsed, 'mcp_servers')) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
delete sandboxParsed.mcp_servers;
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
function inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) {
|
|
690
|
+
if (!isJsonObjectRecord(hostParsed.features)) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
if (Object.hasOwn(sandboxParsed, 'features') && !isJsonObjectRecord(sandboxParsed.features)) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
let sandboxFeatures = sandboxParsed.features;
|
|
697
|
+
let changed = false;
|
|
698
|
+
for (const key of CODEX_DISABLED_FEATURE_FLAGS) {
|
|
699
|
+
if (hostParsed.features[key] !== false) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (!sandboxFeatures) {
|
|
703
|
+
sandboxFeatures = {};
|
|
704
|
+
sandboxParsed.features = sandboxFeatures;
|
|
705
|
+
}
|
|
706
|
+
if (sandboxFeatures[key] !== false) {
|
|
707
|
+
sandboxFeatures[key] = false;
|
|
708
|
+
changed = true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return changed;
|
|
712
|
+
}
|
|
681
713
|
export function ensureCodexModelInheritance(toolDir, hostHomeDir, containerCodexDir = '/home/devuser/.codex') {
|
|
682
714
|
if (!hostHomeDir) {
|
|
683
715
|
return;
|
|
@@ -704,12 +736,13 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir, containerCodex
|
|
|
704
736
|
return;
|
|
705
737
|
}
|
|
706
738
|
}
|
|
739
|
+
let changed = removeCodexMcpServers(sandboxParsed);
|
|
740
|
+
changed = inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) || changed;
|
|
707
741
|
const inheritSpecs = [
|
|
708
742
|
['model', 'string'],
|
|
709
743
|
['model_reasoning_effort', 'string'],
|
|
710
744
|
['model_auto_compact_token_limit', 'number']
|
|
711
745
|
];
|
|
712
|
-
let changed = false;
|
|
713
746
|
for (const [key, type] of inheritSpecs) {
|
|
714
747
|
if (Object.hasOwn(sandboxParsed, key)) {
|
|
715
748
|
continue;
|
|
@@ -1113,9 +1146,9 @@ export async function create(args) {
|
|
|
1113
1146
|
if (claudeCodeEntry) {
|
|
1114
1147
|
ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
|
|
1115
1148
|
ensureClaudeSettings(claudeCodeEntry.dir, effectiveConfig.home);
|
|
1116
|
-
// prepareClaudeCredentials wrote
|
|
1117
|
-
//
|
|
1118
|
-
// claude-code entry was removed
|
|
1149
|
+
// prepareClaudeCredentials wrote OAuth credentials or confirmed
|
|
1150
|
+
// provider/API settings are enough for Claude Code. If no usable
|
|
1151
|
+
// auth was present, the claude-code entry was removed above.
|
|
1119
1152
|
}
|
|
1120
1153
|
const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
|
|
1121
1154
|
if (codexEntry) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
|
|
3
3
|
import { detectEngine } from "../engine.js";
|
|
4
|
-
import { formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
|
|
4
|
+
import { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
|
|
5
5
|
import { runInteractiveEngine } from "../shell.js";
|
|
6
6
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
7
7
|
import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
|
|
@@ -50,7 +50,10 @@ export function runSandboxInteractive(params) {
|
|
|
50
50
|
}
|
|
51
51
|
return runBridge({ engine, dockerArgs, container, home });
|
|
52
52
|
}
|
|
53
|
-
export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY) {
|
|
53
|
+
export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY, providerAuthAvailable = false) {
|
|
54
|
+
if (providerAuthAvailable && (result.status === 'STALE_ACCESS' || result.status === 'MISSING')) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
54
57
|
if (result.status === 'STALE_ACCESS') {
|
|
55
58
|
return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
|
|
56
59
|
}
|
|
@@ -100,8 +103,9 @@ export async function enter(args) {
|
|
|
100
103
|
if (config.tools.includes('claude-code')) {
|
|
101
104
|
try {
|
|
102
105
|
// Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
|
|
106
|
+
const providerAuthAvailable = hasClaudeProviderAuth(config.home);
|
|
103
107
|
const result = reconcileClaudeCredentials(config.home);
|
|
104
|
-
const message = formatCredentialSyncStatus(result);
|
|
108
|
+
const message = formatCredentialSyncStatus(result, process.stderr.isTTY, providerAuthAvailable);
|
|
105
109
|
if (message) {
|
|
106
110
|
process.stderr.write(message);
|
|
107
111
|
}
|
|
@@ -10,7 +10,7 @@ import { parseCustomTools } from "./tools.js";
|
|
|
10
10
|
const DEFAULTS = Object.freeze({
|
|
11
11
|
engine: null,
|
|
12
12
|
runtimes: ['node22'],
|
|
13
|
-
tools: ['claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
13
|
+
tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
14
14
|
dockerfile: null,
|
|
15
15
|
vm: {
|
|
16
16
|
cpu: null,
|
|
@@ -11,6 +11,12 @@ const REDACTION_PATTERNS = [
|
|
|
11
11
|
{ pattern: /gh[psoru]_[A-Za-z0-9]{30,}/g, replacement: '[REDACTED github token]' },
|
|
12
12
|
{ pattern: /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/gi, replacement: 'Bearer [REDACTED]' }
|
|
13
13
|
];
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function hasNonEmptyString(value) {
|
|
18
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
19
|
+
}
|
|
14
20
|
function errorMessage(error) {
|
|
15
21
|
return error instanceof Error ? error.message : 'unknown error';
|
|
16
22
|
}
|
|
@@ -102,6 +108,28 @@ export function validateClaudeCredentialsEnvOverride(env = process.env) {
|
|
|
102
108
|
throw new Error('Invalid AGENT_INFRA_CLAUDE_CREDENTIALS_FILE value. Expected an absolute file path.');
|
|
103
109
|
}
|
|
104
110
|
}
|
|
111
|
+
export function hasClaudeProviderAuthInSettings(settings) {
|
|
112
|
+
if (!isRecord(settings)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const env = isRecord(settings.env) ? settings.env : {};
|
|
116
|
+
return hasNonEmptyString(env.ANTHROPIC_AUTH_TOKEN)
|
|
117
|
+
|| hasNonEmptyString(env.ANTHROPIC_API_KEY)
|
|
118
|
+
|| hasNonEmptyString(settings.apiKeyHelper);
|
|
119
|
+
}
|
|
120
|
+
export function hasClaudeProviderAuth(home, options = {}) {
|
|
121
|
+
const { readFn = (targetPath) => fs.readFileSync(targetPath, 'utf8'), existsFn = fs.existsSync } = options;
|
|
122
|
+
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
123
|
+
if (!existsFn(settingsPath)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return hasClaudeProviderAuthInSettings(JSON.parse(readFn(settingsPath)));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
105
133
|
// Reconcile treats the freshest valid endpoint as authoritative so sandbox
|
|
106
134
|
// token rotations can flow back to the host credential store.
|
|
107
135
|
function validateClaudeCredentialsBlob(raw, blob = null) {
|
|
@@ -395,11 +423,19 @@ export function formatRemaining(expiresAt) {
|
|
|
395
423
|
const minutes = totalMinutes % 60;
|
|
396
424
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
397
425
|
}
|
|
398
|
-
export function prepareClaudeCredentials(home, project, resolvedTools, extractFn = extractClaudeCredentialsBlob, writeFn = writeClaudeCredentialsFile, inspectFn = inspectClaudeKeychainStatus) {
|
|
426
|
+
export function prepareClaudeCredentials(home, project, resolvedTools, extractFn = extractClaudeCredentialsBlob, writeFn = writeClaudeCredentialsFile, inspectFn = inspectClaudeKeychainStatus, providerAuthFn = hasClaudeProviderAuth) {
|
|
399
427
|
const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
400
428
|
if (!claudeCodeEntry) {
|
|
401
429
|
return { status: 'NOT_APPLICABLE' };
|
|
402
430
|
}
|
|
431
|
+
const providerAuthAvailable = () => {
|
|
432
|
+
try {
|
|
433
|
+
return providerAuthFn(home);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
403
439
|
let blob = null;
|
|
404
440
|
const hasCustomInspectFn = inspectFn !== inspectClaudeKeychainStatus;
|
|
405
441
|
const hasCustomExtractFn = extractFn !== extractClaudeCredentialsBlob;
|
|
@@ -410,7 +446,7 @@ export function prepareClaudeCredentials(home, project, resolvedTools, extractFn
|
|
|
410
446
|
blob = inspection.blob;
|
|
411
447
|
break;
|
|
412
448
|
case 'MISSING':
|
|
413
|
-
return { status: 'SKIPPED' };
|
|
449
|
+
return providerAuthAvailable() ? { status: 'OK' } : { status: 'SKIPPED' };
|
|
414
450
|
case 'KEYCHAIN_LOCKED':
|
|
415
451
|
throw new Error([
|
|
416
452
|
'Claude Code credentials are stored in the macOS keychain, but the keychain is locked.',
|
|
@@ -418,6 +454,9 @@ export function prepareClaudeCredentials(home, project, resolvedTools, extractFn
|
|
|
418
454
|
buildLockedGuidance()
|
|
419
455
|
].join('\n'));
|
|
420
456
|
case 'STALE_ACCESS':
|
|
457
|
+
if (providerAuthAvailable()) {
|
|
458
|
+
return { status: 'OK' };
|
|
459
|
+
}
|
|
421
460
|
throw new Error([
|
|
422
461
|
'Claude Code credentials on host are invalid or expired.',
|
|
423
462
|
'',
|
|
@@ -447,7 +486,7 @@ export function prepareClaudeCredentials(home, project, resolvedTools, extractFn
|
|
|
447
486
|
else {
|
|
448
487
|
blob = extractFn(home);
|
|
449
488
|
if (!blob) {
|
|
450
|
-
return { status: 'SKIPPED' };
|
|
489
|
+
return providerAuthAvailable() ? { status: 'OK' } : { status: 'SKIPPED' };
|
|
451
490
|
}
|
|
452
491
|
}
|
|
453
492
|
writeFn(home, project, blob);
|
|
@@ -61,6 +61,19 @@ RUN cat > /usr/local/bin/cc-token-status <<'SCRIPT' && chmod +x /usr/local/bin/c
|
|
|
61
61
|
#!/bin/sh
|
|
62
62
|
set -eu
|
|
63
63
|
|
|
64
|
+
SETTINGS_FILE="/home/devuser/.claude/settings.json"
|
|
65
|
+
if [ -r "$SETTINGS_FILE" ] && jq -e '
|
|
66
|
+
def has_nonempty($v):
|
|
67
|
+
if ($v | type) == "string" then ($v | gsub("^\\s+|\\s+$"; "") | length) > 0 else false end;
|
|
68
|
+
(if type == "object" then . else {} end) as $settings
|
|
69
|
+
| (if ($settings.env | type) == "object" then $settings.env else {} end) as $env
|
|
70
|
+
| has_nonempty($env.ANTHROPIC_AUTH_TOKEN)
|
|
71
|
+
or has_nonempty($env.ANTHROPIC_API_KEY)
|
|
72
|
+
or has_nonempty($settings.apiKeyHelper)
|
|
73
|
+
' "$SETTINGS_FILE" >/dev/null 2>&1; then
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
64
77
|
CRED_FILE="/home/devuser/.claude/.credentials.json"
|
|
65
78
|
[ -r "$CRED_FILE" ] || exit 0
|
|
66
79
|
|
|
@@ -88,6 +88,15 @@ function createBuiltinTools(home, project) {
|
|
|
88
88
|
{ hostPath: hostJoin(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
|
|
89
89
|
{ hostPath: hostJoin(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
|
|
90
90
|
]
|
|
91
|
+
},
|
|
92
|
+
'agent-infra': {
|
|
93
|
+
id: 'agent-infra',
|
|
94
|
+
name: 'agent-infra CLI',
|
|
95
|
+
install: { type: 'npm', cmd: '@fitlab-ai/agent-infra@latest' },
|
|
96
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'agent-infra'),
|
|
97
|
+
containerMount: '/home/devuser/.agent-infra-cli',
|
|
98
|
+
versionCmd: 'ai version --raw',
|
|
99
|
+
setupHint: 'Provides the ai and agent-infra CLI commands inside the sandbox.'
|
|
91
100
|
}
|
|
92
101
|
};
|
|
93
102
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createFeishuTransport, normalizeMessage } from "./transport.js";
|
|
1
|
+
import { cardMessage, createFeishuTransport, normalizeMessage } from "./transport.js";
|
|
2
2
|
// Assemble the feishu adapter. The transport is injectable so unit tests can
|
|
3
3
|
// drive start/dispatch/reply/sendMessage against a fake transport without the
|
|
4
4
|
// real SDK; production uses the default SDK-backed transport.
|
|
@@ -18,7 +18,9 @@ export function createFeishuAdapter(config, transport = createFeishuTransport(co
|
|
|
18
18
|
text: normalized.text,
|
|
19
19
|
messageId: normalized.messageId,
|
|
20
20
|
raw: normalized.raw,
|
|
21
|
-
reply: (text) =>
|
|
21
|
+
reply: async (text) => {
|
|
22
|
+
await transport.send(normalized.chatId, cardMessage(text));
|
|
23
|
+
}
|
|
22
24
|
};
|
|
23
25
|
await adapterCtx.dispatch(message);
|
|
24
26
|
}
|
|
@@ -31,7 +33,7 @@ export function createFeishuAdapter(config, transport = createFeishuTransport(co
|
|
|
31
33
|
await transport.stop();
|
|
32
34
|
},
|
|
33
35
|
async sendMessage(target, text) {
|
|
34
|
-
await transport.send(target.chatId, text);
|
|
36
|
+
await transport.send(target.chatId, cardMessage(text));
|
|
35
37
|
}
|
|
36
38
|
};
|
|
37
39
|
}
|