@fitlab-ai/agent-infra 0.6.4 → 0.7.0
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 +63 -27
- package/README.zh-CN.md +61 -25
- package/bin/cli.ts +18 -6
- package/dist/bin/cli.js +20 -6
- package/dist/lib/cp.js +127 -0
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/init.js +3 -0
- package/dist/lib/sandbox/clipboard/bridge.js +23 -4
- package/dist/lib/sandbox/clipboard/index.js +12 -3
- package/dist/lib/sandbox/commands/create.js +11 -2
- package/dist/lib/sandbox/commands/enter.js +29 -6
- package/dist/lib/sandbox/commands/list-running.js +108 -0
- package/dist/lib/sandbox/commands/ls.js +24 -45
- package/dist/lib/sandbox/commands/rebuild.js +15 -7
- package/dist/lib/sandbox/config.js +3 -0
- package/dist/lib/sandbox/index.js +6 -4
- package/dist/lib/sandbox/readme-scaffold.js +148 -0
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
- package/dist/lib/sandbox/runtimes/base.dockerfile +3 -3
- package/dist/lib/sandbox/tools.js +213 -8
- package/dist/lib/update.js +12 -1
- package/lib/cp.ts +177 -0
- package/lib/defaults.json +1 -0
- package/lib/init.ts +10 -0
- package/lib/sandbox/clipboard/bridge.ts +23 -4
- package/lib/sandbox/clipboard/index.ts +12 -3
- package/lib/sandbox/commands/create.ts +18 -2
- package/lib/sandbox/commands/enter.ts +48 -6
- package/lib/sandbox/commands/list-running.ts +135 -0
- package/lib/sandbox/commands/ls.ts +28 -49
- package/lib/sandbox/commands/rebuild.ts +24 -7
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/index.ts +6 -4
- package/lib/sandbox/readme-scaffold.ts +177 -0
- package/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
- package/lib/sandbox/runtimes/base.dockerfile +3 -3
- package/lib/sandbox/tools.ts +248 -9
- package/lib/update.ts +15 -1
- package/package.json +1 -1
- package/templates/.agents/QUICKSTART.en.md +1 -1
- package/templates/.agents/QUICKSTART.zh-CN.md +1 -1
- package/templates/.agents/README.en.md +79 -2
- package/templates/.agents/README.zh-CN.md +79 -2
- package/templates/.agents/rules/create-issue.en.md +1 -1
- 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/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.github.en.md +6 -5
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +6 -5
- package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +57 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +57 -0
- package/templates/.agents/rules/pr-sync.github.en.md +4 -5
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +4 -5
- package/templates/.agents/rules/task-management.en.md +9 -6
- package/templates/.agents/rules/task-management.zh-CN.md +9 -6
- package/templates/.agents/rules/testing-discipline.en.md +2 -2
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
- package/templates/.agents/scripts/validate-artifact.js +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +16 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +16 -4
- package/templates/.agents/skills/check-task/SKILL.en.md +43 -32
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +42 -31
- package/templates/.agents/skills/code-task/SKILL.en.md +117 -0
- package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +51 -24
- package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
- package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
- package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
- package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
- package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
- package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
- package/templates/.agents/skills/commit/SKILL.en.md +2 -2
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +17 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.en.md +3 -3
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/restore-task/SKILL.en.md +4 -3
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +76 -0
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +102 -0
- package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
- package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +11 -9
- package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +15 -9
- package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
- package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
- package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
- package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
- package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
- package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +76 -0
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +102 -0
- package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
- package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
- package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
- package/templates/.agents/skills/test/SKILL.en.md +2 -2
- package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -0
- package/templates/.agents/templates/task.en.md +3 -3
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
- package/templates/.agents/workflows/feature-development.en.yaml +115 -70
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
- package/templates/.agents/workflows/refactoring.en.yaml +123 -78
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
- package/templates/.claude/commands/code-task.en.md +8 -0
- package/templates/.claude/commands/code-task.zh-CN.md +8 -0
- package/templates/.claude/commands/review-analysis.en.md +8 -0
- package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
- package/templates/.claude/commands/review-code.en.md +8 -0
- package/templates/.claude/commands/review-code.zh-CN.md +8 -0
- package/templates/.claude/commands/review-plan.en.md +8 -0
- package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
- package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
- package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
- package/templates/.opencode/commands/code-task.en.md +11 -0
- package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-analysis.en.md +11 -0
- package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-code.en.md +11 -0
- package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-plan.en.md +11 -0
- package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
- package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
- package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
- package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
- package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
- package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
- package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
- package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
- package/templates/.claude/commands/implement-task.en.md +0 -8
- package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
- package/templates/.claude/commands/refine-task.en.md +0 -8
- package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
- package/templates/.claude/commands/review-task.en.md +0 -8
- package/templates/.claude/commands/review-task.zh-CN.md +0 -8
- package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
- package/templates/.opencode/commands/implement-task.en.md +0 -11
- package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
- package/templates/.opencode/commands/refine-task.en.md +0 -11
- package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
- package/templates/.opencode/commands/review-task.en.md +0 -11
- package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
- /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
package/lib/sandbox/tools.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { safeNameCandidates, sanitizeBranchName } from './constants.ts';
|
|
2
2
|
import { hostJoin } from './engines/wsl2-paths.ts';
|
|
3
3
|
|
|
4
|
+
export type SandboxToolInstall =
|
|
5
|
+
| { type: 'npm'; cmd: string }
|
|
6
|
+
| { type: 'shell'; cmd: string };
|
|
7
|
+
|
|
4
8
|
export type SandboxTool = {
|
|
5
9
|
id: string;
|
|
6
10
|
name: string;
|
|
7
|
-
|
|
11
|
+
install: SandboxToolInstall;
|
|
8
12
|
sandboxBase: string;
|
|
9
13
|
containerMount: string;
|
|
10
14
|
versionCmd: string;
|
|
@@ -21,14 +25,17 @@ type ToolsConfig = {
|
|
|
21
25
|
home: string;
|
|
22
26
|
project: string;
|
|
23
27
|
tools: string[];
|
|
28
|
+
customTools?: SandboxTool[];
|
|
24
29
|
};
|
|
25
30
|
|
|
31
|
+
const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
32
|
+
|
|
26
33
|
function createBuiltinTools(home: string, project: string): Record<string, SandboxTool> {
|
|
27
34
|
return {
|
|
28
35
|
'claude-code': {
|
|
29
36
|
id: 'claude-code',
|
|
30
37
|
name: 'Claude Code',
|
|
31
|
-
|
|
38
|
+
install: { type: 'npm', cmd: '@anthropic-ai/claude-code@stable' },
|
|
32
39
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
|
|
33
40
|
containerMount: '/home/devuser/.claude',
|
|
34
41
|
versionCmd: 'claude --version',
|
|
@@ -58,7 +65,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
58
65
|
codex: {
|
|
59
66
|
id: 'codex',
|
|
60
67
|
name: 'Codex',
|
|
61
|
-
|
|
68
|
+
install: { type: 'npm', cmd: '@openai/codex' },
|
|
62
69
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
|
|
63
70
|
containerMount: '/home/devuser/.codex',
|
|
64
71
|
versionCmd: 'codex --version',
|
|
@@ -73,7 +80,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
73
80
|
opencode: {
|
|
74
81
|
id: 'opencode',
|
|
75
82
|
name: 'OpenCode',
|
|
76
|
-
|
|
83
|
+
install: { type: 'npm', cmd: 'opencode-ai' },
|
|
77
84
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
|
|
78
85
|
containerMount: '/home/devuser/.local/share/opencode',
|
|
79
86
|
versionCmd: 'opencode version',
|
|
@@ -92,7 +99,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
92
99
|
'gemini-cli': {
|
|
93
100
|
id: 'gemini-cli',
|
|
94
101
|
name: 'Gemini CLI',
|
|
95
|
-
|
|
102
|
+
install: { type: 'npm', cmd: '@google/gemini-cli' },
|
|
96
103
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
|
|
97
104
|
containerMount: '/home/devuser/.gemini',
|
|
98
105
|
versionCmd: 'gemini --version',
|
|
@@ -108,16 +115,224 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
108
115
|
};
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
export function builtinToolIds(): string[] {
|
|
119
|
+
return Object.keys(createBuiltinTools('', ''));
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
function validateTool(tool: SandboxTool): void {
|
|
112
|
-
if (!tool.
|
|
113
|
-
throw new Error(`Invalid sandbox tool
|
|
123
|
+
if (!tool.id || !TOOL_ID_PATTERN.test(tool.id)) {
|
|
124
|
+
throw new Error(`Invalid sandbox tool id: ${String(tool.id)}`);
|
|
125
|
+
}
|
|
126
|
+
if (!tool.install || (tool.install.type !== 'npm' && tool.install.type !== 'shell')) {
|
|
127
|
+
throw new Error(`Sandbox tool ${tool.id} has invalid install.type`);
|
|
128
|
+
}
|
|
129
|
+
if (!tool.install.cmd) {
|
|
130
|
+
throw new Error(`Sandbox tool ${tool.id} has empty install.cmd`);
|
|
131
|
+
}
|
|
132
|
+
if (!tool.containerMount || !tool.containerMount.startsWith('/')) {
|
|
133
|
+
throw new Error(`Sandbox tool ${tool.id} containerMount must be an absolute path`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
138
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function asString(value: unknown, field: string, context: string): string {
|
|
142
|
+
if (typeof value !== 'string') {
|
|
143
|
+
throw new Error(`${context}: field "${field}" must be a string`);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function asOptionalNonEmptyString(value: unknown, field: string, context: string): string | undefined {
|
|
149
|
+
if (value === undefined) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
if (typeof value !== 'string') {
|
|
153
|
+
throw new Error(`${context}: field "${field}" must be a string when provided`);
|
|
154
|
+
}
|
|
155
|
+
if (value.length === 0) {
|
|
156
|
+
throw new Error(`${context}: field "${field}" must be non-empty when provided`);
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function asStringRecord(value: unknown, field: string, context: string): Record<string, string> | undefined {
|
|
162
|
+
if (value === undefined) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (!isPlainObject(value)) {
|
|
166
|
+
throw new Error(`${context}: field "${field}" must be an object when provided`);
|
|
167
|
+
}
|
|
168
|
+
const out: Record<string, string> = {};
|
|
169
|
+
for (const [key, val] of Object.entries(value)) {
|
|
170
|
+
if (typeof val !== 'string') {
|
|
171
|
+
throw new Error(`${context}: field "${field}.${key}" must be a string`);
|
|
172
|
+
}
|
|
173
|
+
out[key] = val;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function asStringArray(value: unknown, field: string, context: string): string[] | undefined {
|
|
179
|
+
if (value === undefined) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
if (!Array.isArray(value)) {
|
|
183
|
+
throw new Error(`${context}: field "${field}" must be an array when provided`);
|
|
184
|
+
}
|
|
185
|
+
return value.map((item, index) => {
|
|
186
|
+
if (typeof item !== 'string') {
|
|
187
|
+
throw new Error(`${context}: field "${field}[${index}]" must be a string`);
|
|
188
|
+
}
|
|
189
|
+
return item;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseInstall(value: unknown, context: string): SandboxToolInstall {
|
|
194
|
+
if (!isPlainObject(value)) {
|
|
195
|
+
throw new Error(`${context}: field "install" must be an object`);
|
|
196
|
+
}
|
|
197
|
+
const type = value.type;
|
|
198
|
+
if (type !== 'npm' && type !== 'shell') {
|
|
199
|
+
throw new Error(`${context}: field "install.type" must be "npm" or "shell"`);
|
|
200
|
+
}
|
|
201
|
+
const cmd = asString(value.cmd, 'install.cmd', context);
|
|
202
|
+
if (!cmd) {
|
|
203
|
+
throw new Error(`${context}: field "install.cmd" must be non-empty`);
|
|
204
|
+
}
|
|
205
|
+
return { type, cmd };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseHostPreSeedFiles(value: unknown, context: string): SandboxTool['hostPreSeedFiles'] {
|
|
209
|
+
if (value === undefined) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (!Array.isArray(value)) {
|
|
213
|
+
throw new Error(`${context}: field "hostPreSeedFiles" must be an array when provided`);
|
|
214
|
+
}
|
|
215
|
+
return value.map((item, index) => {
|
|
216
|
+
if (!isPlainObject(item)) {
|
|
217
|
+
throw new Error(`${context}: field "hostPreSeedFiles[${index}]" must be an object`);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
hostPath: asString(item.hostPath, `hostPreSeedFiles[${index}].hostPath`, context),
|
|
221
|
+
sandboxName: asString(item.sandboxName, `hostPreSeedFiles[${index}].sandboxName`, context)
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseHostPreSeedDirs(value: unknown, context: string): SandboxTool['hostPreSeedDirs'] {
|
|
227
|
+
if (value === undefined) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
if (!Array.isArray(value)) {
|
|
231
|
+
throw new Error(`${context}: field "hostPreSeedDirs" must be an array when provided`);
|
|
114
232
|
}
|
|
233
|
+
return value.map((item, index) => {
|
|
234
|
+
if (!isPlainObject(item)) {
|
|
235
|
+
throw new Error(`${context}: field "hostPreSeedDirs[${index}]" must be an object`);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
hostDir: asString(item.hostDir, `hostPreSeedDirs[${index}].hostDir`, context),
|
|
239
|
+
sandboxSubdir: asString(item.sandboxSubdir, `hostPreSeedDirs[${index}].sandboxSubdir`, context)
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseHostLiveMounts(value: unknown, context: string): SandboxTool['hostLiveMounts'] {
|
|
245
|
+
if (value === undefined) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
if (!Array.isArray(value)) {
|
|
249
|
+
throw new Error(`${context}: field "hostLiveMounts" must be an array when provided`);
|
|
250
|
+
}
|
|
251
|
+
return value.map((item, index) => {
|
|
252
|
+
if (!isPlainObject(item)) {
|
|
253
|
+
throw new Error(`${context}: field "hostLiveMounts[${index}]" must be an object`);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
hostPath: asString(item.hostPath, `hostLiveMounts[${index}].hostPath`, context),
|
|
257
|
+
containerSubpath: asString(item.containerSubpath, `hostLiveMounts[${index}].containerSubpath`, context)
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function parseCustomTool(
|
|
263
|
+
entry: unknown,
|
|
264
|
+
index: number,
|
|
265
|
+
options: { home: string }
|
|
266
|
+
): SandboxTool {
|
|
267
|
+
const context = `customTools[${index}]`;
|
|
268
|
+
if (!isPlainObject(entry)) {
|
|
269
|
+
throw new Error(`${context} must be an object`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const id = asString(entry.id, 'id', context);
|
|
273
|
+
if (!TOOL_ID_PATTERN.test(id)) {
|
|
274
|
+
throw new Error(`${context}: field "id" must match ${TOOL_ID_PATTERN.source}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const containerMount = asOptionalNonEmptyString(entry.containerMount, 'containerMount', context)
|
|
278
|
+
?? `/home/devuser/.${id}`;
|
|
279
|
+
if (!containerMount.startsWith('/')) {
|
|
280
|
+
throw new Error(`${context}: field "containerMount" must be an absolute path`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const tool: SandboxTool = {
|
|
284
|
+
id,
|
|
285
|
+
name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
|
|
286
|
+
install: parseInstall(entry.install, context),
|
|
287
|
+
sandboxBase: hostJoin(options.home, '.agent-infra', 'sandboxes', id),
|
|
288
|
+
containerMount,
|
|
289
|
+
versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
|
|
290
|
+
setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
|
|
291
|
+
?? `Run \`${id}\` inside the container to set up.`,
|
|
292
|
+
envVars: asStringRecord(entry.envVars, 'envVars', context),
|
|
293
|
+
hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
|
|
294
|
+
hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
|
|
295
|
+
pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
|
|
296
|
+
hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
|
|
297
|
+
postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
validateTool(tool);
|
|
301
|
+
return tool;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function parseCustomTools(value: unknown, options: { home: string }): SandboxTool[] {
|
|
305
|
+
if (value === undefined || value === null) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
if (!Array.isArray(value)) {
|
|
309
|
+
throw new Error('sandbox: "customTools" must be an array');
|
|
310
|
+
}
|
|
311
|
+
return value.map((entry, index) => parseCustomTool(entry, index, options));
|
|
115
312
|
}
|
|
116
313
|
|
|
117
314
|
export function resolveTools(config: ToolsConfig): SandboxTool[] {
|
|
118
315
|
const builtins = createBuiltinTools(config.home, config.project);
|
|
316
|
+
const customs = config.customTools ?? [];
|
|
317
|
+
|
|
318
|
+
const seen = new Set<string>();
|
|
319
|
+
for (const tool of customs) {
|
|
320
|
+
if (builtins[tool.id]) {
|
|
321
|
+
throw new Error(`Custom sandbox tool id "${tool.id}" collides with a built-in tool`);
|
|
322
|
+
}
|
|
323
|
+
if (seen.has(tool.id)) {
|
|
324
|
+
throw new Error(`Duplicate sandbox tool id "${tool.id}" in customTools`);
|
|
325
|
+
}
|
|
326
|
+
seen.add(tool.id);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const merged: Record<string, SandboxTool> = { ...builtins };
|
|
330
|
+
for (const tool of customs) {
|
|
331
|
+
merged[tool.id] = tool;
|
|
332
|
+
}
|
|
333
|
+
|
|
119
334
|
return config.tools.map((id) => {
|
|
120
|
-
const tool =
|
|
335
|
+
const tool = merged[id];
|
|
121
336
|
if (!tool) {
|
|
122
337
|
throw new Error(`Unknown sandbox tool: ${id}`);
|
|
123
338
|
}
|
|
@@ -139,5 +354,29 @@ export function toolProjectDirCandidates(tool: SandboxTool, project: string): st
|
|
|
139
354
|
}
|
|
140
355
|
|
|
141
356
|
export function toolNpmPackagesArg(tools: SandboxTool[]): string {
|
|
142
|
-
return tools
|
|
357
|
+
return tools
|
|
358
|
+
.filter((tool) => tool.install.type === 'npm')
|
|
359
|
+
.map((tool) => tool.install.cmd)
|
|
360
|
+
.join(' ');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function toolShellInstallScript(tools: SandboxTool[]): string {
|
|
364
|
+
const blocks = tools
|
|
365
|
+
.filter((tool) => tool.install.type === 'shell')
|
|
366
|
+
.map((tool) => `# install: ${tool.id}\n${tool.install.cmd}`);
|
|
367
|
+
|
|
368
|
+
if (blocks.length === 0) {
|
|
369
|
+
return '';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return ['#!/bin/bash', 'set -e', '', ...blocks, ''].join('\n');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function toolShellInstallScriptBase64(tools: SandboxTool[]): string {
|
|
376
|
+
const script = toolShellInstallScript(tools);
|
|
377
|
+
return script ? Buffer.from(script, 'utf8').toString('base64') : '';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function imageSignatureFields(tools: SandboxTool[]): Array<{ id: string; install: SandboxToolInstall }> {
|
|
381
|
+
return tools.map((tool) => ({ id: tool.id, install: tool.install }));
|
|
143
382
|
}
|
package/lib/update.ts
CHANGED
|
@@ -15,6 +15,7 @@ type UpdateConfig = {
|
|
|
15
15
|
org: string;
|
|
16
16
|
language: string;
|
|
17
17
|
platform?: { type?: string };
|
|
18
|
+
requiresPullRequest?: boolean;
|
|
18
19
|
sandbox?: Record<string, unknown>;
|
|
19
20
|
labels?: Record<string, unknown>;
|
|
20
21
|
files?: Partial<FileRegistry>;
|
|
@@ -22,6 +23,7 @@ type UpdateConfig = {
|
|
|
22
23
|
|
|
23
24
|
type Defaults = {
|
|
24
25
|
platform: { type: string };
|
|
26
|
+
requiresPullRequest: boolean;
|
|
25
27
|
sandbox: Record<string, unknown>;
|
|
26
28
|
labels: Record<string, unknown>;
|
|
27
29
|
files: FileRegistry;
|
|
@@ -178,6 +180,7 @@ async function cmdUpdate(): Promise<void> {
|
|
|
178
180
|
const platformAdded = !config.platform;
|
|
179
181
|
const sandboxAdded = !config.sandbox;
|
|
180
182
|
const labelsAdded = !config.labels;
|
|
183
|
+
const requiresPullRequestAdded = config.requiresPullRequest === undefined;
|
|
181
184
|
let configChanged = changed;
|
|
182
185
|
|
|
183
186
|
if (platformAdded) {
|
|
@@ -195,6 +198,11 @@ async function cmdUpdate(): Promise<void> {
|
|
|
195
198
|
configChanged = true;
|
|
196
199
|
}
|
|
197
200
|
|
|
201
|
+
if (requiresPullRequestAdded) {
|
|
202
|
+
config.requiresPullRequest = defaults.requiresPullRequest;
|
|
203
|
+
configChanged = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
198
206
|
if (configChanged) {
|
|
199
207
|
console.log('');
|
|
200
208
|
if (hasNewEntries) {
|
|
@@ -205,7 +213,7 @@ async function cmdUpdate(): Promise<void> {
|
|
|
205
213
|
for (const entry of added.merged) {
|
|
206
214
|
ok(` merged: ${entry}`);
|
|
207
215
|
}
|
|
208
|
-
} else if (platformAdded || sandboxAdded || labelsAdded) {
|
|
216
|
+
} else if (platformAdded || sandboxAdded || labelsAdded || requiresPullRequestAdded) {
|
|
209
217
|
if (platformAdded) {
|
|
210
218
|
info(`Default platform config added to ${CONFIG_PATH}.`);
|
|
211
219
|
}
|
|
@@ -215,6 +223,9 @@ async function cmdUpdate(): Promise<void> {
|
|
|
215
223
|
if (labelsAdded) {
|
|
216
224
|
info(`Default labels.in config added to ${CONFIG_PATH}.`);
|
|
217
225
|
}
|
|
226
|
+
if (requiresPullRequestAdded) {
|
|
227
|
+
info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
|
|
228
|
+
}
|
|
218
229
|
} else {
|
|
219
230
|
info(`File registry changed in ${CONFIG_PATH}.`);
|
|
220
231
|
}
|
|
@@ -227,6 +238,9 @@ async function cmdUpdate(): Promise<void> {
|
|
|
227
238
|
if (hasNewEntries && platformAdded) {
|
|
228
239
|
info(`Default platform config added to ${CONFIG_PATH}.`);
|
|
229
240
|
}
|
|
241
|
+
if (hasNewEntries && requiresPullRequestAdded) {
|
|
242
|
+
info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
|
|
243
|
+
}
|
|
230
244
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
231
245
|
ok(`Updated ${CONFIG_PATH}`);
|
|
232
246
|
}
|
package/package.json
CHANGED
|
@@ -164,7 +164,7 @@ The receiving AI should read this document first to get up to speed.
|
|
|
164
164
|
|
|
165
165
|
### 1. One AI Per Phase
|
|
166
166
|
|
|
167
|
-
Don't have multiple AIs working on the same files simultaneously. Follow the sequential workflow:
|
|
167
|
+
Don't have multiple AIs working on the same files simultaneously. Follow the sequential workflow: analysis → analysis-review → design → design-review → code → code-review → commit (when any review finds issues, re-run the matching upstream stage).
|
|
168
168
|
|
|
169
169
|
### 2. Always Create Handoff Documents
|
|
170
170
|
|
|
@@ -164,7 +164,7 @@ cp .agents/templates/handoff.md .agents/workspace/active/handoff-task-001-phase2
|
|
|
164
164
|
|
|
165
165
|
### 1. 每个阶段一个 AI
|
|
166
166
|
|
|
167
|
-
不要让多个 AI
|
|
167
|
+
不要让多个 AI 同时处理相同的文件。遵循顺序工作流:分析 → 分析审查 → 设计 → 设计审查 → 编码 → 代码审查 → 提交(任一审查发现问题时,回到同名上游阶段重跑)。
|
|
168
168
|
|
|
169
169
|
### 2. 始终创建交接文档
|
|
170
170
|
|
|
@@ -224,8 +224,8 @@ Supported `invoke` placeholders:
|
|
|
224
224
|
|
|
225
225
|
| Placeholder | Replaced with | Example |
|
|
226
226
|
|-------------|---------------|---------|
|
|
227
|
-
| `${skillName}` | The skill command name, such as `review-
|
|
228
|
-
| `${projectName}` | The `.airc.json` `project` value. Use this for namespaced commands. | `/${projectName}:${skillName}` -> `/agent-infra:review-
|
|
227
|
+
| `${skillName}` | The skill command name, such as `review-code` or `commit`. | `<your-cli> ${skillName}` -> `<your-cli> review-code` |
|
|
228
|
+
| `${projectName}` | The `.airc.json` `project` value. Use this for namespaced commands. | `/${projectName}:${skillName}` -> `/agent-infra:review-code` |
|
|
229
229
|
|
|
230
230
|
Non-namespaced custom TUI:
|
|
231
231
|
|
|
@@ -258,6 +258,83 @@ Namespaced custom TUI:
|
|
|
258
258
|
|
|
259
259
|
`customTUIs` should contain one entry per custom TUI. To let `update-agent-infra` generate command files for custom skills, keep at least one existing command file in `dir` that references a built-in skill path such as `.agents/skills/analyze-task/SKILL.md`; agent-infra uses that file as the format reference.
|
|
260
260
|
|
|
261
|
+
## Sandbox Custom Tools
|
|
262
|
+
|
|
263
|
+
`customTUIs` (above) generates slash-command files but does not change the sandbox image. To install a non-npm TUI (pip / cargo / curl-based / pre-built binary) into the sandbox image and live-mount its credentials, declare it under `sandbox.customTools` in `.agents/.airc.json`. Built-in tools (`claude-code`, `codex`, `opencode`, `gemini-cli`) keep working unchanged.
|
|
264
|
+
|
|
265
|
+
### Required fields
|
|
266
|
+
|
|
267
|
+
| Field | Meaning |
|
|
268
|
+
|-------|---------|
|
|
269
|
+
| `id` | Lowercase id matching `^[a-z0-9][a-z0-9-]*$`. Referenced from `sandbox.tools`. Must not collide with a built-in id. |
|
|
270
|
+
| `install` | Install descriptor. `{ "type": "npm", "cmd": "<npm package spec>" }` runs `npm install -g <cmd>`. `{ "type": "shell", "cmd": "<shell>" }` runs the shell command(s) as `devuser` during image build. `cmd` must be non-empty. |
|
|
271
|
+
|
|
272
|
+
Minimal entry — the contract for getting a tool into the image is just these two fields:
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"sandbox": {
|
|
277
|
+
"tools": ["my-shell-tool"],
|
|
278
|
+
"customTools": [
|
|
279
|
+
{
|
|
280
|
+
"id": "my-shell-tool",
|
|
281
|
+
"install": { "type": "shell", "cmd": "curl -fsSL https://example.com/install.sh | bash" }
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Optional integration fields
|
|
289
|
+
|
|
290
|
+
Add only the fields your tool actually needs. Omit them and the loader fills sensible defaults; provide them and the loader uses your value. Provide an explicit empty string and the loader rejects it (preventing silent install-verification bypass).
|
|
291
|
+
|
|
292
|
+
| Field | Default when omitted | When to provide |
|
|
293
|
+
|-------|---------------------|-----------------|
|
|
294
|
+
| `name` | `id` | A friendlier display name in sandbox reports / hints. |
|
|
295
|
+
| `containerMount` | `/home/devuser/.<id>` | Your tool stores its config / state somewhere other than `~/.<id>`. Must be an absolute path. |
|
|
296
|
+
| `versionCmd` | `which <id>` | The installed binary name differs from `id` (e.g. id `anthropic-claude`, binary `claude`); set `"claude --version"` so sandbox-create can verify the install. |
|
|
297
|
+
| `setupHint` | `Run \`<id>\` inside the container to set up.` | The setup story is non-obvious and worth a one-liner. |
|
|
298
|
+
| `envVars` | (none) | Your tool reads config from a path the env points to (e.g. `XDG_CONFIG_HOME`-style or a custom `*_CONFIG` env). Shape: `Record<string, string>`. |
|
|
299
|
+
| `hostPreSeedFiles` / `hostPreSeedDirs` | (none) | Seed the tool's sandbox dir from host files / directories on first launch. |
|
|
300
|
+
| `pathRewriteFiles` | (none) | Seeded files contain absolute host paths that need rewriting to container paths. |
|
|
301
|
+
| `hostLiveMounts` | (none) | Share host credentials live (e.g. OAuth tokens) with the container. Read-write. |
|
|
302
|
+
| `postSetupCmds` | (none) | Run commands inside the container after first setup (e.g. symlinks). |
|
|
303
|
+
|
|
304
|
+
> **`sandboxBase` is not user-configurable.** The loader always assigns `~/.agent-infra/sandboxes/<id>` so `ai sandbox rm` / `prune` can find tool state. Any `sandboxBase` value in `customTools` entries is silently ignored.
|
|
305
|
+
|
|
306
|
+
Real-world example — `anthropic-claude` as a user-defined id with binary name `claude` and host credential live-mount:
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"sandbox": {
|
|
311
|
+
"tools": ["claude-code", "anthropic-claude"],
|
|
312
|
+
"customTools": [
|
|
313
|
+
{
|
|
314
|
+
"id": "anthropic-claude",
|
|
315
|
+
"install": { "type": "npm", "cmd": "@anthropic-ai/claude-code@stable" },
|
|
316
|
+
"versionCmd": "claude --version",
|
|
317
|
+
"hostLiveMounts": [
|
|
318
|
+
{ "hostPath": "~/.claude/.credentials.json", "containerSubpath": ".credentials.json" }
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Trust boundary and execution context
|
|
327
|
+
|
|
328
|
+
- `install.cmd` runs as user `devuser` (non-root) during `docker build`. It can write to the container's filesystem but cannot escape to the host. The trust model is the same as for the `sandbox.dockerfile` escape hatch — you own the `.airc.json` in your repo, so you own what runs at build time.
|
|
329
|
+
- Because the build runs as `devuser`, shell installs cannot `sudo` / `apt-get`. Available options for non-npm distributions:
|
|
330
|
+
- User-scope installers landing in `~/.local/bin`, `~/.cargo/bin`, `~/.npm-global/bin` (e.g. `pipx`, `cargo install`, `curl … | bash` with `INSTALL_DIR=$HOME/.local/bin`).
|
|
331
|
+
- When you genuinely need root or system packages, fall back to the existing `sandbox.dockerfile` field and own the full Dockerfile.
|
|
332
|
+
- Changing `install.cmd` (or any field that participates in the image signature) triggers exactly one image rebuild on the next `ai sandbox` invocation.
|
|
333
|
+
|
|
334
|
+
### Interaction with `sandbox.dockerfile`
|
|
335
|
+
|
|
336
|
+
When you set `sandbox.dockerfile` to point at your own Dockerfile, agent-infra still passes both `AI_TOOL_PACKAGES` (space-separated npm package specs) and `AI_TOOLS_SHELL_INSTALL_B64` (base64-encoded shell install script) as `--build-arg`. Your custom Dockerfile decides whether to consume them; if it does not declare the matching `ARG`, the shell installs for `customTools` are silently skipped — taking over the Dockerfile means taking over the install path.
|
|
337
|
+
|
|
261
338
|
## Skill Authoring Conventions
|
|
262
339
|
|
|
263
340
|
When writing or updating `.agents/skills/*/SKILL.md` files and their templates, keep step numbering consistent:
|
|
@@ -224,8 +224,8 @@ args: "<task-id>" # 可选
|
|
|
224
224
|
|
|
225
225
|
| 占位符 | 替换为 | 示例 |
|
|
226
226
|
|--------|--------|------|
|
|
227
|
-
| `${skillName}` | skill 命令名,例如 `review-
|
|
228
|
-
| `${projectName}` | `.airc.json` 中的 `project` 值,适用于带命名空间的命令。 | `/${projectName}:${skillName}` -> `/agent-infra:review-
|
|
227
|
+
| `${skillName}` | skill 命令名,例如 `review-code` 或 `commit`。 | `<your-cli> ${skillName}` -> `<your-cli> review-code` |
|
|
228
|
+
| `${projectName}` | `.airc.json` 中的 `project` 值,适用于带命名空间的命令。 | `/${projectName}:${skillName}` -> `/agent-infra:review-code` |
|
|
229
229
|
|
|
230
230
|
不带命名空间的自定义 TUI:
|
|
231
231
|
|
|
@@ -258,6 +258,83 @@ args: "<task-id>" # 可选
|
|
|
258
258
|
|
|
259
259
|
`customTUIs` 每个条目对应一个自定义 TUI。若希望 `update-agent-infra` 为自定义 skill 生成命令文件,请在 `dir` 中保留至少一个引用内置 skill 路径的既有命令文件,例如 `.agents/skills/analyze-task/SKILL.md`;agent-infra 会以该文件作为格式参考。
|
|
260
260
|
|
|
261
|
+
## 沙箱自定义工具(Sandbox Custom Tools)
|
|
262
|
+
|
|
263
|
+
上文 `customTUIs` 只负责生成 slash-command 文件,**不影响沙箱镜像**。如果要把一个非 npm 分发的 TUI(pip / cargo / curl 脚本 / 裸二进制)装进沙箱镜像、并 live-mount 它的凭证目录,需要在 `.agents/.airc.json` 的 `sandbox.customTools` 中声明。内建的四个工具(`claude-code` / `codex` / `opencode` / `gemini-cli`)行为保持不变。
|
|
264
|
+
|
|
265
|
+
### 必填字段
|
|
266
|
+
|
|
267
|
+
| 字段 | 含义 |
|
|
268
|
+
|------|------|
|
|
269
|
+
| `id` | 小写 id,匹配 `^[a-z0-9][a-z0-9-]*$`;由 `sandbox.tools` 引用;不可与内建 id 冲突。 |
|
|
270
|
+
| `install` | 安装描述符。`{ "type": "npm", "cmd": "<npm 包规范>" }` 执行 `npm install -g <cmd>`;`{ "type": "shell", "cmd": "<shell>" }` 在镜像构建阶段以 `devuser` 执行 shell。`cmd` 必须非空。 |
|
|
271
|
+
|
|
272
|
+
最小入口——把一个工具装进镜像所需的契约只有这两个字段:
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"sandbox": {
|
|
277
|
+
"tools": ["my-shell-tool"],
|
|
278
|
+
"customTools": [
|
|
279
|
+
{
|
|
280
|
+
"id": "my-shell-tool",
|
|
281
|
+
"install": { "type": "shell", "cmd": "curl -fsSL https://example.com/install.sh | bash" }
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 可选集成字段
|
|
289
|
+
|
|
290
|
+
只在你的工具真正需要时才加。**省略**则 loader 用合理默认值;**显式提供**则用你给的值;**显式给空串**会被拒绝(防止安装验证被绕过)。
|
|
291
|
+
|
|
292
|
+
| 字段 | 省略时的默认值 | 什么时候应该提供 |
|
|
293
|
+
|------|---------------|----------------|
|
|
294
|
+
| `name` | `id` | 想在沙箱报告 / 提示里显示更友好的名称。 |
|
|
295
|
+
| `containerMount` | `/home/devuser/.<id>` | 工具的配置 / 状态目录不在 `~/.<id>` 而在别处。必须是绝对路径。 |
|
|
296
|
+
| `versionCmd` | `which <id>` | 安装后的可执行文件名与 `id` 不同(例如 id 是 `anthropic-claude`,二进制名是 `claude`);填 `"claude --version"` 让 sandbox-create 能验证安装。 |
|
|
297
|
+
| `setupHint` | `Run \`<id>\` inside the container to set up.` | setup 流程不一目了然,值得用一行说明。 |
|
|
298
|
+
| `envVars` | (无) | 工具通过环境变量找配置(如 `XDG_CONFIG_HOME` 风格或自定义 `*_CONFIG` 变量)。形状:`Record<string, string>`。 |
|
|
299
|
+
| `hostPreSeedFiles` / `hostPreSeedDirs` | (无) | 首次启动时从宿主复制文件 / 目录到工具沙箱配置目录。 |
|
|
300
|
+
| `pathRewriteFiles` | (无) | seed 进来的文件里有宿主绝对路径,需要改写为容器路径。 |
|
|
301
|
+
| `hostLiveMounts` | (无) | 把宿主凭证(如 OAuth token)实时挂进容器,读写共享。 |
|
|
302
|
+
| `postSetupCmds` | (无) | 首次安装完成后在容器内执行命令(如建符号链接)。 |
|
|
303
|
+
|
|
304
|
+
> **`sandboxBase` 不由用户配置。** loader 永远使用 `~/.agent-infra/sandboxes/<id>`,这样 `ai sandbox rm` / `prune` 才能找到工具状态目录。`customTools` 条目里写的任何 `sandboxBase` 都会被静默忽略。
|
|
305
|
+
|
|
306
|
+
实际场景示例——`anthropic-claude` 作为用户自定义 id,二进制名是 `claude`,并把宿主凭证 live-mount 进来:
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"sandbox": {
|
|
311
|
+
"tools": ["claude-code", "anthropic-claude"],
|
|
312
|
+
"customTools": [
|
|
313
|
+
{
|
|
314
|
+
"id": "anthropic-claude",
|
|
315
|
+
"install": { "type": "npm", "cmd": "@anthropic-ai/claude-code@stable" },
|
|
316
|
+
"versionCmd": "claude --version",
|
|
317
|
+
"hostLiveMounts": [
|
|
318
|
+
{ "hostPath": "~/.claude/.credentials.json", "containerSubpath": ".credentials.json" }
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### 信任边界与执行身份
|
|
327
|
+
|
|
328
|
+
- `install.cmd` 在 `docker build` 阶段以 `devuser`(非 root)身份执行,只能写容器内文件系统,不能逃逸到宿主。信任模型与现有 `sandbox.dockerfile` 一致:你是 `.airc.json` 的作者,本次构建做什么由你负责。
|
|
329
|
+
- 因为不是 root,shell 安装无法 `sudo` / `apt-get`。非 npm 分发的几条可用路径:
|
|
330
|
+
- 用户态安装器,落到 `~/.local/bin`、`~/.cargo/bin`、`~/.npm-global/bin`(如 `pipx`、`cargo install`、`curl … | bash` 配合 `INSTALL_DIR=$HOME/.local/bin`)。
|
|
331
|
+
- 确实需要 root / 系统包时,仍走原有 `sandbox.dockerfile` 字段,接管整个 Dockerfile。
|
|
332
|
+
- 修改 `install.cmd` 或任何参与镜像签名的字段,下次 `ai sandbox` 命令会触发一次镜像重建。
|
|
333
|
+
|
|
334
|
+
### 与 `sandbox.dockerfile` 的交互
|
|
335
|
+
|
|
336
|
+
当 `sandbox.dockerfile` 指向自定义 Dockerfile 时,agent-infra 仍会把 `AI_TOOL_PACKAGES`(空格分隔的 npm 包规范)和 `AI_TOOLS_SHELL_INSTALL_B64`(base64 编码的 shell 安装脚本)作为 `--build-arg` 传入。你的自定义 Dockerfile 若未声明对应 `ARG`,shell 安装路径会被 docker build 静默忽略——这是接管 Dockerfile 后的应有代价。
|
|
337
|
+
|
|
261
338
|
## Skill 编写规范
|
|
262
339
|
|
|
263
340
|
编写或维护 `.agents/skills/*/SKILL.md` 及其模板时,步骤编号遵循以下规则:
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
This code platform does not provide an Issue creation rule.
|
|
4
4
|
|
|
5
|
-
`create-task` skips the cascade Issue creation step on this platform; the local `task.md` remains a valid artifact. If you later want to bind the task to an Issue, manually write `issue_number` into `task.md` and the subsequent skills (`commit` / `
|
|
5
|
+
`create-task` skips the cascade Issue creation step on this platform; the local `task.md` remains a valid artifact. If you later want to bind the task to an Issue, manually write `issue_number` into `task.md` and the subsequent skills (`commit` / `code-task` / `complete-task`, etc.) will pick up Issue metadata syncing through the existing cascade rules.
|
|
@@ -5,7 +5,7 @@ After `create-task` writes the local `task.md`, follow this rule to cascade Issu
|
|
|
5
5
|
## Boundary
|
|
6
6
|
|
|
7
7
|
- Issue title and body must come from `task.md` only
|
|
8
|
-
- Do not read `analysis.md`, `plan.md`, `
|
|
8
|
+
- Do not read `analysis.md`, `review-analysis.md`, `plan.md`, `review-plan.md`, `code.md`, or any review-code artifact
|
|
9
9
|
- Persistent outputs are limited to the remote Issue and the `issue_number` written back to `task.md`
|
|
10
10
|
- If Issue creation fails, do not roll back `task.md`; the current task remains valid for the workflow, and the user can later manually fill `issue_number` so other skills' cascade sync takes over
|
|
11
11
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## 行为边界
|
|
6
6
|
|
|
7
7
|
- Issue 标题和正文只能来自 `task.md`
|
|
8
|
-
- 不读取 `analysis.md`、`plan.md`、`
|
|
8
|
+
- 不读取 `analysis.md`、`review-analysis.md`、`plan.md`、`review-plan.md`、`code.md` 或代码审查产物
|
|
9
9
|
- 持久产物只有:远端 Issue + `task.md` 中回写的 `issue_number`
|
|
10
10
|
- Issue 创建失败时不回滚 `task.md`;当前 task 仍可继续后续工作流,未来可由用户手动写入 `issue_number`,让其它技能的级联同步接管
|
|
11
11
|
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
当前代码平台未提供 Issue 创建规则。
|
|
4
4
|
|
|
5
|
-
`create-task` 在本平台上会跳过级联创建 Issue 步骤;本地 `task.md` 仍然是有效产物。如果将来需要把任务绑定到一个 Issue,可手动在 `task.md` 中写入 `issue_number`,后续技能(`commit` / `
|
|
5
|
+
`create-task` 在本平台上会跳过级联创建 Issue 步骤;本地 `task.md` 仍然是有效产物。如果将来需要把任务绑定到一个 Issue,可手动在 `task.md` 中写入 `issue_number`,后续技能(`commit` / `code-task` / `complete-task` 等)会按既有的级联同步规则自动接管 Issue 元数据更新。
|