@fitlab-ai/agent-infra 0.8.1 → 0.8.3
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 +3 -3
- package/dist/bin/cli.js +4 -4
- package/dist/lib/cp.js +1 -1
- package/dist/lib/decide.js +2 -2
- package/dist/lib/defaults.json +2 -0
- package/dist/lib/init.js +7 -7
- package/dist/lib/merge.js +1 -1
- package/dist/lib/prompt.js +1 -1
- package/dist/lib/run/index.js +7 -7
- package/dist/lib/run/prompt.js +1 -1
- package/dist/lib/sandbox/capture.js +5 -5
- package/dist/lib/sandbox/clipboard/bridge.js +48 -13
- package/dist/lib/sandbox/clipboard/index.js +5 -6
- package/dist/lib/sandbox/clipboard/linux.js +91 -0
- package/dist/lib/sandbox/clipboard/paths.js +1 -1
- package/dist/lib/sandbox/clipboard/win32.js +144 -0
- package/dist/lib/sandbox/commands/create.js +101 -66
- package/dist/lib/sandbox/commands/enter.js +9 -9
- package/dist/lib/sandbox/commands/list-running.js +2 -2
- package/dist/lib/sandbox/commands/ls.js +7 -7
- package/dist/lib/sandbox/commands/prune.js +8 -8
- package/dist/lib/sandbox/commands/rebuild.js +38 -50
- package/dist/lib/sandbox/commands/refresh.js +2 -2
- package/dist/lib/sandbox/commands/rm.js +11 -11
- package/dist/lib/sandbox/commands/show.js +4 -4
- package/dist/lib/sandbox/commands/start.js +4 -4
- package/dist/lib/sandbox/commands/vm.js +4 -4
- package/dist/lib/sandbox/config.js +11 -5
- package/dist/lib/sandbox/constants.js +4 -1
- package/dist/lib/sandbox/credentials.js +1 -1
- package/dist/lib/sandbox/dotfiles.js +1 -1
- package/dist/lib/sandbox/engine.js +3 -3
- package/dist/lib/sandbox/engines/index.js +5 -5
- package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
- package/dist/lib/sandbox/image-build.js +80 -0
- package/dist/lib/sandbox/image-prune.js +2 -2
- package/dist/lib/sandbox/index.js +10 -10
- package/dist/lib/sandbox/managed-fs.js +1 -1
- package/dist/lib/sandbox/readme-scaffold.js +1 -1
- package/dist/lib/sandbox/runtimes/node20.dockerfile +1 -1
- package/dist/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/dist/lib/sandbox/tools.js +11 -2
- package/dist/lib/server/adapters/feishu/index.js +12 -3
- package/dist/lib/server/adapters/feishu/renderer.js +88 -0
- package/dist/lib/server/adapters/feishu/transport.js +9 -2
- package/dist/lib/server/daemon.js +64 -43
- package/dist/lib/server/display.js +83 -0
- package/dist/lib/server/index.js +2 -2
- package/dist/lib/server/process-control.js +2 -2
- package/dist/lib/server/protocol.js +22 -6
- package/dist/lib/server/streamer.js +5 -4
- package/dist/lib/task/commands/cat.js +2 -2
- package/dist/lib/task/commands/decisions.js +4 -4
- package/dist/lib/task/commands/files.js +3 -3
- package/dist/lib/task/commands/grep.js +3 -3
- package/dist/lib/task/commands/issue-body.js +4 -4
- package/dist/lib/task/commands/log.js +5 -5
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/commands/show.js +1 -1
- package/dist/lib/task/commands/status.js +71 -25
- package/dist/lib/task/index.js +9 -9
- package/dist/lib/task/resolve-ref.js +1 -1
- package/dist/lib/task/workflow-warnings.js +94 -0
- package/dist/lib/update.js +35 -4
- package/lib/defaults.json +2 -0
- package/lib/sandbox/clipboard/bridge.ts +50 -8
- package/lib/sandbox/clipboard/index.ts +5 -6
- package/lib/sandbox/clipboard/linux.ts +124 -0
- package/lib/sandbox/clipboard/win32.ts +173 -0
- package/lib/sandbox/commands/create.ts +123 -65
- package/lib/sandbox/commands/rebuild.ts +42 -54
- package/lib/sandbox/config.ts +14 -2
- package/lib/sandbox/constants.ts +4 -0
- package/lib/sandbox/image-build.ts +134 -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/_contract.ts +3 -0
- package/lib/server/adapters/feishu/index.ts +12 -2
- package/lib/server/adapters/feishu/renderer.ts +99 -0
- package/lib/server/adapters/feishu/transport.ts +19 -3
- package/lib/server/daemon.ts +74 -38
- package/lib/server/display.ts +136 -0
- package/lib/server/protocol.ts +20 -4
- package/lib/server/streamer.ts +5 -4
- package/lib/task/commands/log.ts +3 -3
- package/lib/task/commands/status.ts +102 -21
- package/lib/task/workflow-warnings.ts +121 -0
- package/lib/update.ts +34 -0
- package/package.json +3 -3
- package/templates/.agents/README.en.md +3 -3
- package/templates/.agents/README.zh-CN.md +3 -3
- package/templates/.agents/rules/create-issue.github.en.md +3 -3
- package/templates/.agents/rules/create-issue.github.zh-CN.md +3 -3
- package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
- package/templates/.agents/rules/issue-sync.github.en.md +13 -0
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
- package/templates/.agents/rules/next-step-output.en.md +15 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
- package/templates/.agents/rules/pr-sync.github.en.md +17 -2
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
- package/templates/.agents/scripts/validate-artifact.js +120 -0
- package/templates/.agents/scripts/workflow-warnings.js +290 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +118 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +118 -0
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.en.md +4 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +33 -1
- package/templates/.agents/templates/task.en.md +7 -0
- package/templates/.agents/templates/task.zh-CN.md +7 -0
- package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
- package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
- package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
- package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
- package/templates/.github/workflows/metadata-sync.yml +5 -0
- package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
parsePositiveIntegerOption,
|
|
17
17
|
sandboxBranchLabel,
|
|
18
18
|
sandboxImageConfigLabel,
|
|
19
|
+
sandboxImageRefreshLabel,
|
|
19
20
|
sandboxLabel,
|
|
20
21
|
shareBranchDir,
|
|
21
22
|
shareCommonDir,
|
|
@@ -37,17 +38,13 @@ import {
|
|
|
37
38
|
} from '../shell.ts';
|
|
38
39
|
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
39
40
|
import {
|
|
40
|
-
imageSignatureFields,
|
|
41
41
|
resolveTools,
|
|
42
|
-
toolConfigDirCandidates
|
|
43
|
-
toolNpmPackagesArg,
|
|
44
|
-
toolShellInstallScriptBase64
|
|
42
|
+
toolConfigDirCandidates
|
|
45
43
|
} from '../tools.ts';
|
|
46
44
|
import type { SandboxTool } from '../tools.ts';
|
|
47
45
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
48
46
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
49
47
|
import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
50
|
-
import { resolveBuildUid } from '../engines/native.ts';
|
|
51
48
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
52
49
|
import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.ts';
|
|
53
50
|
import {
|
|
@@ -56,6 +53,14 @@ import {
|
|
|
56
53
|
validateClaudeCredentialsEnvOverride
|
|
57
54
|
} from '../credentials.ts';
|
|
58
55
|
import { detectHostTimezone } from '../host-timezone.ts';
|
|
56
|
+
import {
|
|
57
|
+
buildImageSignature,
|
|
58
|
+
buildSandboxImageArgs,
|
|
59
|
+
isRefreshDisabled,
|
|
60
|
+
isRefreshDue,
|
|
61
|
+
parseImageLabels,
|
|
62
|
+
parseRefreshTimestamp
|
|
63
|
+
} from '../image-build.ts';
|
|
59
64
|
|
|
60
65
|
const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
|
|
61
66
|
const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
|
|
@@ -82,7 +87,7 @@ alias gy='gemini --yolo; tput ed'
|
|
|
82
87
|
`;
|
|
83
88
|
const CONTAINER_HOME = '/home/devuser';
|
|
84
89
|
const CONTAINER_SHELL_CONFIG_MOUNT = `${CONTAINER_HOME}/.host-shell-config`;
|
|
85
|
-
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>]
|
|
90
|
+
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh]
|
|
86
91
|
|
|
87
92
|
Host aliases:
|
|
88
93
|
${'~'}/.agent-infra/aliases/sandbox.sh is auto-created on first run and exposed
|
|
@@ -115,16 +120,6 @@ type HostShellConfig = {
|
|
|
115
120
|
mounts: Array<{ hostPath: string; containerPath: string }>;
|
|
116
121
|
};
|
|
117
122
|
|
|
118
|
-
function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTool[]): string {
|
|
119
|
-
return createHash('sha256')
|
|
120
|
-
.update(JSON.stringify({
|
|
121
|
-
dockerfile: preparedDockerfile.signature,
|
|
122
|
-
tools: imageSignatureFields(tools)
|
|
123
|
-
}))
|
|
124
|
-
.digest('hex')
|
|
125
|
-
.slice(0, 12);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
123
|
function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
|
|
129
124
|
return tools.map((tool) => {
|
|
130
125
|
const candidates = toolConfigDirCandidates(tool, config.project, branch);
|
|
@@ -915,6 +910,42 @@ function resolveHostCatalogPath(value: unknown, hostHomeDir: string): string | n
|
|
|
915
910
|
}
|
|
916
911
|
}
|
|
917
912
|
|
|
913
|
+
const CODEX_DISABLED_FEATURE_FLAGS = ['apps', 'enable_mcp_apps'] as const;
|
|
914
|
+
|
|
915
|
+
function removeCodexMcpServers(sandboxParsed: JsonObject): boolean {
|
|
916
|
+
if (!Object.hasOwn(sandboxParsed, 'mcp_servers')) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
delete sandboxParsed.mcp_servers;
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function inheritDisabledCodexFeatureFlags(sandboxParsed: JsonObject, hostParsed: JsonObject): boolean {
|
|
924
|
+
if (!isJsonObjectRecord(hostParsed.features)) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
if (Object.hasOwn(sandboxParsed, 'features') && !isJsonObjectRecord(sandboxParsed.features)) {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
let sandboxFeatures = sandboxParsed.features as JsonObject | undefined;
|
|
932
|
+
let changed = false;
|
|
933
|
+
for (const key of CODEX_DISABLED_FEATURE_FLAGS) {
|
|
934
|
+
if (hostParsed.features[key] !== false) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (!sandboxFeatures) {
|
|
938
|
+
sandboxFeatures = {};
|
|
939
|
+
sandboxParsed.features = sandboxFeatures;
|
|
940
|
+
}
|
|
941
|
+
if (sandboxFeatures[key] !== false) {
|
|
942
|
+
sandboxFeatures[key] = false;
|
|
943
|
+
changed = true;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return changed;
|
|
947
|
+
}
|
|
948
|
+
|
|
918
949
|
export function ensureCodexModelInheritance(
|
|
919
950
|
toolDir: string,
|
|
920
951
|
hostHomeDir?: string,
|
|
@@ -947,13 +978,15 @@ export function ensureCodexModelInheritance(
|
|
|
947
978
|
}
|
|
948
979
|
}
|
|
949
980
|
|
|
981
|
+
let changed = removeCodexMcpServers(sandboxParsed);
|
|
982
|
+
changed = inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) || changed;
|
|
983
|
+
|
|
950
984
|
const inheritSpecs: Array<readonly [string, 'string' | 'number']> = [
|
|
951
985
|
['model', 'string'],
|
|
952
986
|
['model_reasoning_effort', 'string'],
|
|
953
987
|
['model_auto_compact_token_limit', 'number']
|
|
954
988
|
];
|
|
955
989
|
|
|
956
|
-
let changed = false;
|
|
957
990
|
for (const [key, type] of inheritSpecs) {
|
|
958
991
|
if (Object.hasOwn(sandboxParsed, key)) {
|
|
959
992
|
continue;
|
|
@@ -1153,43 +1186,43 @@ export function buildImage(
|
|
|
1153
1186
|
runFn = runEngine,
|
|
1154
1187
|
runSafeFn = runSafeEngine,
|
|
1155
1188
|
runVerboseFn = runVerboseEngine,
|
|
1156
|
-
env = process.env
|
|
1189
|
+
env = process.env,
|
|
1190
|
+
refresh = false,
|
|
1191
|
+
lastRefresh
|
|
1157
1192
|
}: {
|
|
1158
1193
|
engine?: string;
|
|
1159
1194
|
runFn?: EngineRunFn;
|
|
1160
1195
|
runSafeFn?: EngineRunSafeFn;
|
|
1161
1196
|
runVerboseFn?: EngineRunVerboseFn;
|
|
1162
1197
|
env?: NodeJS.ProcessEnv;
|
|
1198
|
+
refresh?: boolean;
|
|
1199
|
+
lastRefresh?: number;
|
|
1163
1200
|
} = {}
|
|
1164
1201
|
): void {
|
|
1165
1202
|
const selectedEngine = engine ?? detectEngine({ engine: config.engine });
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1203
|
+
runVerboseFn(
|
|
1204
|
+
selectedEngine,
|
|
1205
|
+
'docker',
|
|
1206
|
+
buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
|
|
1207
|
+
engine: selectedEngine,
|
|
1208
|
+
runFn,
|
|
1209
|
+
runSafeFn,
|
|
1210
|
+
env,
|
|
1211
|
+
refresh,
|
|
1212
|
+
lastRefresh
|
|
1213
|
+
}),
|
|
1214
|
+
{ cwd: config.repoRoot }
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1172
1217
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
'
|
|
1176
|
-
|
|
1177
|
-
'--
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
'--build-arg',
|
|
1182
|
-
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
1183
|
-
'--build-arg',
|
|
1184
|
-
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
1185
|
-
'--label',
|
|
1186
|
-
sandboxLabel(config),
|
|
1187
|
-
'--label',
|
|
1188
|
-
`${sandboxImageConfigLabel(config)}=${imageSignature}`,
|
|
1189
|
-
'-f',
|
|
1190
|
-
toEnginePath(selectedEngine, dockerfilePath),
|
|
1191
|
-
toEnginePath(selectedEngine, config.repoRoot)
|
|
1192
|
-
], { cwd: config.repoRoot });
|
|
1218
|
+
function readImageLabels(config: Pick<SandboxCreateConfig, 'imageName'> & Pick<SandboxCreateConfig, 'project'>, engine: string): Record<string, string> {
|
|
1219
|
+
return parseImageLabels(runSafeEngine(engine, 'docker', [
|
|
1220
|
+
'image',
|
|
1221
|
+
'inspect',
|
|
1222
|
+
'--format',
|
|
1223
|
+
'{{ json .Config.Labels }}',
|
|
1224
|
+
config.imageName
|
|
1225
|
+
]));
|
|
1193
1226
|
}
|
|
1194
1227
|
|
|
1195
1228
|
export async function create(args: string[]): Promise<void> {
|
|
@@ -1200,6 +1233,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1200
1233
|
options: {
|
|
1201
1234
|
cpu: { type: 'string' },
|
|
1202
1235
|
memory: { type: 'string' },
|
|
1236
|
+
'no-refresh': { type: 'boolean' },
|
|
1203
1237
|
help: { type: 'boolean', short: 'h' }
|
|
1204
1238
|
}
|
|
1205
1239
|
});
|
|
@@ -1249,7 +1283,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1249
1283
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
1250
1284
|
const preparedDockerfile = prepareDockerfile(effectiveConfig);
|
|
1251
1285
|
const baseBranch = base ?? runSafe('git', ['-C', effectiveConfig.repoRoot, 'branch', '--show-current']);
|
|
1252
|
-
const expectedImageSignature =
|
|
1286
|
+
const expectedImageSignature = buildImageSignature(preparedDockerfile, tools);
|
|
1253
1287
|
const engine = detectEngine(effectiveConfig);
|
|
1254
1288
|
|
|
1255
1289
|
p.intro(pc.cyan('AI Sandbox'));
|
|
@@ -1272,27 +1306,53 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1272
1306
|
p.log.success('Docker is ready');
|
|
1273
1307
|
|
|
1274
1308
|
const imageExists = runOkEngine(engine, 'docker', ['image', 'inspect', effectiveConfig.imageName]);
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
])
|
|
1283
|
-
|
|
1284
|
-
const needsImageBuild =
|
|
1309
|
+
const imageLabels = imageExists ? readImageLabels(effectiveConfig, engine) : {};
|
|
1310
|
+
const currentImageSignature = imageLabels[sandboxImageConfigLabel(effectiveConfig)] ?? '';
|
|
1311
|
+
const currentLastRefresh = parseRefreshTimestamp(imageLabels[sandboxImageRefreshLabel(effectiveConfig)] ?? '');
|
|
1312
|
+
const signatureStale = !imageExists || currentImageSignature !== expectedImageSignature;
|
|
1313
|
+
const now = Date.now();
|
|
1314
|
+
const refreshDue = imageExists
|
|
1315
|
+
&& !signatureStale
|
|
1316
|
+
&& !isRefreshDisabled(process.env, values['no-refresh'] ?? false)
|
|
1317
|
+
&& isRefreshDue(currentLastRefresh, now, effectiveConfig.refreshIntervalDays);
|
|
1318
|
+
const needsImageBuild = signatureStale || refreshDue;
|
|
1285
1319
|
|
|
1286
1320
|
if (needsImageBuild) {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1321
|
+
const buildRefresh = !imageExists || refreshDue;
|
|
1322
|
+
const buildLastRefresh = buildRefresh ? now : currentLastRefresh || 0;
|
|
1323
|
+
|
|
1324
|
+
p.log.step(
|
|
1325
|
+
refreshDue
|
|
1326
|
+
? 'Refreshing stale image...'
|
|
1327
|
+
: imageExists
|
|
1328
|
+
? 'Rebuilding stale image...'
|
|
1329
|
+
: 'Building image for first use...'
|
|
1294
1330
|
);
|
|
1295
|
-
|
|
1331
|
+
try {
|
|
1332
|
+
buildImage(
|
|
1333
|
+
effectiveConfig,
|
|
1334
|
+
tools,
|
|
1335
|
+
preparedDockerfile.path,
|
|
1336
|
+
expectedImageSignature,
|
|
1337
|
+
{ engine, refresh: buildRefresh, lastRefresh: buildLastRefresh }
|
|
1338
|
+
);
|
|
1339
|
+
p.log.success(
|
|
1340
|
+
refreshDue
|
|
1341
|
+
? 'Image refreshed'
|
|
1342
|
+
: imageExists
|
|
1343
|
+
? 'Image rebuilt'
|
|
1344
|
+
: 'Image built'
|
|
1345
|
+
);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (refreshDue && !signatureStale && imageExists) {
|
|
1348
|
+
p.log.warn(
|
|
1349
|
+
'Scheduled sandbox image refresh failed; continuing with the existing image. ' +
|
|
1350
|
+
commandErrorMessage(error)
|
|
1351
|
+
);
|
|
1352
|
+
} else {
|
|
1353
|
+
throw error;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1296
1356
|
} else {
|
|
1297
1357
|
p.log.step(`Using existing image ${effectiveConfig.imageName}`);
|
|
1298
1358
|
}
|
|
@@ -1541,8 +1601,6 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1541
1601
|
path.join(effectiveConfig.repoRoot, '.git'),
|
|
1542
1602
|
`${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`
|
|
1543
1603
|
),
|
|
1544
|
-
'-v',
|
|
1545
|
-
volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
|
|
1546
1604
|
...dotfilesMount,
|
|
1547
1605
|
...toolVolumes,
|
|
1548
1606
|
...tmpfsArgs,
|
|
@@ -1,40 +1,27 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
2
|
import * as p from '@clack/prompts';
|
|
4
3
|
import pc from 'picocolors';
|
|
5
4
|
import { loadConfig } from '../config.ts';
|
|
6
5
|
import type { SandboxConfig } from '../config.ts';
|
|
7
6
|
import { prepareDockerfile } from '../dockerfile.ts';
|
|
8
|
-
import {
|
|
7
|
+
import { sandboxImageRefreshLabel } from '../constants.ts';
|
|
9
8
|
import { detectEngine, ensureDocker } from '../engine.ts';
|
|
10
|
-
import { runEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
|
|
9
|
+
import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
|
|
11
10
|
import { pruneSandboxDanglingImages } from '../image-prune.ts';
|
|
12
|
-
import {
|
|
13
|
-
imageSignatureFields,
|
|
14
|
-
resolveTools,
|
|
15
|
-
toolNpmPackagesArg,
|
|
16
|
-
toolShellInstallScriptBase64
|
|
17
|
-
} from '../tools.ts';
|
|
11
|
+
import { resolveTools } from '../tools.ts';
|
|
18
12
|
import type { SandboxTool } from '../tools.ts';
|
|
19
|
-
import {
|
|
20
|
-
|
|
13
|
+
import {
|
|
14
|
+
buildImageSignature,
|
|
15
|
+
buildSandboxImageArgs,
|
|
16
|
+
parseImageLabels,
|
|
17
|
+
parseRefreshTimestamp
|
|
18
|
+
} from '../image-build.ts';
|
|
21
19
|
|
|
22
20
|
const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
|
|
23
21
|
|
|
24
|
-
type PreparedDockerfile = ReturnType<typeof prepareDockerfile>;
|
|
25
22
|
type EngineRunFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => string;
|
|
26
23
|
type EngineRunSafeFn = EngineRunFn;
|
|
27
24
|
|
|
28
|
-
function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTool[]): string {
|
|
29
|
-
return createHash('sha256')
|
|
30
|
-
.update(JSON.stringify({
|
|
31
|
-
dockerfile: preparedDockerfile.signature,
|
|
32
|
-
tools: imageSignatureFields(tools)
|
|
33
|
-
}))
|
|
34
|
-
.digest('hex')
|
|
35
|
-
.slice(0, 12);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
25
|
export function buildArgs(
|
|
39
26
|
config: SandboxConfig,
|
|
40
27
|
tools: SandboxTool[],
|
|
@@ -45,49 +32,41 @@ export function buildArgs(
|
|
|
45
32
|
runFn = runEngine,
|
|
46
33
|
runSafeFn = runSafeEngine,
|
|
47
34
|
env = process.env,
|
|
48
|
-
refresh = false
|
|
35
|
+
refresh = false,
|
|
36
|
+
lastRefresh
|
|
49
37
|
}: {
|
|
50
38
|
engine?: string;
|
|
51
39
|
runFn?: EngineRunFn;
|
|
52
40
|
runSafeFn?: EngineRunSafeFn;
|
|
53
41
|
env?: NodeJS.ProcessEnv;
|
|
54
42
|
refresh?: boolean;
|
|
43
|
+
lastRefresh?: number;
|
|
55
44
|
} = {}
|
|
56
45
|
): string[] {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
engine: selectedEngine,
|
|
46
|
+
return buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
|
|
47
|
+
engine: engine ?? detectEngine(config),
|
|
60
48
|
runFn,
|
|
61
49
|
runSafeFn,
|
|
62
|
-
env
|
|
50
|
+
env,
|
|
51
|
+
refresh,
|
|
52
|
+
lastRefresh
|
|
63
53
|
});
|
|
54
|
+
}
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
config.imageName,
|
|
69
|
-
'--build-arg',
|
|
70
|
-
`HOST_UID=${hostUid}`,
|
|
71
|
-
'--build-arg',
|
|
72
|
-
`HOST_GID=${hostGid}`,
|
|
73
|
-
'--build-arg',
|
|
74
|
-
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
75
|
-
'--build-arg',
|
|
76
|
-
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
77
|
-
'--label',
|
|
78
|
-
sandboxLabel(config),
|
|
79
|
-
'--label',
|
|
80
|
-
`${sandboxImageConfigLabel(config)}=${imageSignature}`,
|
|
81
|
-
'-f',
|
|
82
|
-
toEnginePath(selectedEngine, dockerfilePath),
|
|
83
|
-
toEnginePath(selectedEngine, config.repoRoot)
|
|
84
|
-
];
|
|
85
|
-
|
|
86
|
-
if (refresh) {
|
|
87
|
-
args.splice(1, 0, '--no-cache', '--pull');
|
|
56
|
+
function readExistingLastRefresh(config: SandboxConfig, engine: string): number {
|
|
57
|
+
if (!runOkEngine(engine, 'docker', ['image', 'inspect', config.imageName])) {
|
|
58
|
+
return 0;
|
|
88
59
|
}
|
|
89
60
|
|
|
90
|
-
|
|
61
|
+
const labels = parseImageLabels(runSafeEngine(engine, 'docker', [
|
|
62
|
+
'image',
|
|
63
|
+
'inspect',
|
|
64
|
+
'--format',
|
|
65
|
+
'{{ json .Config.Labels }}',
|
|
66
|
+
config.imageName
|
|
67
|
+
]));
|
|
68
|
+
|
|
69
|
+
return parseRefreshTimestamp(labels[sandboxImageRefreshLabel(config)] ?? '');
|
|
91
70
|
}
|
|
92
71
|
|
|
93
72
|
export async function rebuild(args: string[]): Promise<void> {
|
|
@@ -110,19 +89,24 @@ export async function rebuild(args: string[]): Promise<void> {
|
|
|
110
89
|
const config = loadConfig();
|
|
111
90
|
const tools = resolveTools(config);
|
|
112
91
|
const preparedDockerfile = prepareDockerfile(config);
|
|
113
|
-
const imageSignature =
|
|
92
|
+
const imageSignature = buildImageSignature(preparedDockerfile, tools);
|
|
114
93
|
const quiet = values.quiet ?? false;
|
|
115
94
|
const refresh = values.refresh ?? false;
|
|
116
95
|
const engine = detectEngine(config);
|
|
117
96
|
|
|
118
97
|
await ensureDocker(config, undefined);
|
|
98
|
+
const lastRefresh = refresh ? Date.now() : readExistingLastRefresh(config, engine);
|
|
119
99
|
p.intro(pc.cyan('Rebuilding sandbox image'));
|
|
120
100
|
|
|
121
101
|
try {
|
|
122
102
|
if (quiet) {
|
|
123
103
|
const spinner = p.spinner();
|
|
124
104
|
spinner.start('Building image...');
|
|
125
|
-
runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
|
|
105
|
+
runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
|
|
106
|
+
engine,
|
|
107
|
+
refresh,
|
|
108
|
+
lastRefresh
|
|
109
|
+
}), {
|
|
126
110
|
cwd: config.repoRoot
|
|
127
111
|
});
|
|
128
112
|
spinner.stop(pc.green('Sandbox image rebuilt'));
|
|
@@ -131,7 +115,11 @@ export async function rebuild(args: string[]): Promise<void> {
|
|
|
131
115
|
runVerboseEngine(
|
|
132
116
|
engine,
|
|
133
117
|
'docker',
|
|
134
|
-
buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
|
|
118
|
+
buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
|
|
119
|
+
engine,
|
|
120
|
+
refresh,
|
|
121
|
+
lastRefresh
|
|
122
|
+
}),
|
|
135
123
|
{ cwd: config.repoRoot }
|
|
136
124
|
);
|
|
137
125
|
p.log.success(pc.green('Sandbox image rebuilt'));
|
package/lib/sandbox/config.ts
CHANGED
|
@@ -12,7 +12,8 @@ import type { SandboxTool } from './tools.ts';
|
|
|
12
12
|
const DEFAULTS = Object.freeze({
|
|
13
13
|
engine: null,
|
|
14
14
|
runtimes: ['node22'],
|
|
15
|
-
tools: ['claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
15
|
+
tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
16
|
+
refreshIntervalDays: 7,
|
|
16
17
|
dockerfile: null,
|
|
17
18
|
vm: {
|
|
18
19
|
cpu: null,
|
|
@@ -29,6 +30,7 @@ type SandboxConfigInput = {
|
|
|
29
30
|
runtimes?: string[];
|
|
30
31
|
tools?: string[];
|
|
31
32
|
customTools?: unknown;
|
|
33
|
+
refreshIntervalDays?: unknown;
|
|
32
34
|
dockerfile?: string | null;
|
|
33
35
|
vm?: Record<string, unknown>;
|
|
34
36
|
};
|
|
@@ -55,6 +57,7 @@ export type SandboxConfig = {
|
|
|
55
57
|
runtimes: string[];
|
|
56
58
|
tools: string[];
|
|
57
59
|
customTools: SandboxTool[];
|
|
60
|
+
refreshIntervalDays: number;
|
|
58
61
|
dockerfile: string | null;
|
|
59
62
|
vm: SandboxVmConfig;
|
|
60
63
|
};
|
|
@@ -80,11 +83,16 @@ function asPositiveNumberOrNull(value: unknown): number | null {
|
|
|
80
83
|
return typeof value === 'number' ? value : null;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
function
|
|
86
|
+
function asNonNegativeIntegerOrDefault(value: unknown, fallback: number): number {
|
|
87
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes: string[]; tools: string[]; refreshIntervalDays: number } {
|
|
84
91
|
return {
|
|
85
92
|
engine: DEFAULTS.engine,
|
|
86
93
|
runtimes: [...DEFAULTS.runtimes],
|
|
87
94
|
tools: [...DEFAULTS.tools],
|
|
95
|
+
refreshIntervalDays: DEFAULTS.refreshIntervalDays,
|
|
88
96
|
dockerfile: DEFAULTS.dockerfile,
|
|
89
97
|
vm: { ...DEFAULTS.vm }
|
|
90
98
|
};
|
|
@@ -160,6 +168,10 @@ export function loadConfig({
|
|
|
160
168
|
? [...sandbox.tools]
|
|
161
169
|
: defaults.tools,
|
|
162
170
|
customTools,
|
|
171
|
+
refreshIntervalDays: asNonNegativeIntegerOrDefault(
|
|
172
|
+
sandbox.refreshIntervalDays,
|
|
173
|
+
defaults.refreshIntervalDays
|
|
174
|
+
),
|
|
163
175
|
dockerfile,
|
|
164
176
|
vm: {
|
|
165
177
|
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
package/lib/sandbox/constants.ts
CHANGED
|
@@ -107,6 +107,10 @@ export function sandboxImageConfigLabel(config: Pick<SandboxPathConfig, 'project
|
|
|
107
107
|
return `${sandboxLabel(config)}.image-config`;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
export function sandboxImageRefreshLabel(config: Pick<SandboxPathConfig, 'project'>): string {
|
|
111
|
+
return `${sandboxLabel(config)}.last-refresh`;
|
|
112
|
+
}
|
|
113
|
+
|
|
110
114
|
export function parsePositiveIntegerOption(value: unknown, optionName: string): number | undefined {
|
|
111
115
|
if (value === undefined || value === null) {
|
|
112
116
|
return undefined;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { detectEngine } from './engine.ts';
|
|
3
|
+
import { resolveBuildUid } from './engines/native.ts';
|
|
4
|
+
import { toEnginePath } from './engines/wsl2-paths.ts';
|
|
5
|
+
import { sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel } from './constants.ts';
|
|
6
|
+
import { runEngine, runSafeEngine } from './shell.ts';
|
|
7
|
+
import {
|
|
8
|
+
imageSignatureFields,
|
|
9
|
+
toolNpmPackagesArg,
|
|
10
|
+
toolShellInstallScriptBase64
|
|
11
|
+
} from './tools.ts';
|
|
12
|
+
import type { SandboxTool } from './tools.ts';
|
|
13
|
+
|
|
14
|
+
type PreparedDockerfileSignature = {
|
|
15
|
+
signature: unknown;
|
|
16
|
+
};
|
|
17
|
+
type BuildImageConfig = {
|
|
18
|
+
project: string;
|
|
19
|
+
imageName: string;
|
|
20
|
+
repoRoot: string;
|
|
21
|
+
engine?: string | null;
|
|
22
|
+
};
|
|
23
|
+
type EngineRunFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => string;
|
|
24
|
+
type EngineRunSafeFn = EngineRunFn;
|
|
25
|
+
|
|
26
|
+
export function buildImageSignature(preparedDockerfile: PreparedDockerfileSignature, tools: SandboxTool[]): string {
|
|
27
|
+
return createHash('sha256')
|
|
28
|
+
.update(JSON.stringify({
|
|
29
|
+
dockerfile: preparedDockerfile.signature,
|
|
30
|
+
tools: imageSignatureFields(tools)
|
|
31
|
+
}))
|
|
32
|
+
.digest('hex')
|
|
33
|
+
.slice(0, 12);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildSandboxImageArgs(
|
|
37
|
+
config: BuildImageConfig,
|
|
38
|
+
tools: SandboxTool[],
|
|
39
|
+
dockerfilePath: string,
|
|
40
|
+
imageSignature: string,
|
|
41
|
+
{
|
|
42
|
+
engine,
|
|
43
|
+
runFn = runEngine,
|
|
44
|
+
runSafeFn = runSafeEngine,
|
|
45
|
+
env = process.env,
|
|
46
|
+
refresh = false,
|
|
47
|
+
lastRefresh
|
|
48
|
+
}: {
|
|
49
|
+
engine?: string;
|
|
50
|
+
runFn?: EngineRunFn;
|
|
51
|
+
runSafeFn?: EngineRunSafeFn;
|
|
52
|
+
env?: NodeJS.ProcessEnv;
|
|
53
|
+
refresh?: boolean;
|
|
54
|
+
lastRefresh?: number;
|
|
55
|
+
} = {}
|
|
56
|
+
): string[] {
|
|
57
|
+
const selectedEngine = engine ?? detectEngine({ engine: config.engine });
|
|
58
|
+
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
59
|
+
engine: selectedEngine,
|
|
60
|
+
runFn,
|
|
61
|
+
runSafeFn,
|
|
62
|
+
env
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const args = [
|
|
66
|
+
'build',
|
|
67
|
+
'-t',
|
|
68
|
+
config.imageName,
|
|
69
|
+
'--build-arg',
|
|
70
|
+
`HOST_UID=${hostUid}`,
|
|
71
|
+
'--build-arg',
|
|
72
|
+
`HOST_GID=${hostGid}`,
|
|
73
|
+
'--build-arg',
|
|
74
|
+
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
75
|
+
'--build-arg',
|
|
76
|
+
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
77
|
+
'--label',
|
|
78
|
+
sandboxLabel(config),
|
|
79
|
+
'--label',
|
|
80
|
+
`${sandboxImageConfigLabel(config)}=${imageSignature}`
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
if (lastRefresh !== undefined) {
|
|
84
|
+
args.push('--label', `${sandboxImageRefreshLabel(config)}=${lastRefresh}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
args.push(
|
|
88
|
+
'-f',
|
|
89
|
+
toEnginePath(selectedEngine, dockerfilePath),
|
|
90
|
+
toEnginePath(selectedEngine, config.repoRoot)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (refresh) {
|
|
94
|
+
args.splice(1, 0, '--no-cache', '--pull');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return args;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseRefreshTimestamp(value: string): number {
|
|
101
|
+
const parsed = Number(value);
|
|
102
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parseImageLabels(raw: string): Record<string, string> {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
108
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
return Object.fromEntries(
|
|
112
|
+
Object.entries(parsed).filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isRefreshDisabled(env: NodeJS.ProcessEnv, noRefreshFlag: boolean): boolean {
|
|
120
|
+
if (noRefreshFlag) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const value = env.AI_SANDBOX_NO_REFRESH?.trim().toLowerCase();
|
|
125
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isRefreshDue(lastRefresh: number, now: number, intervalDays: number): boolean {
|
|
129
|
+
if (intervalDays <= 0 || lastRefresh > now) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return now - lastRefresh >= intervalDays * 24 * 60 * 60 * 1000;
|
|
134
|
+
}
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -124,6 +124,15 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
124
124
|
{ hostPath: hostJoin(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
|
|
125
125
|
{ hostPath: hostJoin(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
|
|
126
126
|
]
|
|
127
|
+
},
|
|
128
|
+
'agent-infra': {
|
|
129
|
+
id: 'agent-infra',
|
|
130
|
+
name: 'agent-infra CLI',
|
|
131
|
+
install: { type: 'npm', cmd: '@fitlab-ai/agent-infra@latest' },
|
|
132
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'agent-infra'),
|
|
133
|
+
containerMount: '/home/devuser/.agent-infra-cli',
|
|
134
|
+
versionCmd: 'ai version --raw',
|
|
135
|
+
setupHint: 'Provides the ai and agent-infra CLI commands inside the sandbox.'
|
|
127
136
|
}
|
|
128
137
|
};
|
|
129
138
|
}
|