@fitlab-ai/agent-infra 0.5.10 → 0.6.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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/{cli.js → cli.ts} +21 -17
- package/dist/bin/cli.js +116 -0
- package/dist/lib/defaults.json +61 -0
- package/dist/lib/init.js +238 -0
- package/dist/lib/log.js +18 -0
- package/dist/lib/merge.js +747 -0
- package/dist/lib/paths.js +18 -0
- package/dist/lib/prompt.js +85 -0
- package/dist/lib/render.js +139 -0
- package/dist/lib/sandbox/commands/create.js +1173 -0
- package/dist/lib/sandbox/commands/enter.js +98 -0
- package/dist/lib/sandbox/commands/ls.js +93 -0
- package/dist/lib/sandbox/commands/rebuild.js +101 -0
- package/dist/lib/sandbox/commands/refresh.js +85 -0
- package/dist/lib/sandbox/commands/rm.js +226 -0
- package/dist/lib/sandbox/commands/vm.js +144 -0
- package/dist/lib/sandbox/config.js +85 -0
- package/dist/lib/sandbox/constants.js +104 -0
- package/dist/lib/sandbox/credentials.js +437 -0
- package/dist/lib/sandbox/dockerfile.js +76 -0
- package/dist/lib/sandbox/dotfiles.js +170 -0
- package/dist/lib/sandbox/engine.js +155 -0
- package/dist/lib/sandbox/engines/colima.js +64 -0
- package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
- package/dist/lib/sandbox/engines/index.js +25 -0
- package/dist/lib/sandbox/engines/native.js +96 -0
- package/dist/lib/sandbox/engines/orbstack.js +63 -0
- package/dist/lib/sandbox/engines/selinux.js +48 -0
- package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
- package/dist/lib/sandbox/engines/wsl2.js +57 -0
- package/dist/lib/sandbox/index.js +70 -0
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
- package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +148 -0
- package/dist/lib/sandbox/task-resolver.js +35 -0
- package/dist/lib/sandbox/tools.js +115 -0
- package/dist/lib/update.js +186 -0
- package/dist/lib/version.js +5 -0
- package/dist/package.json +5 -0
- package/lib/{init.js → init.ts} +48 -18
- package/lib/{log.js → log.ts} +4 -4
- package/lib/{merge.js → merge.ts} +129 -63
- package/lib/paths.ts +18 -0
- package/lib/{prompt.js → prompt.ts} +12 -12
- package/lib/{render.js → render.ts} +30 -17
- package/lib/sandbox/commands/{create.js → create.ts} +224 -118
- package/lib/sandbox/commands/{enter.js → enter.ts} +17 -14
- package/lib/sandbox/commands/{ls.js → ls.ts} +10 -10
- package/lib/sandbox/commands/{rebuild.js → rebuild.ts} +38 -21
- package/lib/sandbox/commands/{refresh.js → refresh.ts} +16 -7
- package/lib/sandbox/commands/{rm.js → rm.ts} +15 -13
- package/lib/sandbox/commands/{vm.js → vm.ts} +14 -11
- package/lib/sandbox/{config.js → config.ts} +55 -10
- package/lib/sandbox/{constants.js → constants.ts} +30 -18
- package/lib/sandbox/{credentials.js → credentials.ts} +160 -46
- package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
- package/lib/sandbox/{dotfiles.js → dotfiles.ts} +66 -19
- package/lib/sandbox/{engine.js → engine.ts} +57 -25
- package/lib/sandbox/engines/{colima.js → colima.ts} +9 -7
- package/lib/sandbox/engines/{docker-desktop.js → docker-desktop.ts} +5 -3
- package/lib/sandbox/engines/index.ts +74 -0
- package/lib/sandbox/engines/{native.js → native.ts} +25 -6
- package/lib/sandbox/engines/{orbstack.js → orbstack.ts} +7 -5
- package/lib/sandbox/engines/{selinux.js → selinux.ts} +11 -5
- package/lib/sandbox/engines/{wsl2-paths.js → wsl2-paths.ts} +15 -9
- package/lib/sandbox/engines/{wsl2.js → wsl2.ts} +9 -7
- package/lib/sandbox/{index.js → index.ts} +8 -8
- package/lib/sandbox/{shell.js → shell.ts} +30 -17
- package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
- package/lib/sandbox/{tools.js → tools.ts} +30 -26
- package/lib/{update.js → update.ts} +33 -10
- package/package.json +17 -9
- package/lib/paths.js +0 -9
- package/lib/sandbox/engines/index.js +0 -27
- /package/lib/{version.js → version.ts} +0 -0
|
@@ -2,7 +2,103 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { hostJoin } from './engines/wsl2-paths.
|
|
5
|
+
import { hostJoin } from './engines/wsl2-paths.ts';
|
|
6
|
+
|
|
7
|
+
type ExecFn = (file: string, args: string[], options?: Record<string, unknown>) => string | Buffer | void;
|
|
8
|
+
type ReadFn = (targetPath: string) => string;
|
|
9
|
+
type ExistsFn = (targetPath: string) => boolean;
|
|
10
|
+
type EnvFn = () => NodeJS.ProcessEnv;
|
|
11
|
+
|
|
12
|
+
type SecurityClassification = 'OK' | 'LOCKED' | 'NOT_FOUND' | 'OTHER';
|
|
13
|
+
|
|
14
|
+
type SecurityResult =
|
|
15
|
+
| { ok: true; stdout: string; stderr: ''; classification: 'OK' }
|
|
16
|
+
| { ok: false; stdout: ''; stderr: string; classification: Exclude<SecurityClassification, 'OK'> };
|
|
17
|
+
|
|
18
|
+
type CredentialStatus = 'OK'
|
|
19
|
+
| 'MISSING'
|
|
20
|
+
| 'STALE_ACCESS'
|
|
21
|
+
| 'KEYCHAIN_LOCKED'
|
|
22
|
+
| 'KEYCHAIN_ERROR'
|
|
23
|
+
| 'KEYCHAIN_WRITE_FAILED';
|
|
24
|
+
|
|
25
|
+
type CredentialInspection =
|
|
26
|
+
| { status: 'OK'; blob: string; expiresAt: unknown }
|
|
27
|
+
| { status: Exclude<CredentialStatus, 'OK' | 'KEYCHAIN_WRITE_FAILED'>; blob?: undefined; expiresAt?: unknown; detail?: string };
|
|
28
|
+
|
|
29
|
+
type CredentialEndpoint = CredentialInspection & {
|
|
30
|
+
name: string;
|
|
31
|
+
project?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type OkCredentialEndpoint = CredentialEndpoint & {
|
|
35
|
+
status: 'OK';
|
|
36
|
+
blob: string;
|
|
37
|
+
};
|
|
38
|
+
type ExpiringOkCredentialEndpoint = OkCredentialEndpoint & {
|
|
39
|
+
expiresAt: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type Warning = {
|
|
43
|
+
source?: string;
|
|
44
|
+
classification?: string;
|
|
45
|
+
message?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type WriteResult =
|
|
49
|
+
| { ok: true; classification?: undefined; error?: undefined }
|
|
50
|
+
| { ok: false; classification: Exclude<SecurityClassification, 'OK'>; error: string };
|
|
51
|
+
|
|
52
|
+
type InspectOptions = {
|
|
53
|
+
readFn?: ReadFn;
|
|
54
|
+
existsFn?: ExistsFn;
|
|
55
|
+
envFn?: EnvFn;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type WriteHostOptions = {
|
|
59
|
+
execFn?: ExecFn;
|
|
60
|
+
mkdirFn?: typeof fs.mkdirSync;
|
|
61
|
+
chmodFn?: typeof fs.chmodSync;
|
|
62
|
+
writeFileFn?: typeof fs.writeFileSync;
|
|
63
|
+
renameFn?: typeof fs.renameSync;
|
|
64
|
+
rmFn?: typeof fs.rmSync;
|
|
65
|
+
randomFn?: () => string;
|
|
66
|
+
envFn?: EnvFn;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type ReconcileOptions = InspectOptions & {
|
|
70
|
+
execFn?: ExecFn;
|
|
71
|
+
writeFn?: (home: string, project: string, blob: string) => void;
|
|
72
|
+
writeHostFn?: (home: string, blob: string, options?: WriteHostOptions) => WriteResult;
|
|
73
|
+
discoverFn?: (home: string) => string[];
|
|
74
|
+
projects?: string[] | null;
|
|
75
|
+
singleProject?: string | null;
|
|
76
|
+
inspection?: CredentialInspection | null;
|
|
77
|
+
};
|
|
78
|
+
type ReconcileResult = {
|
|
79
|
+
status: CredentialStatus;
|
|
80
|
+
authoritative: string | null;
|
|
81
|
+
expiresAt: unknown;
|
|
82
|
+
hostWritten: boolean;
|
|
83
|
+
filesWritten: string[];
|
|
84
|
+
fileErrors: Array<{ project: string; error: string }>;
|
|
85
|
+
warnings: Warning[];
|
|
86
|
+
detail?: string | null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type ResolvedTool = {
|
|
90
|
+
tool: {
|
|
91
|
+
id: string;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type CredentialPayload = {
|
|
96
|
+
claudeAiOauth?: CredentialPayload;
|
|
97
|
+
scopes?: unknown;
|
|
98
|
+
accessToken?: unknown;
|
|
99
|
+
refreshToken?: unknown;
|
|
100
|
+
expiresAt?: unknown;
|
|
101
|
+
};
|
|
6
102
|
|
|
7
103
|
const LOCKED_PATTERN = /errSecInteractionNotAllowed|User interaction is not allowed/i;
|
|
8
104
|
const NOT_FOUND_PATTERN = /errSecItemNotFound|specified item could not be found/i;
|
|
@@ -13,7 +109,11 @@ const REDACTION_PATTERNS = [
|
|
|
13
109
|
{ pattern: /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/gi, replacement: 'Bearer [REDACTED]' }
|
|
14
110
|
];
|
|
15
111
|
|
|
16
|
-
|
|
112
|
+
function errorMessage(error: unknown): string {
|
|
113
|
+
return error instanceof Error ? error.message : 'unknown error';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function redactCommandError(text: unknown): string {
|
|
17
117
|
if (!text || typeof text !== 'string') {
|
|
18
118
|
return '';
|
|
19
119
|
}
|
|
@@ -26,8 +126,10 @@ export function redactCommandError(text) {
|
|
|
26
126
|
|
|
27
127
|
export const redactSecurityOutput = redactCommandError;
|
|
28
128
|
|
|
29
|
-
function extractStderrSafely(error) {
|
|
30
|
-
const stderr = error
|
|
129
|
+
function extractStderrSafely(error: unknown): string {
|
|
130
|
+
const stderr = typeof error === 'object' && error !== null && 'stderr' in error
|
|
131
|
+
? error.stderr
|
|
132
|
+
: undefined;
|
|
31
133
|
if (Buffer.isBuffer(stderr)) {
|
|
32
134
|
return stderr.toString('utf8');
|
|
33
135
|
}
|
|
@@ -37,7 +139,7 @@ function extractStderrSafely(error) {
|
|
|
37
139
|
return '';
|
|
38
140
|
}
|
|
39
141
|
|
|
40
|
-
function classifySecurityFailure(text) {
|
|
142
|
+
function classifySecurityFailure(text: string): Exclude<SecurityClassification, 'OK'> {
|
|
41
143
|
if (LOCKED_PATTERN.test(text)) {
|
|
42
144
|
return 'LOCKED';
|
|
43
145
|
}
|
|
@@ -47,14 +149,16 @@ function classifySecurityFailure(text) {
|
|
|
47
149
|
return 'OTHER';
|
|
48
150
|
}
|
|
49
151
|
|
|
50
|
-
function runSecurity(args, options = {}, execFn = execFileSync) {
|
|
152
|
+
function runSecurity(args: string[], options: Record<string, unknown> = {}, execFn: ExecFn = execFileSync): SecurityResult {
|
|
51
153
|
try {
|
|
52
154
|
const stdout = execFn('security', args, {
|
|
53
155
|
encoding: 'utf8',
|
|
54
156
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
157
|
...options
|
|
56
158
|
});
|
|
57
|
-
const text = typeof stdout === 'string'
|
|
159
|
+
const text = typeof stdout === 'string'
|
|
160
|
+
? stdout
|
|
161
|
+
: (Buffer.isBuffer(stdout) ? stdout.toString('utf8') : '');
|
|
58
162
|
return { ok: true, stdout: text, stderr: '', classification: 'OK' };
|
|
59
163
|
} catch (error) {
|
|
60
164
|
const stderr = redactCommandError(extractStderrSafely(error));
|
|
@@ -67,7 +171,7 @@ function runSecurity(args, options = {}, execFn = execFileSync) {
|
|
|
67
171
|
}
|
|
68
172
|
}
|
|
69
173
|
|
|
70
|
-
export function buildLockedGuidance() {
|
|
174
|
+
export function buildLockedGuidance(): string {
|
|
71
175
|
return [
|
|
72
176
|
'macOS keychain is locked (errSecInteractionNotAllowed).',
|
|
73
177
|
'Options to recover:',
|
|
@@ -90,7 +194,7 @@ export function buildLockedGuidance() {
|
|
|
90
194
|
].join('\n');
|
|
91
195
|
}
|
|
92
196
|
|
|
93
|
-
export function claudeCredentialsEnvOverride(env = process.env) {
|
|
197
|
+
export function claudeCredentialsEnvOverride(env: NodeJS.ProcessEnv = process.env): { path: string; source: string } | null {
|
|
94
198
|
const raw = env?.AGENT_INFRA_CLAUDE_CREDENTIALS_FILE;
|
|
95
199
|
if (!raw || typeof raw !== 'string') {
|
|
96
200
|
return null;
|
|
@@ -98,7 +202,7 @@ export function claudeCredentialsEnvOverride(env = process.env) {
|
|
|
98
202
|
return { path: raw, source: 'AGENT_INFRA_CLAUDE_CREDENTIALS_FILE' };
|
|
99
203
|
}
|
|
100
204
|
|
|
101
|
-
export function validateClaudeCredentialsEnvOverride(env = process.env) {
|
|
205
|
+
export function validateClaudeCredentialsEnvOverride(env: NodeJS.ProcessEnv = process.env): void {
|
|
102
206
|
const raw = env?.AGENT_INFRA_CLAUDE_CREDENTIALS_FILE;
|
|
103
207
|
if (raw === undefined || raw === '') {
|
|
104
208
|
return;
|
|
@@ -112,15 +216,15 @@ export function validateClaudeCredentialsEnvOverride(env = process.env) {
|
|
|
112
216
|
|
|
113
217
|
// Reconcile treats the freshest valid endpoint as authoritative so sandbox
|
|
114
218
|
// token rotations can flow back to the host credential store.
|
|
115
|
-
function validateClaudeCredentialsBlob(raw, blob = null) {
|
|
219
|
+
function validateClaudeCredentialsBlob(raw: unknown, blob: string | null = null): CredentialInspection {
|
|
116
220
|
const trimmed = typeof raw === 'string' ? raw.trim() : '';
|
|
117
221
|
if (!trimmed) {
|
|
118
222
|
return { status: 'MISSING' };
|
|
119
223
|
}
|
|
120
224
|
|
|
121
|
-
let parsed;
|
|
225
|
+
let parsed: CredentialPayload;
|
|
122
226
|
try {
|
|
123
|
-
parsed = JSON.parse(trimmed);
|
|
227
|
+
parsed = JSON.parse(trimmed) as CredentialPayload;
|
|
124
228
|
} catch {
|
|
125
229
|
return { status: 'STALE_ACCESS' };
|
|
126
230
|
}
|
|
@@ -140,7 +244,11 @@ function validateClaudeCredentialsBlob(raw, blob = null) {
|
|
|
140
244
|
};
|
|
141
245
|
}
|
|
142
246
|
|
|
143
|
-
export function readClaudeCredentialsFile(
|
|
247
|
+
export function readClaudeCredentialsFile(
|
|
248
|
+
filePath: string,
|
|
249
|
+
readFn: ReadFn = (targetPath) => fs.readFileSync(targetPath, 'utf8'),
|
|
250
|
+
existsFn: ExistsFn = fs.existsSync
|
|
251
|
+
): CredentialInspection {
|
|
144
252
|
if (!existsFn(filePath)) {
|
|
145
253
|
return { status: 'MISSING' };
|
|
146
254
|
}
|
|
@@ -153,7 +261,11 @@ export function readClaudeCredentialsFile(filePath, readFn = (targetPath) => fs.
|
|
|
153
261
|
}
|
|
154
262
|
}
|
|
155
263
|
|
|
156
|
-
export function inspectClaudeKeychainStatus(
|
|
264
|
+
export function inspectClaudeKeychainStatus(
|
|
265
|
+
home: string,
|
|
266
|
+
execFn: ExecFn = execFileSync,
|
|
267
|
+
options: InspectOptions = {}
|
|
268
|
+
): CredentialInspection {
|
|
157
269
|
const {
|
|
158
270
|
readFn,
|
|
159
271
|
existsFn,
|
|
@@ -191,7 +303,7 @@ export function inspectClaudeKeychainStatus(home, execFn = execFileSync, options
|
|
|
191
303
|
return readClaudeCredentialsFile(credentialsPath, readFn, existsFn);
|
|
192
304
|
}
|
|
193
305
|
|
|
194
|
-
export function inspectClaudeMountFile(home, project, options = {}) {
|
|
306
|
+
export function inspectClaudeMountFile(home: string, project: string, options: InspectOptions = {}): CredentialInspection {
|
|
195
307
|
return readClaudeCredentialsFile(
|
|
196
308
|
claudeCredentialsPath(home, project),
|
|
197
309
|
options.readFn,
|
|
@@ -199,20 +311,20 @@ export function inspectClaudeMountFile(home, project, options = {}) {
|
|
|
199
311
|
);
|
|
200
312
|
}
|
|
201
313
|
|
|
202
|
-
export function extractClaudeCredentialsBlob(home, execFn = execFileSync) {
|
|
314
|
+
export function extractClaudeCredentialsBlob(home: string, execFn: ExecFn = execFileSync): string | null {
|
|
203
315
|
const inspection = inspectClaudeKeychainStatus(home, execFn);
|
|
204
316
|
return inspection.status === 'OK' ? inspection.blob : null;
|
|
205
317
|
}
|
|
206
318
|
|
|
207
|
-
export function claudeCredentialsDir(home, project) {
|
|
319
|
+
export function claudeCredentialsDir(home: string, project: string): string {
|
|
208
320
|
return hostJoin(home, '.agent-infra', 'credentials', project, 'claude-code');
|
|
209
321
|
}
|
|
210
322
|
|
|
211
|
-
export function claudeCredentialsPath(home, project) {
|
|
323
|
+
export function claudeCredentialsPath(home: string, project: string): string {
|
|
212
324
|
return hostJoin(claudeCredentialsDir(home, project), '.credentials.json');
|
|
213
325
|
}
|
|
214
326
|
|
|
215
|
-
export function writeClaudeCredentialsFile(home, project, blob) {
|
|
327
|
+
export function writeClaudeCredentialsFile(home: string, project: string, blob: string): void {
|
|
216
328
|
const dir = claudeCredentialsDir(home, project);
|
|
217
329
|
const filePath = claudeCredentialsPath(home, project);
|
|
218
330
|
|
|
@@ -222,7 +334,7 @@ export function writeClaudeCredentialsFile(home, project, blob) {
|
|
|
222
334
|
fs.chmodSync(filePath, 0o600);
|
|
223
335
|
}
|
|
224
336
|
|
|
225
|
-
export function discoverProjects(home) {
|
|
337
|
+
export function discoverProjects(home: string): string[] {
|
|
226
338
|
const credentialsRoot = hostJoin(home, '.agent-infra', 'credentials');
|
|
227
339
|
if (!fs.existsSync(credentialsRoot)) {
|
|
228
340
|
return [];
|
|
@@ -234,7 +346,7 @@ export function discoverProjects(home) {
|
|
|
234
346
|
.filter((project) => fs.existsSync(claudeCredentialsPath(home, project)));
|
|
235
347
|
}
|
|
236
348
|
|
|
237
|
-
export function writeClaudeCredentialsToHost(home, blob, options = {}) {
|
|
349
|
+
export function writeClaudeCredentialsToHost(home: string, blob: string, options: WriteHostOptions = {}): WriteResult {
|
|
238
350
|
const {
|
|
239
351
|
execFn = execFileSync,
|
|
240
352
|
mkdirFn = fs.mkdirSync,
|
|
@@ -293,24 +405,24 @@ export function writeClaudeCredentialsToHost(home, blob, options = {}) {
|
|
|
293
405
|
return {
|
|
294
406
|
ok: false,
|
|
295
407
|
classification: 'OTHER',
|
|
296
|
-
error: redactCommandError(error
|
|
408
|
+
error: redactCommandError(errorMessage(error))
|
|
297
409
|
};
|
|
298
410
|
}
|
|
299
411
|
}
|
|
300
412
|
|
|
301
|
-
export function formatCredentialWarnings(warnings = []) {
|
|
413
|
+
export function formatCredentialWarnings(warnings: Array<string | Warning> = []): string {
|
|
302
414
|
return warnings
|
|
303
415
|
.map((warning) => (typeof warning === 'string' ? warning : warning?.message))
|
|
304
416
|
.filter(Boolean)
|
|
305
417
|
.join('; ');
|
|
306
418
|
}
|
|
307
419
|
|
|
308
|
-
function endpointNameForFile(project) {
|
|
420
|
+
function endpointNameForFile(project: string): string {
|
|
309
421
|
return `file:${project}`;
|
|
310
422
|
}
|
|
311
423
|
|
|
312
|
-
function chooseAuthoritativeEndpoint(endpoints) {
|
|
313
|
-
const okEndpoints = endpoints.filter((endpoint) => endpoint.status === 'OK');
|
|
424
|
+
function chooseAuthoritativeEndpoint(endpoints: CredentialEndpoint[]): OkCredentialEndpoint | null {
|
|
425
|
+
const okEndpoints = endpoints.filter((endpoint): endpoint is OkCredentialEndpoint => endpoint.status === 'OK');
|
|
314
426
|
if (okEndpoints.length === 0) {
|
|
315
427
|
return null;
|
|
316
428
|
}
|
|
@@ -320,17 +432,19 @@ function chooseAuthoritativeEndpoint(endpoints) {
|
|
|
320
432
|
return hostEndpoint;
|
|
321
433
|
}
|
|
322
434
|
|
|
323
|
-
const withExpiresAt = okEndpoints.filter(
|
|
435
|
+
const withExpiresAt = okEndpoints.filter(
|
|
436
|
+
(endpoint): endpoint is ExpiringOkCredentialEndpoint => typeof endpoint.expiresAt === 'number'
|
|
437
|
+
);
|
|
324
438
|
if (withExpiresAt.length > 0) {
|
|
325
439
|
return withExpiresAt.reduce((best, endpoint) => (
|
|
326
440
|
endpoint.expiresAt > best.expiresAt ? endpoint : best
|
|
327
441
|
));
|
|
328
442
|
}
|
|
329
443
|
|
|
330
|
-
return okEndpoints.find((endpoint) => endpoint.name === 'host') ?? okEndpoints[0];
|
|
444
|
+
return okEndpoints.find((endpoint) => endpoint.name === 'host') ?? okEndpoints[0] ?? null;
|
|
331
445
|
}
|
|
332
446
|
|
|
333
|
-
function shouldWriteEndpoint(authoritative, target) {
|
|
447
|
+
function shouldWriteEndpoint(authoritative: OkCredentialEndpoint, target: CredentialEndpoint): boolean {
|
|
334
448
|
if (target.status !== 'OK') {
|
|
335
449
|
return true;
|
|
336
450
|
}
|
|
@@ -347,7 +461,7 @@ function shouldWriteEndpoint(authoritative, target) {
|
|
|
347
461
|
return false;
|
|
348
462
|
}
|
|
349
463
|
|
|
350
|
-
export function reconcileClaudeCredentials(home, options = {}) {
|
|
464
|
+
export function reconcileClaudeCredentials(home: string, options: ReconcileOptions = {}): ReconcileResult {
|
|
351
465
|
const {
|
|
352
466
|
execFn = execFileSync,
|
|
353
467
|
writeFn = writeClaudeCredentialsFile,
|
|
@@ -387,13 +501,13 @@ export function reconcileClaudeCredentials(home, options = {}) {
|
|
|
387
501
|
filesWritten: [],
|
|
388
502
|
fileErrors: [],
|
|
389
503
|
warnings: [],
|
|
390
|
-
detail: hostEndpoint.detail ?? null
|
|
504
|
+
detail: 'detail' in hostEndpoint ? hostEndpoint.detail ?? null : null
|
|
391
505
|
};
|
|
392
506
|
}
|
|
393
507
|
|
|
394
|
-
const warnings = [];
|
|
395
|
-
const filesWritten = [];
|
|
396
|
-
const fileErrors = [];
|
|
508
|
+
const warnings: Warning[] = [];
|
|
509
|
+
const filesWritten: string[] = [];
|
|
510
|
+
const fileErrors: Array<{ project: string; error: string }> = [];
|
|
397
511
|
let hostWritten = false;
|
|
398
512
|
let hostWriteFailed = false;
|
|
399
513
|
|
|
@@ -420,7 +534,7 @@ export function reconcileClaudeCredentials(home, options = {}) {
|
|
|
420
534
|
writeFn(home, endpoint.project, authoritative.blob);
|
|
421
535
|
filesWritten.push(endpoint.project);
|
|
422
536
|
} catch (error) {
|
|
423
|
-
fileErrors.push({ project: endpoint.project, error: error
|
|
537
|
+
fileErrors.push({ project: endpoint.project, error: errorMessage(error) });
|
|
424
538
|
}
|
|
425
539
|
}
|
|
426
540
|
|
|
@@ -435,7 +549,7 @@ export function reconcileClaudeCredentials(home, options = {}) {
|
|
|
435
549
|
};
|
|
436
550
|
}
|
|
437
551
|
|
|
438
|
-
export function syncClaudeCredentialsFromKeychain(home, project, options = {}) {
|
|
552
|
+
export function syncClaudeCredentialsFromKeychain(home: string, project: string, options: ReconcileOptions = {}) {
|
|
439
553
|
const result = reconcileClaudeCredentials(home, {
|
|
440
554
|
...options,
|
|
441
555
|
singleProject: project
|
|
@@ -454,7 +568,7 @@ export function syncClaudeCredentialsFromKeychain(home, project, options = {}) {
|
|
|
454
568
|
};
|
|
455
569
|
}
|
|
456
570
|
|
|
457
|
-
export function formatRemaining(expiresAt) {
|
|
571
|
+
export function formatRemaining(expiresAt: unknown): string {
|
|
458
572
|
if (typeof expiresAt !== 'number') {
|
|
459
573
|
return 'unknown';
|
|
460
574
|
}
|
|
@@ -471,19 +585,19 @@ export function formatRemaining(expiresAt) {
|
|
|
471
585
|
}
|
|
472
586
|
|
|
473
587
|
export function assertClaudeCredentialsAvailable(
|
|
474
|
-
home,
|
|
475
|
-
project,
|
|
476
|
-
resolvedTools,
|
|
477
|
-
extractFn = extractClaudeCredentialsBlob,
|
|
478
|
-
writeFn = writeClaudeCredentialsFile,
|
|
479
|
-
inspectFn = inspectClaudeKeychainStatus
|
|
480
|
-
) {
|
|
588
|
+
home: string,
|
|
589
|
+
project: string,
|
|
590
|
+
resolvedTools: ResolvedTool[],
|
|
591
|
+
extractFn: (home: string) => string | null = extractClaudeCredentialsBlob,
|
|
592
|
+
writeFn: (home: string, project: string, blob: string) => void = writeClaudeCredentialsFile,
|
|
593
|
+
inspectFn: (home: string) => CredentialInspection = inspectClaudeKeychainStatus
|
|
594
|
+
): void {
|
|
481
595
|
const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
482
596
|
if (!claudeCodeEntry) {
|
|
483
597
|
return;
|
|
484
598
|
}
|
|
485
599
|
|
|
486
|
-
let blob = null;
|
|
600
|
+
let blob: string | null = null;
|
|
487
601
|
const hasCustomInspectFn = inspectFn !== inspectClaudeKeychainStatus;
|
|
488
602
|
const hasCustomExtractFn = extractFn !== extractClaudeCredentialsBlob;
|
|
489
603
|
if (hasCustomInspectFn || !hasCustomExtractFn) {
|
|
@@ -9,19 +9,26 @@ const RUNTIMES_DIR = path.join(
|
|
|
9
9
|
'runtimes'
|
|
10
10
|
);
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
type DockerfileConfig = {
|
|
13
|
+
repoRoot: string;
|
|
14
|
+
project: string;
|
|
15
|
+
dockerfile: string | null;
|
|
16
|
+
runtimes: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function listRuntimeFragments(): string[] {
|
|
13
20
|
return fs.readdirSync(RUNTIMES_DIR)
|
|
14
21
|
.filter((file) => file.endsWith('.dockerfile'))
|
|
15
22
|
.map((file) => file.replace(/\.dockerfile$/, ''));
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
export function availableRuntimes() {
|
|
25
|
+
export function availableRuntimes(): string[] {
|
|
19
26
|
return listRuntimeFragments()
|
|
20
27
|
.filter((name) => name !== 'base' && name !== 'ai-tools')
|
|
21
28
|
.sort();
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
function dockerfileContent(config) {
|
|
31
|
+
function dockerfileContent(config: DockerfileConfig): string {
|
|
25
32
|
if (config.dockerfile) {
|
|
26
33
|
const customPath = path.resolve(config.repoRoot, config.dockerfile);
|
|
27
34
|
if (!fs.existsSync(customPath)) {
|
|
@@ -52,14 +59,14 @@ function dockerfileContent(config) {
|
|
|
52
59
|
return `${content}\n`;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
export function dockerfileSignature(config) {
|
|
62
|
+
export function dockerfileSignature(config: DockerfileConfig): string {
|
|
56
63
|
return createHash('sha256')
|
|
57
64
|
.update(dockerfileContent(config))
|
|
58
65
|
.digest('hex')
|
|
59
66
|
.slice(0, 12);
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
export function prepareDockerfile(config) {
|
|
69
|
+
export function prepareDockerfile(config: DockerfileConfig): { path: string; signature: string; cleanup: () => void } {
|
|
63
70
|
if (config.dockerfile) {
|
|
64
71
|
const customPath = path.resolve(config.repoRoot, config.dockerfile);
|
|
65
72
|
if (!fs.existsSync(customPath)) {
|
|
@@ -86,7 +93,7 @@ export function prepareDockerfile(config) {
|
|
|
86
93
|
};
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
export function composeDockerfile(config) {
|
|
96
|
+
export function composeDockerfile(config: DockerfileConfig): string {
|
|
90
97
|
const content = dockerfileContent(config);
|
|
91
98
|
|
|
92
99
|
const tempPath = path.join(os.tmpdir(), `${config.project}-sandbox.Dockerfile`);
|
|
@@ -1,13 +1,45 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { hostJoin } from './engines/wsl2-paths.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { hostJoin } from './engines/wsl2-paths.ts';
|
|
4
|
+
|
|
5
|
+
type DotfilesWarning = {
|
|
6
|
+
rel: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
detail?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type DotfilesFs = Pick<typeof fs, 'copyFileSync' | 'existsSync' | 'mkdirSync' | 'readdirSync' | 'realpathSync' | 'rmSync' | 'statSync'>;
|
|
12
|
+
|
|
13
|
+
type MaterializeOptions = {
|
|
14
|
+
writeStderr?: (message: string) => void;
|
|
15
|
+
maxDepth?: number;
|
|
16
|
+
fsModule?: DotfilesFs;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type WalkContext = {
|
|
20
|
+
srcDir: string;
|
|
21
|
+
dstDir: string;
|
|
22
|
+
relParts: string[];
|
|
23
|
+
depth: number;
|
|
24
|
+
maxDepth: number;
|
|
25
|
+
activeDirs: Set<string>;
|
|
26
|
+
warnings: DotfilesWarning[];
|
|
27
|
+
writeStderr: (message: string) => void;
|
|
28
|
+
fsModule: DotfilesFs;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function dotfilesCacheDir(home: string, project: string): string {
|
|
6
32
|
return hostJoin(home, '.agent-infra', '.cache', 'dotfiles-resolved', project);
|
|
7
33
|
}
|
|
8
34
|
|
|
9
|
-
function dotfilesWarning(
|
|
10
|
-
|
|
35
|
+
function dotfilesWarning(
|
|
36
|
+
warnings: DotfilesWarning[],
|
|
37
|
+
writeStderr: (message: string) => void,
|
|
38
|
+
relPath: string,
|
|
39
|
+
reason: string,
|
|
40
|
+
detail = ''
|
|
41
|
+
): void {
|
|
42
|
+
const warning: DotfilesWarning = { rel: relPath, reason };
|
|
11
43
|
if (detail) {
|
|
12
44
|
warning.detail = detail;
|
|
13
45
|
}
|
|
@@ -17,17 +49,31 @@ function dotfilesWarning(warnings, writeStderr, relPath, reason, detail = '') {
|
|
|
17
49
|
writeStderr(`sandbox-dotfiles (host): skipping ${relPath} (${reason}${suffix})\n`);
|
|
18
50
|
}
|
|
19
51
|
|
|
20
|
-
function
|
|
52
|
+
function errorDetail(error: unknown): string {
|
|
53
|
+
return error instanceof Error ? error.message : 'unknown error';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function errorCodeOrDetail(error: unknown): string {
|
|
57
|
+
return typeof error === 'object' && error !== null && 'code' in error
|
|
58
|
+
? String(error.code)
|
|
59
|
+
: errorDetail(error);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function copyDotfile(
|
|
63
|
+
srcPath: string,
|
|
64
|
+
dstPath: string,
|
|
65
|
+
context: Pick<WalkContext, 'fsModule' | 'warnings' | 'writeStderr'> & { relPath: string }
|
|
66
|
+
): void {
|
|
21
67
|
const { fsModule, relPath, warnings, writeStderr } = context;
|
|
22
68
|
try {
|
|
23
69
|
fsModule.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
24
70
|
fsModule.copyFileSync(srcPath, dstPath);
|
|
25
71
|
} catch (error) {
|
|
26
|
-
dotfilesWarning(warnings, writeStderr, relPath, 'copy failed', error
|
|
72
|
+
dotfilesWarning(warnings, writeStderr, relPath, 'copy failed', errorCodeOrDetail(error));
|
|
27
73
|
}
|
|
28
74
|
}
|
|
29
75
|
|
|
30
|
-
function walkAndMaterializeDotfiles(context) {
|
|
76
|
+
function walkAndMaterializeDotfiles(context: WalkContext): void {
|
|
31
77
|
const {
|
|
32
78
|
srcDir,
|
|
33
79
|
dstDir,
|
|
@@ -46,11 +92,11 @@ function walkAndMaterializeDotfiles(context) {
|
|
|
46
92
|
return;
|
|
47
93
|
}
|
|
48
94
|
|
|
49
|
-
let entries;
|
|
95
|
+
let entries: fs.Dirent[];
|
|
50
96
|
try {
|
|
51
97
|
entries = fsModule.readdirSync(srcDir, { withFileTypes: true });
|
|
52
98
|
} catch (error) {
|
|
53
|
-
dotfilesWarning(warnings, writeStderr, relPath, 'read failed', error
|
|
99
|
+
dotfilesWarning(warnings, writeStderr, relPath, 'read failed', errorCodeOrDetail(error));
|
|
54
100
|
return;
|
|
55
101
|
}
|
|
56
102
|
|
|
@@ -61,20 +107,21 @@ function walkAndMaterializeDotfiles(context) {
|
|
|
61
107
|
const childRelPath = childRelParts.join('/');
|
|
62
108
|
|
|
63
109
|
if (entry.isSymbolicLink()) {
|
|
64
|
-
let resolvedTarget;
|
|
110
|
+
let resolvedTarget: string;
|
|
65
111
|
try {
|
|
66
112
|
resolvedTarget = fsModule.realpathSync(childSrc);
|
|
67
113
|
} catch (error) {
|
|
68
|
-
const
|
|
69
|
-
|
|
114
|
+
const code = errorCodeOrDetail(error);
|
|
115
|
+
const reason = code === 'ELOOP' ? 'symlink loop' : 'dangling symlink';
|
|
116
|
+
dotfilesWarning(warnings, writeStderr, childRelPath, reason, code || 'unresolved');
|
|
70
117
|
continue;
|
|
71
118
|
}
|
|
72
119
|
|
|
73
|
-
let targetStat;
|
|
120
|
+
let targetStat: fs.Stats;
|
|
74
121
|
try {
|
|
75
122
|
targetStat = fsModule.statSync(resolvedTarget);
|
|
76
123
|
} catch (error) {
|
|
77
|
-
dotfilesWarning(warnings, writeStderr, childRelPath, 'target stat failed', error
|
|
124
|
+
dotfilesWarning(warnings, writeStderr, childRelPath, 'target stat failed', errorCodeOrDetail(error));
|
|
78
125
|
continue;
|
|
79
126
|
}
|
|
80
127
|
|
|
@@ -112,7 +159,7 @@ function walkAndMaterializeDotfiles(context) {
|
|
|
112
159
|
}
|
|
113
160
|
|
|
114
161
|
if (entry.isDirectory()) {
|
|
115
|
-
let childRealPath = null;
|
|
162
|
+
let childRealPath: string | null = null;
|
|
116
163
|
try {
|
|
117
164
|
childRealPath = fsModule.realpathSync(childSrc);
|
|
118
165
|
} catch {
|
|
@@ -149,7 +196,7 @@ function walkAndMaterializeDotfiles(context) {
|
|
|
149
196
|
}
|
|
150
197
|
}
|
|
151
198
|
|
|
152
|
-
export function materializeDotfiles(srcDir, cacheDir, options = {}) {
|
|
199
|
+
export function materializeDotfiles(srcDir: string, cacheDir: string, options: MaterializeOptions = {}) {
|
|
153
200
|
const {
|
|
154
201
|
writeStderr = (message) => process.stderr.write(message),
|
|
155
202
|
maxDepth = 32,
|
|
@@ -165,8 +212,8 @@ export function materializeDotfiles(srcDir, cacheDir, options = {}) {
|
|
|
165
212
|
fsModule.rmSync(path.join(cacheDir, entry), { recursive: true, force: true });
|
|
166
213
|
}
|
|
167
214
|
|
|
168
|
-
const warnings = [];
|
|
169
|
-
const activeDirs = new Set();
|
|
215
|
+
const warnings: DotfilesWarning[] = [];
|
|
216
|
+
const activeDirs = new Set<string>();
|
|
170
217
|
try {
|
|
171
218
|
activeDirs.add(fsModule.realpathSync(srcDir));
|
|
172
219
|
} catch {
|