@archme/cli 0.13.0 → 0.14.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/dist/cli.js +2567 -1318
- package/package.json +2 -2
- package/sandbox-image.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8291,9 +8291,13 @@ var init_execa = __esm({
|
|
|
8291
8291
|
// src/auth/credentials.ts
|
|
8292
8292
|
var credentials_exports = {};
|
|
8293
8293
|
__export(credentials_exports, {
|
|
8294
|
+
CredentialStoreError: () => CredentialStoreError,
|
|
8295
|
+
CredentialStoreUnreadableError: () => CredentialStoreUnreadableError,
|
|
8296
|
+
CredentialStoreUnwritableError: () => CredentialStoreUnwritableError,
|
|
8294
8297
|
clearCredential: () => clearCredential,
|
|
8295
8298
|
clearCredentials: () => clearCredentials,
|
|
8296
8299
|
credentialKind: () => credentialKind,
|
|
8300
|
+
credentialStoreMessage: () => credentialStoreMessage,
|
|
8297
8301
|
describeCredentialSource: () => describeCredentialSource,
|
|
8298
8302
|
loadAllCredentials: () => loadAllCredentials,
|
|
8299
8303
|
loadCredentials: () => loadCredentials,
|
|
@@ -8344,10 +8348,32 @@ function parseStore(raw) {
|
|
|
8344
8348
|
function serializeStore(identities) {
|
|
8345
8349
|
return JSON.stringify({ v: 2, identities }, null, 2);
|
|
8346
8350
|
}
|
|
8351
|
+
function credentialStoreMessage(platform3, detail, action = "read") {
|
|
8352
|
+
const lead = `Could not ${action} your ArchMe sign-in`;
|
|
8353
|
+
if (platform3 === "darwin") {
|
|
8354
|
+
return `${lead}: the macOS login keychain is locked (common over SSH). Run \`security unlock-keychain ~/Library/Keychains/login.keychain-db\`, or run this from the Mac's desktop session. (keytar: ${detail})`;
|
|
8355
|
+
}
|
|
8356
|
+
if (platform3 === "linux") {
|
|
8357
|
+
return `${lead}: the system keyring (Secret Service) is locked or unavailable (common over SSH): ${detail}. Unlock the keyring, or run this from your desktop session.`;
|
|
8358
|
+
}
|
|
8359
|
+
return `${lead} from the OS credential store: ${detail}`;
|
|
8360
|
+
}
|
|
8361
|
+
async function keychainWrite(action, op) {
|
|
8362
|
+
try {
|
|
8363
|
+
return await op();
|
|
8364
|
+
} catch (err4) {
|
|
8365
|
+
throw new CredentialStoreUnwritableError(action, err4);
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8347
8368
|
async function readStore(supabaseUrl) {
|
|
8348
8369
|
const keytar = await loadKeytar();
|
|
8349
8370
|
if (keytar) {
|
|
8350
|
-
|
|
8371
|
+
let raw;
|
|
8372
|
+
try {
|
|
8373
|
+
raw = await keytar.getPassword(SERVICE_NAME, supabaseUrl);
|
|
8374
|
+
} catch (err4) {
|
|
8375
|
+
throw new CredentialStoreUnreadableError(err4);
|
|
8376
|
+
}
|
|
8351
8377
|
return { list: raw ? parseStore(raw).filter((c3) => c3.supabase_url === supabaseUrl) : [], source: "keychain" };
|
|
8352
8378
|
}
|
|
8353
8379
|
const all = await readFileStore();
|
|
@@ -8383,12 +8409,7 @@ async function saveCredentials(credentials) {
|
|
|
8383
8409
|
const merged2 = mergeBindings(credentials, prior2);
|
|
8384
8410
|
const next2 = list2.filter((c3) => c3.device_id !== credentials.device_id);
|
|
8385
8411
|
next2.push(merged2);
|
|
8386
|
-
|
|
8387
|
-
await keytar.setPassword(SERVICE_NAME, credentials.supabase_url, serializeStore(next2));
|
|
8388
|
-
} catch (err4) {
|
|
8389
|
-
warnCredentialPersistFailure("macOS Keychain", err4);
|
|
8390
|
-
throw err4;
|
|
8391
|
-
}
|
|
8412
|
+
await keychainWrite("save", () => keytar.setPassword(SERVICE_NAME, credentials.supabase_url, serializeStore(next2)));
|
|
8392
8413
|
return { credentials: merged2, source: "keychain" };
|
|
8393
8414
|
}
|
|
8394
8415
|
const all = await readFileStore();
|
|
@@ -8411,10 +8432,10 @@ async function replaceUrlCredentials(supabaseUrl, creds) {
|
|
|
8411
8432
|
const keytar = await loadKeytar();
|
|
8412
8433
|
if (keytar) {
|
|
8413
8434
|
if (creds.length === 0) {
|
|
8414
|
-
await keytar.deletePassword(SERVICE_NAME, supabaseUrl);
|
|
8435
|
+
await keychainWrite("remove", () => keytar.deletePassword(SERVICE_NAME, supabaseUrl));
|
|
8415
8436
|
return { credentials: null, source: "keychain" };
|
|
8416
8437
|
}
|
|
8417
|
-
await keytar.setPassword(SERVICE_NAME, supabaseUrl, serializeStore(creds));
|
|
8438
|
+
await keychainWrite("save", () => keytar.setPassword(SERVICE_NAME, supabaseUrl, serializeStore(creds)));
|
|
8418
8439
|
return { credentials: creds[creds.length - 1], source: "keychain" };
|
|
8419
8440
|
}
|
|
8420
8441
|
const all = await readFileStore();
|
|
@@ -8437,10 +8458,10 @@ async function removeFromStore(supabaseUrl, match2) {
|
|
|
8437
8458
|
const { list: list2 } = await readStore(supabaseUrl);
|
|
8438
8459
|
const remaining = list2.filter((c3) => !match2(c3));
|
|
8439
8460
|
if (remaining.length === 0) {
|
|
8440
|
-
await keytar.deletePassword(SERVICE_NAME, supabaseUrl);
|
|
8461
|
+
await keychainWrite("remove", () => keytar.deletePassword(SERVICE_NAME, supabaseUrl));
|
|
8441
8462
|
return;
|
|
8442
8463
|
}
|
|
8443
|
-
await keytar.setPassword(SERVICE_NAME, supabaseUrl, serializeStore(remaining));
|
|
8464
|
+
await keychainWrite("remove", () => keytar.setPassword(SERVICE_NAME, supabaseUrl, serializeStore(remaining)));
|
|
8444
8465
|
return;
|
|
8445
8466
|
}
|
|
8446
8467
|
const all = await readFileStore();
|
|
@@ -8480,12 +8501,31 @@ async function loadKeytar() {
|
|
|
8480
8501
|
return null;
|
|
8481
8502
|
}
|
|
8482
8503
|
}
|
|
8483
|
-
var SERVICE_NAME, storeMutex;
|
|
8504
|
+
var SERVICE_NAME, storeMutex, CredentialStoreError, CredentialStoreUnreadableError, CredentialStoreUnwritableError;
|
|
8484
8505
|
var init_credentials = __esm({
|
|
8485
8506
|
"src/auth/credentials.ts"() {
|
|
8486
8507
|
"use strict";
|
|
8487
8508
|
SERVICE_NAME = "archme-daemon";
|
|
8488
8509
|
storeMutex = Promise.resolve();
|
|
8510
|
+
CredentialStoreError = class extends Error {
|
|
8511
|
+
constructor(action, cause, platform3 = process.platform) {
|
|
8512
|
+
super(credentialStoreMessage(platform3, cause instanceof Error ? cause.message : String(cause), action), { cause });
|
|
8513
|
+
this.action = action;
|
|
8514
|
+
this.name = "CredentialStoreError";
|
|
8515
|
+
}
|
|
8516
|
+
};
|
|
8517
|
+
CredentialStoreUnreadableError = class extends CredentialStoreError {
|
|
8518
|
+
constructor(cause, platform3 = process.platform) {
|
|
8519
|
+
super("read", cause, platform3);
|
|
8520
|
+
this.name = "CredentialStoreUnreadableError";
|
|
8521
|
+
}
|
|
8522
|
+
};
|
|
8523
|
+
CredentialStoreUnwritableError = class extends CredentialStoreError {
|
|
8524
|
+
constructor(action, cause, platform3 = process.platform) {
|
|
8525
|
+
super(action, cause, platform3);
|
|
8526
|
+
this.name = "CredentialStoreUnwritableError";
|
|
8527
|
+
}
|
|
8528
|
+
};
|
|
8489
8529
|
}
|
|
8490
8530
|
});
|
|
8491
8531
|
|
|
@@ -9294,7 +9334,14 @@ async function mintKeyedAccount(deps) {
|
|
|
9294
9334
|
`Created ${parsed.handle}${saved ? " and saved its key" : ""}, but signing in failed: ${grant.error}. ` + (saved ? "Run `archme login --key` to try again." : `Try again with the key printed above: ${KEY_VIA_ENV}.`)
|
|
9295
9335
|
);
|
|
9296
9336
|
}
|
|
9297
|
-
|
|
9337
|
+
try {
|
|
9338
|
+
await storeKeyedLogin(deps, grant, data);
|
|
9339
|
+
} catch (err4) {
|
|
9340
|
+
if (!(err4 instanceof CredentialStoreError)) throw err4;
|
|
9341
|
+
throw new LoginError(
|
|
9342
|
+
`${err4.message} Your account ${parsed.handle} was created${saved ? " and its key saved" : ""}; once that's fixed, sign in with it: ${saved ? "`archme login --key`" : KEY_VIA_ENV}.`
|
|
9343
|
+
);
|
|
9344
|
+
}
|
|
9298
9345
|
deps.log(`\u2713 Created your ArchMe account ${parsed.handle} \u2014 no email or password, just a key.`);
|
|
9299
9346
|
if (saved) {
|
|
9300
9347
|
deps.log(` The key is saved in ${deps.keyRingPath}. Save it in your password manager too: it is the`);
|
|
@@ -9895,7 +9942,17 @@ async function ensureStartPrerequisites(opts = {}) {
|
|
|
9895
9942
|
`archme daemon service commands are not supported on ${platform3}. Use \`archme daemon run\` for foreground operation.`
|
|
9896
9943
|
);
|
|
9897
9944
|
}
|
|
9898
|
-
|
|
9945
|
+
let signedIn;
|
|
9946
|
+
try {
|
|
9947
|
+
signedIn = opts.isSignedIn ? await opts.isSignedIn() : await defaultIsSignedIn(env);
|
|
9948
|
+
} catch (err4) {
|
|
9949
|
+
if (!(err4 instanceof CredentialStoreUnreadableError)) throw err4;
|
|
9950
|
+
const controller = opts.controller ?? pickController(platform3, env);
|
|
9951
|
+
if (!await controller.unitExists()) throw err4;
|
|
9952
|
+
console.error(`Warning: ${err4.message}
|
|
9953
|
+
Starting the installed service anyway; it reads its own sign-in when it boots.`);
|
|
9954
|
+
return;
|
|
9955
|
+
}
|
|
9899
9956
|
if (!signedIn) {
|
|
9900
9957
|
if (!opts.noLogin && isatty(0) && isatty(1)) {
|
|
9901
9958
|
out.write("Not signed in \u2014 launching `archme login`\u2026\n");
|
|
@@ -9920,7 +9977,7 @@ async function runStartFlow(opts = {}) {
|
|
|
9920
9977
|
stderrPath: logPaths.stderr
|
|
9921
9978
|
};
|
|
9922
9979
|
const unitContent = platform3 === "darwin" ? renderLaunchdPlist(renderOpts) : renderSystemdUnit(renderOpts);
|
|
9923
|
-
const controller = pickController(platform3, env);
|
|
9980
|
+
const controller = opts.controller ?? pickController(platform3, env);
|
|
9924
9981
|
const result = await orchestrateStart(controller, unitContent, {
|
|
9925
9982
|
skipStart: opts.start === false
|
|
9926
9983
|
});
|
|
@@ -9946,6 +10003,7 @@ var init_install = __esm({
|
|
|
9946
10003
|
init_paths();
|
|
9947
10004
|
init_unitFiles();
|
|
9948
10005
|
init_serviceController();
|
|
10006
|
+
init_credentials();
|
|
9949
10007
|
ENV_KEYS_TO_BAKE = [
|
|
9950
10008
|
"PATH",
|
|
9951
10009
|
"HOME",
|
|
@@ -10102,10 +10160,15 @@ var init_logs = __esm({
|
|
|
10102
10160
|
import { readFile as readFile5, stat as stat2 } from "node:fs/promises";
|
|
10103
10161
|
function renderStatus(s) {
|
|
10104
10162
|
const lines = [];
|
|
10163
|
+
const unreadable = s.credentials && "unreadable" in s.credentials ? s.credentials.unreadable : null;
|
|
10164
|
+
const early = (exitCode) => {
|
|
10165
|
+
if (unreadable) lines.push(`Signed in: unknown \u2014 ${unreadable}`);
|
|
10166
|
+
return { exitCode, rendered: lines.join("\n") + "\n" };
|
|
10167
|
+
};
|
|
10105
10168
|
if (!s.unitInstalled) {
|
|
10106
10169
|
lines.push(`Service: ${LABEL} (not installed)`);
|
|
10107
10170
|
lines.push("Run `archme daemon start` to install + start.");
|
|
10108
|
-
return
|
|
10171
|
+
return early(1);
|
|
10109
10172
|
}
|
|
10110
10173
|
const serviceStateBits = ["installed"];
|
|
10111
10174
|
if (s.inspection.enabled) serviceStateBits.push("enabled at login");
|
|
@@ -10113,17 +10176,19 @@ function renderStatus(s) {
|
|
|
10113
10176
|
lines.push(`Service: ${LABEL} (${serviceStateBits.join(", ")})`);
|
|
10114
10177
|
if (!s.inspection.loaded) {
|
|
10115
10178
|
lines.push("Process: installed, not running \u2014 run `archme daemon start` to load + start the service.");
|
|
10116
|
-
return
|
|
10179
|
+
return early(2);
|
|
10117
10180
|
}
|
|
10118
10181
|
if (!s.inspection.running) {
|
|
10119
10182
|
lines.push("Process: service loaded but no process");
|
|
10120
10183
|
lines.push("Check `archme daemon logs` for crash details.");
|
|
10121
|
-
return
|
|
10184
|
+
return early(2);
|
|
10122
10185
|
}
|
|
10123
10186
|
const uptime = s.uptimeMs != null ? `, uptime ${formatDuration(s.uptimeMs)}` : "";
|
|
10124
10187
|
lines.push(`Process: running, pid ${s.inspection.pid}${uptime}`);
|
|
10125
10188
|
let unhealthy = false;
|
|
10126
|
-
if (
|
|
10189
|
+
if (unreadable) {
|
|
10190
|
+
lines.push(`Signed in: unknown \u2014 ${unreadable}`);
|
|
10191
|
+
} else if (s.credentials && "deviceId" in s.credentials) {
|
|
10127
10192
|
const dev = s.credentials.deviceId ?? "unknown";
|
|
10128
10193
|
const who = s.credentials.email ?? "OAuth user";
|
|
10129
10194
|
lines.push(`Signed in: ${who} device ${dev}`);
|
|
@@ -10193,7 +10258,8 @@ async function gatherStatus(d) {
|
|
|
10193
10258
|
let supabase = null;
|
|
10194
10259
|
if (inspection.running) {
|
|
10195
10260
|
try {
|
|
10196
|
-
|
|
10261
|
+
const deviceId = credentials && "deviceId" in credentials ? credentials.deviceId : null;
|
|
10262
|
+
supabase = await d.loadSupabaseState(deviceId);
|
|
10197
10263
|
} catch {
|
|
10198
10264
|
supabase = { reachable: false, inFlightJob: null };
|
|
10199
10265
|
}
|
|
@@ -16391,15 +16457,23 @@ var init_sessionAuth = __esm({
|
|
|
16391
16457
|
this.userId = resolveUserId(userInfo2, ctx.fallbackUserId);
|
|
16392
16458
|
this.lastUserInfo = userInfo2;
|
|
16393
16459
|
}
|
|
16394
|
-
|
|
16395
|
-
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16460
|
+
let saved = null;
|
|
16461
|
+
try {
|
|
16462
|
+
saved = await saveCredentials({
|
|
16463
|
+
kind: this.kind,
|
|
16464
|
+
refresh_token: nextRefreshToken,
|
|
16465
|
+
access_token: this.accessToken,
|
|
16466
|
+
org_id: ctx.orgId,
|
|
16467
|
+
device_id: ctx.deviceId,
|
|
16468
|
+
user_id: this.userId ?? void 0,
|
|
16469
|
+
supabase_url: this.cfg.supabaseUrl
|
|
16470
|
+
});
|
|
16471
|
+
} catch (err4) {
|
|
16472
|
+
if (!(err4 instanceof CredentialStoreError)) throw err4;
|
|
16473
|
+
console.error(
|
|
16474
|
+
`auth: ${err4.message} The refreshed sign-in was NOT saved: this process keeps working, but the saved one is now out of date. If this process ends before a later refresh saves it, sign in again then. ${this.reloginHint()}`
|
|
16475
|
+
);
|
|
16476
|
+
}
|
|
16403
16477
|
return { userInfo: userInfo2, saved, nextRefreshToken };
|
|
16404
16478
|
}
|
|
16405
16479
|
// The timer closure reads the LIVE this.refreshToken/deviceId/orgId at fire
|
|
@@ -16492,11 +16566,11 @@ var init_sessionAuth = __esm({
|
|
|
16492
16566
|
const refreshed = await this.refreshOrRecover(refreshToken, this.userId);
|
|
16493
16567
|
if (!refreshed.ok) {
|
|
16494
16568
|
if (refreshed.permanent) {
|
|
16495
|
-
await
|
|
16569
|
+
const notCleared = await this.clearStoredCredential(deviceId);
|
|
16496
16570
|
this.clearTokensOnly();
|
|
16497
16571
|
throw new LoadStoredAndRefreshError(
|
|
16498
16572
|
"invalid_grant",
|
|
16499
|
-
`${errorPrefix} failed permanently: ${refreshed.error}. ${this.reloginHint()}`
|
|
16573
|
+
`${errorPrefix} failed permanently: ${refreshed.error}. ${this.reloginHint()}${notCleared}`
|
|
16500
16574
|
);
|
|
16501
16575
|
}
|
|
16502
16576
|
throw new LoadStoredAndRefreshError("transient", `${errorPrefix} failed: ${refreshed.error}`);
|
|
@@ -16507,7 +16581,7 @@ var init_sessionAuth = __esm({
|
|
|
16507
16581
|
orgId,
|
|
16508
16582
|
fallbackUserId: this.userId ?? void 0
|
|
16509
16583
|
});
|
|
16510
|
-
console.log(`auth: persisted refreshed token to ${describeCredentialSource(saved)}`);
|
|
16584
|
+
if (saved) console.log(`auth: persisted refreshed token to ${describeCredentialSource(saved)}`);
|
|
16511
16585
|
})();
|
|
16512
16586
|
this.refreshInFlight = p;
|
|
16513
16587
|
return p.finally(() => {
|
|
@@ -16520,6 +16594,18 @@ var init_sessionAuth = __esm({
|
|
|
16520
16594
|
if (refreshed.ok || !refreshed.permanent) return refreshed;
|
|
16521
16595
|
return this.signInWithSavedKey(userId, refreshed.error);
|
|
16522
16596
|
}
|
|
16597
|
+
// Remove a dead login from the store. The token is revoked whether or not
|
|
16598
|
+
// this works, so a store that refuses the delete must not turn a permanent
|
|
16599
|
+
// failure into a transient one (which would retry a dead token forever).
|
|
16600
|
+
async clearStoredCredential(deviceId) {
|
|
16601
|
+
try {
|
|
16602
|
+
await clearCredential(this.cfg.supabaseUrl, deviceId);
|
|
16603
|
+
return "";
|
|
16604
|
+
} catch (err4) {
|
|
16605
|
+
if (!(err4 instanceof CredentialStoreError)) throw err4;
|
|
16606
|
+
return ` (${err4.message})`;
|
|
16607
|
+
}
|
|
16608
|
+
}
|
|
16523
16609
|
reloginHint() {
|
|
16524
16610
|
return this.kind === "key" ? "Run `archme login --key` to sign in with your key again." : "Run `archme login` to pair again.";
|
|
16525
16611
|
}
|
|
@@ -16564,10 +16650,10 @@ var init_sessionAuth = __esm({
|
|
|
16564
16650
|
const refreshed = await this.refreshOrRecover(stored.refresh_token, stored.user_id ?? null);
|
|
16565
16651
|
if (!refreshed.ok) {
|
|
16566
16652
|
if (refreshed.permanent) {
|
|
16567
|
-
await
|
|
16653
|
+
const notCleared = await this.clearStoredCredential(stored.device_id);
|
|
16568
16654
|
throw new LoadStoredAndRefreshError(
|
|
16569
16655
|
"invalid_grant",
|
|
16570
|
-
`stored archme credentials are invalid or revoked: ${refreshed.error}. ${this.reloginHint()}`
|
|
16656
|
+
`stored archme credentials are invalid or revoked: ${refreshed.error}. ${this.reloginHint()}${notCleared}`
|
|
16571
16657
|
);
|
|
16572
16658
|
}
|
|
16573
16659
|
throw new LoadStoredAndRefreshError("transient", `token refresh failed (transient): ${refreshed.error}`);
|
|
@@ -16941,12 +17027,12 @@ function defaultReadFile(path7) {
|
|
|
16941
17027
|
return readFileSync4(path7, "utf8");
|
|
16942
17028
|
}
|
|
16943
17029
|
function findPackageRoot2(startDir, opts = {}) {
|
|
16944
|
-
const
|
|
17030
|
+
const readFile20 = opts.readFile ?? defaultReadFile;
|
|
16945
17031
|
const stopAt = parse(startDir).root;
|
|
16946
17032
|
let dir = startDir;
|
|
16947
17033
|
for (; ; ) {
|
|
16948
17034
|
try {
|
|
16949
|
-
const parsed = JSON.parse(
|
|
17035
|
+
const parsed = JSON.parse(readFile20(join16(dir, "package.json")));
|
|
16950
17036
|
if (parsed.name === PACKAGE_NAME) return dir;
|
|
16951
17037
|
} catch {
|
|
16952
17038
|
}
|
|
@@ -18343,6 +18429,26 @@ function expandHome(p, home2) {
|
|
|
18343
18429
|
if (p.startsWith("~/")) return join20(home2, p.slice(2));
|
|
18344
18430
|
return p;
|
|
18345
18431
|
}
|
|
18432
|
+
function sandboxTokenLocation(env, home2 = homedir6()) {
|
|
18433
|
+
const raw = (env.ARCHME_SANDBOX_CLAUDE_TOKEN_FILE ?? "").trim();
|
|
18434
|
+
return raw ? { path: expandHome(raw, home2), override: true } : { path: join20(home2, ".archme", "sandbox-claude-token"), override: false };
|
|
18435
|
+
}
|
|
18436
|
+
function resolveSandboxImage(env, shippedImage) {
|
|
18437
|
+
let image = (env.ARCHME_SANDBOX_IMAGE ?? "").trim();
|
|
18438
|
+
if (!image) {
|
|
18439
|
+
try {
|
|
18440
|
+
image = shippedImage() ?? "";
|
|
18441
|
+
} catch (e) {
|
|
18442
|
+
throw new SandboxSettingsError(`the shipped sandbox image is unusable: ${e.message}`);
|
|
18443
|
+
}
|
|
18444
|
+
}
|
|
18445
|
+
if (!image) {
|
|
18446
|
+
throw new SandboxSettingsError(
|
|
18447
|
+
"no sandbox image: this install ships no sandbox-image.json, so set ARCHME_SANDBOX_IMAGE"
|
|
18448
|
+
);
|
|
18449
|
+
}
|
|
18450
|
+
return image;
|
|
18451
|
+
}
|
|
18346
18452
|
function readSandboxSettings(env, concurrency, home2 = homedir6(), shippedImage = () => null) {
|
|
18347
18453
|
const modeRaw = (env.ARCHME_SANDBOX ?? "").trim().toLowerCase() || "off";
|
|
18348
18454
|
if (modeRaw !== "off" && modeRaw !== "shadow" && modeRaw !== "required") {
|
|
@@ -18358,14 +18464,10 @@ function readSandboxSettings(env, concurrency, home2 = homedir6(), shippedImage
|
|
|
18358
18464
|
boxCpus: SANDBOX_DEFAULTS.boxCpus,
|
|
18359
18465
|
volumeGiB: SANDBOX_DEFAULTS.volumeGiB,
|
|
18360
18466
|
maxModelRequests: SANDBOX_DEFAULTS.maxModelRequests,
|
|
18361
|
-
claudeTokenPath:
|
|
18467
|
+
claudeTokenPath: sandboxTokenLocation({}, home2).path,
|
|
18468
|
+
claudeTokenOverride: false
|
|
18362
18469
|
};
|
|
18363
18470
|
if (mode === "off") return { mode, ...base };
|
|
18364
|
-
if (mode === "required") {
|
|
18365
|
-
throw new SandboxSettingsError(
|
|
18366
|
-
"ARCHME_SANDBOX=required needs network confinement (pf + the GitHub gatekeeper, step 19.3), which this release does not have; use ARCHME_SANDBOX=shadow"
|
|
18367
|
-
);
|
|
18368
|
-
}
|
|
18369
18471
|
if (!Number.isInteger(concurrency)) {
|
|
18370
18472
|
throw new SandboxSettingsError(`MAX_CONCURRENT_JOBS must be a whole number (got ${concurrency})`);
|
|
18371
18473
|
}
|
|
@@ -18378,23 +18480,11 @@ function readSandboxSettings(env, concurrency, home2 = homedir6(), shippedImage
|
|
|
18378
18480
|
if (runtime !== "apple-container") {
|
|
18379
18481
|
throw new SandboxSettingsError(`ARCHME_SANDBOX_RUNTIME must be apple-container (got "${runtime}")`);
|
|
18380
18482
|
}
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
try {
|
|
18384
|
-
image = shippedImage() ?? "";
|
|
18385
|
-
} catch (e) {
|
|
18386
|
-
throw new SandboxSettingsError(`the shipped sandbox image is unusable: ${e.message}`);
|
|
18387
|
-
}
|
|
18388
|
-
}
|
|
18389
|
-
if (!image) {
|
|
18390
|
-
throw new SandboxSettingsError(
|
|
18391
|
-
"no sandbox image: this install ships no sandbox-image.json, so set ARCHME_SANDBOX_IMAGE"
|
|
18392
|
-
);
|
|
18393
|
-
}
|
|
18394
|
-
const projects = new Set(
|
|
18483
|
+
const image = resolveSandboxImage(env, shippedImage);
|
|
18484
|
+
const projects = mode === "required" ? /* @__PURE__ */ new Set() : new Set(
|
|
18395
18485
|
(env.ARCHME_SANDBOX_PROJECTS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
18396
18486
|
);
|
|
18397
|
-
const
|
|
18487
|
+
const token = sandboxTokenLocation(env, home2);
|
|
18398
18488
|
return {
|
|
18399
18489
|
mode,
|
|
18400
18490
|
projects,
|
|
@@ -18407,7 +18497,8 @@ function readSandboxSettings(env, concurrency, home2 = homedir6(), shippedImage
|
|
|
18407
18497
|
boxCpus: SANDBOX_DEFAULTS.boxCpus,
|
|
18408
18498
|
volumeGiB: intEnv(env, "ARCHME_SANDBOX_VOLUME_GIB", SANDBOX_DEFAULTS.volumeGiB, { min: 1 }),
|
|
18409
18499
|
maxModelRequests: intEnv(env, "ARCHME_SANDBOX_MAX_MODEL_REQUESTS", SANDBOX_DEFAULTS.maxModelRequests, { min: 1 }),
|
|
18410
|
-
claudeTokenPath:
|
|
18500
|
+
claudeTokenPath: token.path,
|
|
18501
|
+
claudeTokenOverride: token.override
|
|
18411
18502
|
};
|
|
18412
18503
|
}
|
|
18413
18504
|
function shouldSandbox(settings, job, { isLocalRepo }) {
|
|
@@ -18483,6 +18574,132 @@ var init_imageManifest = __esm({
|
|
|
18483
18574
|
}
|
|
18484
18575
|
});
|
|
18485
18576
|
|
|
18577
|
+
// src/daemon/sandboxJob/result.ts
|
|
18578
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
18579
|
+
function isValidNonce(nonce) {
|
|
18580
|
+
return typeof nonce === "string" && RESULT_NONCE_RE.test(nonce);
|
|
18581
|
+
}
|
|
18582
|
+
function encode(result, nonce) {
|
|
18583
|
+
return `${RESULT_PREFIX}${nonce} ${Buffer.from(JSON.stringify(result), "utf8").toString("base64")}`;
|
|
18584
|
+
}
|
|
18585
|
+
function fits(line, cap) {
|
|
18586
|
+
return Buffer.byteLength(line, "utf8") <= cap;
|
|
18587
|
+
}
|
|
18588
|
+
function withTruncatedError(r) {
|
|
18589
|
+
return { ...r, error: r.error ? `${r.error}; ${RESULT_TRUNCATED}` : RESULT_TRUNCATED };
|
|
18590
|
+
}
|
|
18591
|
+
function minimal(r) {
|
|
18592
|
+
if (r.kind === "probe") {
|
|
18593
|
+
const m2 = { kind: "probe", outcomes: {}, memoryPeakBytes: r.memoryPeakBytes, durations_ms: {}, error: RESULT_TRUNCATED };
|
|
18594
|
+
return m2;
|
|
18595
|
+
}
|
|
18596
|
+
if (r.kind === "clone") {
|
|
18597
|
+
const m2 = { kind: "clone", outcome: r.outcome, memoryPeakBytes: r.memoryPeakBytes, durations_ms: {}, error: RESULT_TRUNCATED };
|
|
18598
|
+
return m2;
|
|
18599
|
+
}
|
|
18600
|
+
const m = {
|
|
18601
|
+
kind: "agent",
|
|
18602
|
+
exitCode: r.exitCode,
|
|
18603
|
+
signal: r.signal,
|
|
18604
|
+
claudeStdout: "",
|
|
18605
|
+
affected: null,
|
|
18606
|
+
promptBytes: r.promptBytes,
|
|
18607
|
+
memoryPeakBytes: r.memoryPeakBytes,
|
|
18608
|
+
durations_ms: {},
|
|
18609
|
+
error: RESULT_TRUNCATED
|
|
18610
|
+
};
|
|
18611
|
+
return m;
|
|
18612
|
+
}
|
|
18613
|
+
function formatResultLine(result, nonce, cap = RESULT_MAX_BYTES) {
|
|
18614
|
+
if (!isValidNonce(nonce)) throw new Error("result: invalid nonce");
|
|
18615
|
+
const encode_ = (r) => encode(r, nonce);
|
|
18616
|
+
let line = encode_(result);
|
|
18617
|
+
if (fits(line, cap)) return line;
|
|
18618
|
+
if (result.kind === "agent") {
|
|
18619
|
+
const noAffected = { ...result, affected: null };
|
|
18620
|
+
line = encode_(withTruncatedError(noAffected));
|
|
18621
|
+
if (fits(line, cap)) return line;
|
|
18622
|
+
line = encode_(withTruncatedError({ ...noAffected, claudeStdout: "" }));
|
|
18623
|
+
if (fits(line, cap)) return line;
|
|
18624
|
+
}
|
|
18625
|
+
return encode_(minimal(result));
|
|
18626
|
+
}
|
|
18627
|
+
function isObj(v) {
|
|
18628
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
18629
|
+
}
|
|
18630
|
+
function isNumOrNull(v) {
|
|
18631
|
+
return v === null || typeof v === "number" && Number.isFinite(v);
|
|
18632
|
+
}
|
|
18633
|
+
function shapeError(o2) {
|
|
18634
|
+
if (!isNumOrNull(o2.memoryPeakBytes)) return "memoryPeakBytes";
|
|
18635
|
+
if (!isObj(o2.durations_ms) || !Object.values(o2.durations_ms).every((v) => typeof v === "number")) return "durations_ms";
|
|
18636
|
+
if (o2.error !== void 0 && typeof o2.error !== "string") return "error";
|
|
18637
|
+
if (o2.kind === "clone") {
|
|
18638
|
+
if (o2.outcome !== "diff" && o2.outcome !== "skip" && o2.outcome !== "rejected") return "outcome";
|
|
18639
|
+
if (o2.diff !== void 0 && !isObj(o2.diff)) return "diff";
|
|
18640
|
+
return null;
|
|
18641
|
+
}
|
|
18642
|
+
if (o2.kind === "probe") {
|
|
18643
|
+
if (!isObj(o2.outcomes) || !Object.values(o2.outcomes).every((v) => typeof v === "string")) return "outcomes";
|
|
18644
|
+
return null;
|
|
18645
|
+
}
|
|
18646
|
+
if (o2.kind === "agent") {
|
|
18647
|
+
if (!isNumOrNull(o2.exitCode)) return "exitCode";
|
|
18648
|
+
if (o2.signal !== null && typeof o2.signal !== "string") return "signal";
|
|
18649
|
+
if (typeof o2.claudeStdout !== "string") return "claudeStdout";
|
|
18650
|
+
if (o2.affected !== null && !(isObj(o2.affected) && ["stories", "components", "unresolvedFiles"].every((k) => Array.isArray(o2.affected[k])))) {
|
|
18651
|
+
return "affected";
|
|
18652
|
+
}
|
|
18653
|
+
if (typeof o2.promptBytes !== "number") return "promptBytes";
|
|
18654
|
+
return null;
|
|
18655
|
+
}
|
|
18656
|
+
return "kind";
|
|
18657
|
+
}
|
|
18658
|
+
function parseResultLine(stdout, nonce, cap = RESULT_MAX_BYTES) {
|
|
18659
|
+
if (!isValidNonce(nonce)) return { ok: false, reason: "no result line" };
|
|
18660
|
+
const prefix = `${RESULT_PREFIX}${nonce} `;
|
|
18661
|
+
const lines = stdout.split("\n");
|
|
18662
|
+
let line;
|
|
18663
|
+
for (let i2 = lines.length - 1; i2 >= 0; i2--) {
|
|
18664
|
+
const l = lines[i2].replace(/\r$/, "");
|
|
18665
|
+
if (l.startsWith(prefix)) {
|
|
18666
|
+
line = l;
|
|
18667
|
+
break;
|
|
18668
|
+
}
|
|
18669
|
+
}
|
|
18670
|
+
if (line === void 0) return { ok: false, reason: "no result line" };
|
|
18671
|
+
if (Buffer.byteLength(line, "utf8") > cap) return { ok: false, reason: "result line exceeds cap" };
|
|
18672
|
+
const b64 = line.slice(prefix.length).trim();
|
|
18673
|
+
if (b64 === "" || b64.length % 4 !== 0 || !BASE64_RE.test(b64)) return { ok: false, reason: "result is not base64" };
|
|
18674
|
+
let data;
|
|
18675
|
+
try {
|
|
18676
|
+
data = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
|
|
18677
|
+
} catch {
|
|
18678
|
+
return { ok: false, reason: "result is not JSON" };
|
|
18679
|
+
}
|
|
18680
|
+
if (!isObj(data)) return { ok: false, reason: "result is not an object" };
|
|
18681
|
+
const bad = shapeError(data);
|
|
18682
|
+
if (bad) return { ok: false, reason: `result has a malformed ${bad}` };
|
|
18683
|
+
return { ok: true, result: data };
|
|
18684
|
+
}
|
|
18685
|
+
async function readMemoryPeak(path7 = "/sys/fs/cgroup/memory.peak", read = (p) => readFile13(p, "utf8")) {
|
|
18686
|
+
try {
|
|
18687
|
+
const n2 = Number.parseInt((await read(path7)).trim(), 10);
|
|
18688
|
+
return Number.isFinite(n2) && n2 >= 0 ? n2 : null;
|
|
18689
|
+
} catch {
|
|
18690
|
+
return null;
|
|
18691
|
+
}
|
|
18692
|
+
}
|
|
18693
|
+
var RESULT_TRUNCATED, BASE64_RE;
|
|
18694
|
+
var init_result2 = __esm({
|
|
18695
|
+
"src/daemon/sandboxJob/result.ts"() {
|
|
18696
|
+
"use strict";
|
|
18697
|
+
init_types();
|
|
18698
|
+
RESULT_TRUNCATED = "result truncated";
|
|
18699
|
+
BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
18700
|
+
}
|
|
18701
|
+
});
|
|
18702
|
+
|
|
18486
18703
|
// src/daemon/sandbox/inputDir.ts
|
|
18487
18704
|
import { mkdtemp as mkdtemp3, readdir as readdir2, rm as rm7, writeFile as writeFile10, chmod as chmod4 } from "node:fs/promises";
|
|
18488
18705
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
@@ -18514,893 +18731,1272 @@ var init_inputDir = __esm({
|
|
|
18514
18731
|
}
|
|
18515
18732
|
});
|
|
18516
18733
|
|
|
18517
|
-
// src/daemon/sandbox/
|
|
18518
|
-
|
|
18519
|
-
|
|
18520
|
-
|
|
18521
|
-
|
|
18522
|
-
|
|
18523
|
-
|
|
18524
|
-
|
|
18525
|
-
|
|
18526
|
-
|
|
18527
|
-
|
|
18528
|
-
|
|
18529
|
-
|
|
18530
|
-
|
|
18531
|
-
|
|
18532
|
-
}
|
|
18533
|
-
try {
|
|
18534
|
-
const st = await fh.stat();
|
|
18535
|
-
if (!st.isFile()) throw new SandboxBootError(`sandbox Claude token ${path7} is not a regular file`);
|
|
18536
|
-
if ((st.mode & 63) !== 0) {
|
|
18537
|
-
throw new SandboxBootError(
|
|
18538
|
-
`sandbox Claude token ${path7} is readable by group or others (mode ${(st.mode & 511).toString(8)}); chmod 600 it`
|
|
18539
|
-
);
|
|
18734
|
+
// src/daemon/sandbox/sweep.ts
|
|
18735
|
+
async function sweepJobs(deps) {
|
|
18736
|
+
const now = deps.now ?? Date.now;
|
|
18737
|
+
const t0 = now();
|
|
18738
|
+
const failures = [];
|
|
18739
|
+
const boxes = [];
|
|
18740
|
+
const volumes = [];
|
|
18741
|
+
const jobBoxes = (await deps.list()).filter(isJobBox);
|
|
18742
|
+
await Promise.all(jobBoxes.map(async (b) => {
|
|
18743
|
+
try {
|
|
18744
|
+
await deps.kill(b.id).catch(() => void 0);
|
|
18745
|
+
await deps.remove(b.id);
|
|
18746
|
+
boxes.push(b.id);
|
|
18747
|
+
} catch (e) {
|
|
18748
|
+
failures.push(`box ${b.id}: ${e.message}`);
|
|
18540
18749
|
}
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
);
|
|
18750
|
+
}));
|
|
18751
|
+
const jobVolumes = (await deps.listVolumes()).filter(isJobVolume);
|
|
18752
|
+
for (const v of jobVolumes) {
|
|
18753
|
+
try {
|
|
18754
|
+
await deps.removeVolume(v.name);
|
|
18755
|
+
volumes.push(v.name);
|
|
18756
|
+
} catch (e) {
|
|
18757
|
+
failures.push(`volume ${v.name}: ${e.message}`);
|
|
18547
18758
|
}
|
|
18548
|
-
return token;
|
|
18549
|
-
} finally {
|
|
18550
|
-
await fh.close();
|
|
18551
18759
|
}
|
|
18760
|
+
const report = { boxes: boxes.sort(), volumes, ms: now() - t0 };
|
|
18761
|
+
if (failures.length > 0) throw new SweepError(report, failures);
|
|
18762
|
+
return report;
|
|
18552
18763
|
}
|
|
18553
|
-
|
|
18554
|
-
|
|
18555
|
-
|
|
18556
|
-
deps.log(
|
|
18557
|
-
` sandbox: swept ${sweep.boxes.length} box(es), ${sweep.volumes.length} volume(s) in ${sweep.ms}ms, ${inputDirs} input dir(s)`
|
|
18558
|
-
);
|
|
18559
|
-
const slots = await parts.runtime.ensureSlots(settings.slots);
|
|
18560
|
-
const pool = parts.makePool(slots);
|
|
18561
|
-
const claudeToken = await (deps.readClaudeToken ?? readClaudeTokenFile)(settings.claudeTokenPath);
|
|
18562
|
-
const gates = await parts.startGates(slots, {
|
|
18563
|
-
mcpBaseUrl: deps.mcpBaseUrl,
|
|
18564
|
-
claudeToken,
|
|
18565
|
-
remint: deps.remint,
|
|
18566
|
-
log: deps.log
|
|
18567
|
-
});
|
|
18568
|
-
let preflight;
|
|
18569
|
-
try {
|
|
18570
|
-
preflight = await parts.runtime.preflight();
|
|
18571
|
-
} catch (e) {
|
|
18572
|
-
await gates.close().catch(() => {
|
|
18573
|
-
});
|
|
18574
|
-
throw e;
|
|
18575
|
-
}
|
|
18576
|
-
if (!preflight.ok) {
|
|
18577
|
-
await gates.close().catch(() => {
|
|
18578
|
-
});
|
|
18579
|
-
const failed = preflight.checks.filter((c3) => !c3.ok).map((c3) => `${c3.name}${c3.detail ? ` (${c3.detail})` : ""}`);
|
|
18580
|
-
throw new SandboxBootError(`sandbox preflight failed: ${failed.join(", ") || "unknown"}`);
|
|
18581
|
-
}
|
|
18582
|
-
return {
|
|
18583
|
-
host: { settings, runtime: parts.runtime, registry: parts.registry, gates, pool, io: parts.io },
|
|
18584
|
-
sweep,
|
|
18585
|
-
preflight
|
|
18586
|
-
};
|
|
18587
|
-
}
|
|
18588
|
-
var SandboxBootError;
|
|
18589
|
-
var init_host = __esm({
|
|
18590
|
-
"src/daemon/sandbox/host.ts"() {
|
|
18764
|
+
var JOB_VOLUME_PREFIX, SweepError, isJobBox, isJobVolume;
|
|
18765
|
+
var init_sweep = __esm({
|
|
18766
|
+
"src/daemon/sandbox/sweep.ts"() {
|
|
18591
18767
|
"use strict";
|
|
18592
|
-
|
|
18593
|
-
|
|
18594
|
-
|
|
18595
|
-
|
|
18596
|
-
|
|
18768
|
+
init_types();
|
|
18769
|
+
JOB_VOLUME_PREFIX = "archme-job-";
|
|
18770
|
+
SweepError = class extends Error {
|
|
18771
|
+
constructor(report, failures) {
|
|
18772
|
+
super(`sandbox sweep left ${failures.length} item(s): ${failures.join("; ")}`);
|
|
18773
|
+
this.report = report;
|
|
18774
|
+
this.failures = failures;
|
|
18775
|
+
this.name = "SweepError";
|
|
18597
18776
|
}
|
|
18598
18777
|
};
|
|
18778
|
+
isJobBox = (b) => JOB_LABEL in b.labels;
|
|
18779
|
+
isJobVolume = (v) => v.name.startsWith(JOB_VOLUME_PREFIX) || JOB_LABEL in v.labels;
|
|
18599
18780
|
}
|
|
18600
18781
|
});
|
|
18601
18782
|
|
|
18602
|
-
// src/daemon/sandbox/
|
|
18603
|
-
|
|
18783
|
+
// src/daemon/sandbox/appleContainer.ts
|
|
18784
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
18785
|
+
import { homedir as homedir7 } from "node:os";
|
|
18786
|
+
import { isAbsolute, normalize } from "node:path";
|
|
18787
|
+
function assertName(kind, value) {
|
|
18788
|
+
if (!NAME_RE.test(value)) throw new Error(`sandbox: invalid ${kind} name ${JSON.stringify(value)}`);
|
|
18789
|
+
}
|
|
18790
|
+
function assertImage(image) {
|
|
18791
|
+
if (!image || image.startsWith("-") || /\s/.test(image)) {
|
|
18792
|
+
throw new Error(`sandbox: invalid image ${JSON.stringify(image)}`);
|
|
18793
|
+
}
|
|
18794
|
+
}
|
|
18795
|
+
function labelArgs(labels) {
|
|
18796
|
+
const out = [];
|
|
18797
|
+
for (const [k, v] of Object.entries(labels)) {
|
|
18798
|
+
if (!LABEL_KEY_RE.test(k)) throw new Error(`sandbox: invalid label key ${JSON.stringify(k)}`);
|
|
18799
|
+
if (CONTROL_RE.test(v)) throw new Error(`sandbox: label ${k} value contains a control character`);
|
|
18800
|
+
out.push("--label", `${k}=${v}`);
|
|
18801
|
+
}
|
|
18802
|
+
return out;
|
|
18803
|
+
}
|
|
18804
|
+
function assertInputDir(dir, home2 = homedir7()) {
|
|
18805
|
+
if (!isAbsolute(dir)) throw new Error(`sandbox: inputDir must be absolute: ${dir}`);
|
|
18806
|
+
const strip2 = (p) => p.replace(/\/+$/, "") || "/";
|
|
18807
|
+
const n2 = strip2(normalize(dir));
|
|
18808
|
+
if (n2 !== strip2(dir)) throw new Error(`sandbox: inputDir must be normalized: ${dir}`);
|
|
18809
|
+
if (/[:,]/.test(n2) || CONTROL_RE.test(n2)) throw new Error(`sandbox: inputDir has a forbidden character: ${dir}`);
|
|
18810
|
+
const homeN = strip2(normalize(home2));
|
|
18811
|
+
if (n2 === "/" || n2 === homeN || /^\/(Users|home)(\/[^/]+)?$/.test(n2) || n2 === "/root" || n2 === "/var/root") {
|
|
18812
|
+
throw new Error(`sandbox: inputDir must not be / or a home directory: ${dir}`);
|
|
18813
|
+
}
|
|
18814
|
+
}
|
|
18815
|
+
function envArgs(env) {
|
|
18816
|
+
const out = [];
|
|
18817
|
+
for (const [k, v] of Object.entries(env)) {
|
|
18818
|
+
if (!ENV_KEY_RE.test(k)) throw new Error(`sandbox: invalid env key ${JSON.stringify(k)}`);
|
|
18819
|
+
if (CONTROL_RE.test(v)) throw new Error(`sandbox: env ${k} value contains a newline or NUL`);
|
|
18820
|
+
out.push("-e", `${k}=${v}`);
|
|
18821
|
+
}
|
|
18822
|
+
return out;
|
|
18823
|
+
}
|
|
18824
|
+
function slotFor(index) {
|
|
18825
|
+
if (!Number.isInteger(index) || index < 0 || index >= MAX_SLOTS) {
|
|
18826
|
+
throw new Error(`sandbox: slot index out of range: ${index}`);
|
|
18827
|
+
}
|
|
18828
|
+
const third = SLOT_RANGE_BASE_THIRD_OCTET + index;
|
|
18604
18829
|
return {
|
|
18605
|
-
|
|
18606
|
-
|
|
18607
|
-
|
|
18608
|
-
|
|
18609
|
-
repo_branch: job.repo_branch,
|
|
18610
|
-
archme_branch: job.archme_branch,
|
|
18611
|
-
before_sha: job.before_sha,
|
|
18612
|
-
after_sha: job.after_sha,
|
|
18613
|
-
forced: job.forced,
|
|
18614
|
-
pr_number: job.pr_number ?? null,
|
|
18615
|
-
source_mode: job.source_mode,
|
|
18616
|
-
commit_message: job.commit_message ?? null,
|
|
18617
|
-
head_commit_timestamp: job.head_commit_timestamp ?? null,
|
|
18618
|
-
author_name: job.author_name ?? null,
|
|
18619
|
-
author_login: job.author_login ?? null
|
|
18830
|
+
index,
|
|
18831
|
+
network: `archme-slot-${index}`,
|
|
18832
|
+
subnet: `192.168.${third}.0/24`,
|
|
18833
|
+
gateway: `192.168.${third}.1`
|
|
18620
18834
|
};
|
|
18621
18835
|
}
|
|
18622
|
-
|
|
18623
|
-
|
|
18624
|
-
const
|
|
18625
|
-
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
18629
|
-
|
|
18630
|
-
|
|
18631
|
-
|
|
18632
|
-
|
|
18633
|
-
|
|
18634
|
-
|
|
18635
|
-
|
|
18636
|
-
|
|
18637
|
-
|
|
18638
|
-
|
|
18639
|
-
|
|
18640
|
-
|
|
18641
|
-
|
|
18642
|
-
|
|
18643
|
-
|
|
18644
|
-
|
|
18645
|
-
|
|
18646
|
-
|
|
18647
|
-
|
|
18648
|
-
|
|
18649
|
-
|
|
18650
|
-
|
|
18651
|
-
|
|
18652
|
-
|
|
18653
|
-
|
|
18654
|
-
|
|
18655
|
-
|
|
18656
|
-
|
|
18657
|
-
|
|
18658
|
-
|
|
18659
|
-
|
|
18660
|
-
|
|
18661
|
-
|
|
18662
|
-
|
|
18663
|
-
|
|
18664
|
-
|
|
18665
|
-
|
|
18666
|
-
|
|
18667
|
-
|
|
18668
|
-
};
|
|
18669
|
-
const writeInput = async (step, input) => {
|
|
18670
|
-
const w = await deps.writeInputDir(step, input);
|
|
18671
|
-
let done = false;
|
|
18672
|
-
const cleanup = async () => {
|
|
18673
|
-
if (done) return;
|
|
18674
|
-
done = true;
|
|
18675
|
-
await w.cleanup().catch(() => {
|
|
18676
|
-
});
|
|
18677
|
-
};
|
|
18678
|
-
inputCleanups.push(cleanup);
|
|
18679
|
-
return { dir: w.dir, cleanup };
|
|
18680
|
-
};
|
|
18681
|
-
const runBox = async (spec2, timeoutMs) => {
|
|
18682
|
-
if (deps.isCancelled(job.id)) return { kind: "refused", reason: "cancel" };
|
|
18683
|
-
if (!await deps.registry.beginStep(job.id, spec2.name)) return { kind: "refused", reason: "shutting_down" };
|
|
18684
|
-
liveBoxes.add(spec2.name);
|
|
18685
|
-
const started = now();
|
|
18686
|
-
let killCause = null;
|
|
18687
|
-
const kill = (cause) => {
|
|
18688
|
-
if (killCause) return;
|
|
18689
|
-
killCause = cause;
|
|
18690
|
-
deps.log(`killing ${spec2.name} (${cause})`);
|
|
18691
|
-
if (cause === "lease_lost") deps.gates.clearJob(slot);
|
|
18692
|
-
void deps.runtime.kill(spec2.name).catch((e) => deps.log(`kill ${spec2.name} failed: ${e.message}`));
|
|
18693
|
-
};
|
|
18694
|
-
if (spec2.args[0] === "agent") agentStarted = true;
|
|
18695
|
-
const handle = await deps.runtime.runStep(spec2);
|
|
18696
|
-
const timer = setTimeout(() => kill("timeout"), timeoutMs);
|
|
18697
|
-
const poll = setInterval(() => {
|
|
18698
|
-
const c3 = currentKillCause();
|
|
18699
|
-
if (c3) kill(c3);
|
|
18700
|
-
}, pollMs);
|
|
18701
|
-
try {
|
|
18702
|
-
const exit = await handle.done;
|
|
18703
|
-
liveBoxes.delete(spec2.name);
|
|
18704
|
-
deps.registry.endStep(job.id, spec2.name);
|
|
18705
|
-
if (!killCause && exit.killed) killCause = currentKillCause() ?? "shutdown";
|
|
18706
|
-
return { kind: "ran", exit, killCause, ms: now() - started };
|
|
18707
|
-
} finally {
|
|
18708
|
-
clearTimeout(timer);
|
|
18709
|
-
clearInterval(poll);
|
|
18710
|
-
}
|
|
18711
|
-
};
|
|
18712
|
-
const spec = (step, network, env, inputDir) => ({
|
|
18713
|
-
name: `archme-job-${job.id}-${step}`,
|
|
18714
|
-
image: settings.image,
|
|
18715
|
-
args: [step],
|
|
18836
|
+
function slotNetwork(slot) {
|
|
18837
|
+
if (!slot || typeof slot !== "object") throw new Error("sandbox: a step needs a slot network");
|
|
18838
|
+
const want = slotFor(slot.index);
|
|
18839
|
+
if (slot.network !== want.network) throw new Error(`sandbox: ${slot.network} is not slot ${slot.index}'s network`);
|
|
18840
|
+
return want.network;
|
|
18841
|
+
}
|
|
18842
|
+
function buildRunArgs(spec, home2) {
|
|
18843
|
+
assertName("box", spec.name);
|
|
18844
|
+
if (spec.volume !== void 0) assertName("volume", spec.volume);
|
|
18845
|
+
assertImage(spec.image);
|
|
18846
|
+
assertInputDir(spec.inputDir, home2);
|
|
18847
|
+
if (!spec.labels[JOB_LABEL]) throw new Error(`sandbox: step labels must include ${JOB_LABEL}`);
|
|
18848
|
+
const { cpus, memoryMiB } = spec.limits;
|
|
18849
|
+
if (!Number.isInteger(cpus) || cpus < 1) throw new Error(`sandbox: invalid cpus ${cpus}`);
|
|
18850
|
+
if (!Number.isInteger(memoryMiB) || memoryMiB < MIN_MEMORY_MIB) {
|
|
18851
|
+
throw new Error(`sandbox: memoryMiB must be an integer \u2265 ${MIN_MEMORY_MIB}: ${memoryMiB}`);
|
|
18852
|
+
}
|
|
18853
|
+
const network = slotNetwork(spec.network);
|
|
18854
|
+
return [
|
|
18855
|
+
// `-i`: stdin carries the per-step result nonce (one line, then EOF) to
|
|
18856
|
+
// the wrapper — the only channel that is neither a file, env nor argv.
|
|
18857
|
+
"run",
|
|
18858
|
+
"-i",
|
|
18859
|
+
"--rm",
|
|
18860
|
+
"--init",
|
|
18861
|
+
"--read-only",
|
|
18862
|
+
"--cap-drop",
|
|
18863
|
+
"ALL",
|
|
18864
|
+
"--user",
|
|
18865
|
+
`${BOX_UID}:${BOX_UID}`,
|
|
18866
|
+
"--tmpfs",
|
|
18867
|
+
"/tmp",
|
|
18868
|
+
"--no-dns",
|
|
18869
|
+
"--kernel-arg",
|
|
18870
|
+
"ipv6.disable=1",
|
|
18871
|
+
// Progress lines otherwise land on stderr ahead of the box's own output.
|
|
18872
|
+
"--progress",
|
|
18873
|
+
"none",
|
|
18874
|
+
"--cpus",
|
|
18875
|
+
String(cpus),
|
|
18876
|
+
"--memory",
|
|
18877
|
+
`${memoryMiB}M`,
|
|
18878
|
+
"--name",
|
|
18879
|
+
spec.name,
|
|
18880
|
+
...labelArgs(spec.labels),
|
|
18881
|
+
"--network",
|
|
18716
18882
|
network,
|
|
18717
|
-
|
|
18718
|
-
|
|
18719
|
-
|
|
18720
|
-
|
|
18721
|
-
|
|
18722
|
-
|
|
18723
|
-
|
|
18724
|
-
|
|
18725
|
-
|
|
18726
|
-
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
|
|
18730
|
-
|
|
18731
|
-
|
|
18732
|
-
|
|
18733
|
-
|
|
18734
|
-
|
|
18735
|
-
|
|
18736
|
-
|
|
18737
|
-
|
|
18738
|
-
|
|
18739
|
-
|
|
18740
|
-
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
|
|
18748
|
-
|
|
18749
|
-
|
|
18750
|
-
|
|
18751
|
-
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
|
|
18757
|
-
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18764
|
-
|
|
18765
|
-
|
|
18766
|
-
|
|
18767
|
-
|
|
18768
|
-
|
|
18769
|
-
|
|
18770
|
-
|
|
18771
|
-
|
|
18772
|
-
|
|
18773
|
-
|
|
18774
|
-
|
|
18775
|
-
}
|
|
18776
|
-
|
|
18777
|
-
|
|
18778
|
-
|
|
18779
|
-
|
|
18780
|
-
|
|
18781
|
-
|
|
18782
|
-
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
|
|
18786
|
-
|
|
18787
|
-
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
18791
|
-
|
|
18792
|
-
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
|
|
18804
|
-
|
|
18805
|
-
|
|
18883
|
+
// Verified: `:ro` on a directory bind is a read-only virtiofs mount — even
|
|
18884
|
+
// root in the box gets EROFS on create/mkdir.
|
|
18885
|
+
"-v",
|
|
18886
|
+
`${spec.inputDir}:/job:ro`,
|
|
18887
|
+
...spec.volume !== void 0 ? ["-v", `${spec.volume}:/work`] : [],
|
|
18888
|
+
...envArgs(spec.env),
|
|
18889
|
+
spec.image,
|
|
18890
|
+
...spec.args
|
|
18891
|
+
];
|
|
18892
|
+
}
|
|
18893
|
+
function buildInitVolumeArgs(volume, uid, labels, image, slot) {
|
|
18894
|
+
assertName("volume", volume);
|
|
18895
|
+
assertImage(image);
|
|
18896
|
+
const network = slotNetwork(slot);
|
|
18897
|
+
if (!Number.isInteger(uid) || uid <= 0) throw new Error(`sandbox: invalid uid ${uid}`);
|
|
18898
|
+
return [
|
|
18899
|
+
"run",
|
|
18900
|
+
"--rm",
|
|
18901
|
+
"--init",
|
|
18902
|
+
"--read-only",
|
|
18903
|
+
"--cap-drop",
|
|
18904
|
+
"ALL",
|
|
18905
|
+
"--cap-add",
|
|
18906
|
+
"CHOWN",
|
|
18907
|
+
"--user",
|
|
18908
|
+
"0:0",
|
|
18909
|
+
"--no-dns",
|
|
18910
|
+
"--kernel-arg",
|
|
18911
|
+
"ipv6.disable=1",
|
|
18912
|
+
"--progress",
|
|
18913
|
+
"none",
|
|
18914
|
+
"--network",
|
|
18915
|
+
network,
|
|
18916
|
+
"--memory",
|
|
18917
|
+
`${INIT_MEMORY_MIB}M`,
|
|
18918
|
+
...labelArgs(labels),
|
|
18919
|
+
"-v",
|
|
18920
|
+
`${volume}:/work`,
|
|
18921
|
+
"--entrypoint",
|
|
18922
|
+
"chown",
|
|
18923
|
+
image,
|
|
18924
|
+
`${uid}:${uid}`,
|
|
18925
|
+
"/work"
|
|
18926
|
+
];
|
|
18927
|
+
}
|
|
18928
|
+
function buildCreateVolumeArgs(name, sizeGiB, labels) {
|
|
18929
|
+
assertName("volume", name);
|
|
18930
|
+
if (!Number.isInteger(sizeGiB) || sizeGiB < 1) throw new Error(`sandbox: invalid volume size ${sizeGiB}`);
|
|
18931
|
+
return ["volume", "create", "-s", `${sizeGiB}G`, ...labelArgs(labels), name];
|
|
18932
|
+
}
|
|
18933
|
+
function buildEnsureNetworkArgs(slot) {
|
|
18934
|
+
return [
|
|
18935
|
+
"network",
|
|
18936
|
+
"create",
|
|
18937
|
+
"--internal",
|
|
18938
|
+
"--subnet",
|
|
18939
|
+
slot.subnet,
|
|
18940
|
+
"--label",
|
|
18941
|
+
`${SLOT_LABEL}=${slot.index}`,
|
|
18942
|
+
slot.network
|
|
18943
|
+
];
|
|
18944
|
+
}
|
|
18945
|
+
function labelsOf(v) {
|
|
18946
|
+
const out = {};
|
|
18947
|
+
for (const [k, x] of Object.entries(obj(v))) if (typeof x === "string") out[k] = x;
|
|
18948
|
+
return out;
|
|
18949
|
+
}
|
|
18950
|
+
function parseArray(json, what) {
|
|
18951
|
+
const parsed = JSON.parse(json.trim() || "[]");
|
|
18952
|
+
if (!Array.isArray(parsed)) throw new Error(`sandbox: ${what} is not a JSON array`);
|
|
18953
|
+
return parsed;
|
|
18954
|
+
}
|
|
18955
|
+
function parseBoxList(json) {
|
|
18956
|
+
return parseArray(json, "container ls").flatMap((raw) => {
|
|
18957
|
+
const r = obj(raw);
|
|
18958
|
+
const id = str(r.id) ?? str(obj(r.configuration).id);
|
|
18959
|
+
if (!id) return [];
|
|
18960
|
+
return [{ id, state: str(obj(r.status).state) ?? "unknown", labels: labelsOf(obj(r.configuration).labels) }];
|
|
18961
|
+
});
|
|
18962
|
+
}
|
|
18963
|
+
function parseVolumeList(json) {
|
|
18964
|
+
return parseArray(json, "container volume ls").flatMap((raw) => {
|
|
18965
|
+
const r = obj(raw);
|
|
18966
|
+
const name = str(obj(r.configuration).name) ?? str(r.id);
|
|
18967
|
+
if (!name) return [];
|
|
18968
|
+
return [{ name, labels: labelsOf(obj(r.configuration).labels) }];
|
|
18969
|
+
});
|
|
18970
|
+
}
|
|
18971
|
+
function parseNetworkList(json) {
|
|
18972
|
+
return parseArray(json, "container network ls").flatMap((raw) => {
|
|
18973
|
+
const r = obj(raw);
|
|
18974
|
+
const c3 = obj(r.configuration);
|
|
18975
|
+
const name = str(c3.name) ?? str(r.id);
|
|
18976
|
+
if (!name) return [];
|
|
18977
|
+
return [{
|
|
18978
|
+
name,
|
|
18979
|
+
subnet: str(c3.ipv4Subnet) ?? str(obj(r.status).ipv4Subnet),
|
|
18980
|
+
mode: str(c3.mode),
|
|
18981
|
+
labels: labelsOf(c3.labels)
|
|
18982
|
+
}];
|
|
18983
|
+
});
|
|
18984
|
+
}
|
|
18985
|
+
function isBoxNotFound(stderr) {
|
|
18986
|
+
return /notFound: "container with ID .* not found"/.test(stderr);
|
|
18987
|
+
}
|
|
18988
|
+
function isBoxStopped(stderr) {
|
|
18989
|
+
return /invalidState: "no runtime client exists: container is stopped"/.test(stderr);
|
|
18990
|
+
}
|
|
18991
|
+
function defaultExec2(bin = "container") {
|
|
18992
|
+
return async (args) => {
|
|
18993
|
+
const r = await execa(bin, args, { reject: false, stdin: "ignore" });
|
|
18994
|
+
const stderr = typeof r.stderr === "string" ? r.stderr : "";
|
|
18806
18995
|
return {
|
|
18807
|
-
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18811
|
-
stderr: agent.exit.stderr,
|
|
18812
|
-
affected: ar.affected,
|
|
18813
|
-
metrics
|
|
18996
|
+
exitCode: r.exitCode ?? -1,
|
|
18997
|
+
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
18998
|
+
stderr: r.exitCode === void 0 ? `${stderr}
|
|
18999
|
+
${r.message}`.trim() : stderr
|
|
18814
19000
|
};
|
|
18815
19001
|
};
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
}
|
|
18829
|
-
deps.log(`sandbox cleanup (${what}) failed: ${e.message}`);
|
|
18830
|
-
}
|
|
19002
|
+
}
|
|
19003
|
+
function defaultSpawn(bin = "container") {
|
|
19004
|
+
return (args) => {
|
|
19005
|
+
const p = execa(bin, args, { reject: false, stdin: "pipe", buffer: false });
|
|
19006
|
+
return {
|
|
19007
|
+
stdin: p.stdin,
|
|
19008
|
+
stdout: p.stdout,
|
|
19009
|
+
stderr: p.stderr,
|
|
19010
|
+
exited: p.then((r) => ({
|
|
19011
|
+
exitCode: r.exitCode ?? null,
|
|
19012
|
+
signal: r.signal ?? null,
|
|
19013
|
+
error: r.exitCode === void 0 && !r.signal ? r.message : void 0
|
|
19014
|
+
}))
|
|
18831
19015
|
};
|
|
18832
|
-
|
|
18833
|
-
if (volumeTouched) await step("remove volume", () => deps.runtime.removeVolume(volume));
|
|
18834
|
-
await step("counters", readCounters);
|
|
18835
|
-
await step("clear gates", () => deps.gates.clearJob(slot));
|
|
18836
|
-
await step("free slot", () => deps.pool.free(slot.index));
|
|
18837
|
-
for (const c3 of inputCleanups) await step("input dir", c3);
|
|
18838
|
-
await step("release", () => deps.registry.release(job.id));
|
|
18839
|
-
}
|
|
19016
|
+
};
|
|
18840
19017
|
}
|
|
18841
|
-
var
|
|
18842
|
-
var
|
|
18843
|
-
"src/daemon/sandbox/
|
|
19018
|
+
var MIN_MEMORY_MIB, STDOUT_CAP_BYTES, STDERR_CAP_BYTES, INIT_MEMORY_MIB, NAME_RE, ENV_KEY_RE, LABEL_KEY_RE, CONTROL_RE, obj, str, TailBuffer, AppleContainerRuntime;
|
|
19019
|
+
var init_appleContainer = __esm({
|
|
19020
|
+
"src/daemon/sandbox/appleContainer.ts"() {
|
|
18844
19021
|
"use strict";
|
|
19022
|
+
init_execa();
|
|
19023
|
+
init_sweep();
|
|
18845
19024
|
init_types();
|
|
18846
|
-
|
|
18847
|
-
|
|
18848
|
-
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
|
|
18858
|
-
|
|
18859
|
-
|
|
18860
|
-
|
|
18861
|
-
|
|
18862
|
-
|
|
18863
|
-
|
|
18864
|
-
|
|
18865
|
-
|
|
18866
|
-
|
|
18867
|
-
|
|
18868
|
-
|
|
18869
|
-
|
|
19025
|
+
MIN_MEMORY_MIB = 200;
|
|
19026
|
+
STDOUT_CAP_BYTES = 4 * 1024 * 1024;
|
|
19027
|
+
STDERR_CAP_BYTES = 1024 * 1024;
|
|
19028
|
+
INIT_MEMORY_MIB = 256;
|
|
19029
|
+
NAME_RE = /^[a-z0-9][a-z0-9_.-]*$/;
|
|
19030
|
+
ENV_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
19031
|
+
LABEL_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
19032
|
+
CONTROL_RE = /[\u0000\n\r]/;
|
|
19033
|
+
obj = (v) => v && typeof v === "object" ? v : {};
|
|
19034
|
+
str = (v) => typeof v === "string" ? v : null;
|
|
19035
|
+
TailBuffer = class {
|
|
19036
|
+
constructor(cap) {
|
|
19037
|
+
this.cap = cap;
|
|
19038
|
+
}
|
|
19039
|
+
chunks = [];
|
|
19040
|
+
size = 0;
|
|
19041
|
+
truncated = false;
|
|
19042
|
+
push(chunk) {
|
|
19043
|
+
this.chunks.push(chunk);
|
|
19044
|
+
this.size += chunk.length;
|
|
19045
|
+
while (this.size > this.cap && this.chunks.length > 0) {
|
|
19046
|
+
const head = this.chunks[0];
|
|
19047
|
+
const over = this.size - this.cap;
|
|
19048
|
+
this.truncated = true;
|
|
19049
|
+
if (head.length <= over) {
|
|
19050
|
+
this.chunks.shift();
|
|
19051
|
+
this.size -= head.length;
|
|
19052
|
+
} else {
|
|
19053
|
+
this.chunks[0] = head.subarray(over);
|
|
19054
|
+
this.size -= over;
|
|
19055
|
+
}
|
|
19056
|
+
}
|
|
19057
|
+
}
|
|
19058
|
+
toString() {
|
|
19059
|
+
let buf = Buffer.concat(this.chunks);
|
|
19060
|
+
if (this.truncated) {
|
|
19061
|
+
let i2 = 0;
|
|
19062
|
+
while (i2 < buf.length && i2 < 3 && (buf[i2] & 192) === 128) i2++;
|
|
19063
|
+
buf = buf.subarray(i2);
|
|
19064
|
+
}
|
|
19065
|
+
return buf.toString("utf8");
|
|
19066
|
+
}
|
|
19067
|
+
};
|
|
19068
|
+
AppleContainerRuntime = class {
|
|
19069
|
+
constructor(opts = {}) {
|
|
19070
|
+
this.opts = opts;
|
|
19071
|
+
this.exec = opts.exec ?? defaultExec2(opts.bin);
|
|
19072
|
+
this.spawn = opts.spawn ?? defaultSpawn(opts.bin);
|
|
19073
|
+
}
|
|
19074
|
+
name = "apple-container";
|
|
19075
|
+
exec;
|
|
19076
|
+
spawn;
|
|
19077
|
+
/** Boxes `kill()` was called for while their step was running. */
|
|
19078
|
+
killedNames = /* @__PURE__ */ new Set();
|
|
19079
|
+
running = /* @__PURE__ */ new Set();
|
|
19080
|
+
async must(args, what) {
|
|
19081
|
+
const r = await this.exec(args);
|
|
19082
|
+
if (r.exitCode !== 0) throw new Error(`sandbox: ${what} failed (exit ${r.exitCode}): ${r.stderr.trim()}`);
|
|
19083
|
+
return r;
|
|
19084
|
+
}
|
|
19085
|
+
async preflight() {
|
|
19086
|
+
const checks = [];
|
|
19087
|
+
const status = await this.exec(["system", "status"]);
|
|
19088
|
+
const up = status.exitCode === 0 && /^status\s+running\b/m.test(status.stdout);
|
|
19089
|
+
checks.push({ name: "runtime_status", ok: up, detail: up ? void 0 : (status.stderr || status.stdout).trim() });
|
|
19090
|
+
if (this.opts.image) {
|
|
19091
|
+
let img = await this.exec(["image", "inspect", this.opts.image]);
|
|
19092
|
+
if (img.exitCode !== 0 && up && this.opts.image.includes("@sha256:")) {
|
|
19093
|
+
const pull = await this.exec(["image", "pull", this.opts.image]);
|
|
19094
|
+
img = pull.exitCode === 0 ? await this.exec(["image", "inspect", this.opts.image]) : pull;
|
|
19095
|
+
}
|
|
19096
|
+
checks.push({ name: "image_present", ok: img.exitCode === 0, detail: img.exitCode === 0 ? void 0 : img.stderr.trim() });
|
|
19097
|
+
} else {
|
|
19098
|
+
checks.push({ name: "image_present", ok: false, detail: "no image configured" });
|
|
19099
|
+
}
|
|
19100
|
+
const want = this.opts.slots ?? 0;
|
|
19101
|
+
if (up) {
|
|
19102
|
+
const nets = await this.listNetworks();
|
|
19103
|
+
const missing = [];
|
|
19104
|
+
for (let i2 = 0; i2 < want; i2++) {
|
|
19105
|
+
const s = slotFor(i2);
|
|
19106
|
+
const n2 = nets.find((x) => x.name === s.network);
|
|
19107
|
+
if (!n2 || n2.subnet !== s.subnet) missing.push(n2 ? `${s.network} (subnet ${n2.subnet})` : s.network);
|
|
19108
|
+
}
|
|
19109
|
+
checks.push({ name: "slot_networks", ok: missing.length === 0, detail: missing.length ? `missing/wrong: ${missing.join(", ")}` : void 0 });
|
|
19110
|
+
} else {
|
|
19111
|
+
checks.push({ name: "slot_networks", ok: false, detail: "runtime not running" });
|
|
19112
|
+
}
|
|
19113
|
+
return { ok: checks.every((c3) => c3.ok), checks };
|
|
19114
|
+
}
|
|
19115
|
+
async listNetworks() {
|
|
19116
|
+
const r = await this.must(["network", "ls", "--format", "json"], "network ls");
|
|
19117
|
+
return parseNetworkList(r.stdout);
|
|
19118
|
+
}
|
|
19119
|
+
async ensureSlots(n2) {
|
|
19120
|
+
if (!Number.isInteger(n2) || n2 < 1 || n2 > MAX_SLOTS) throw new Error(`sandbox: slot count out of range: ${n2}`);
|
|
19121
|
+
const slots = Array.from({ length: n2 }, (_, i2) => slotFor(i2));
|
|
19122
|
+
let nets = await this.listNetworks();
|
|
19123
|
+
for (const s of slots) {
|
|
19124
|
+
const existing = nets.find((x) => x.name === s.network);
|
|
19125
|
+
if (existing) {
|
|
19126
|
+
if (existing.subnet !== s.subnet) {
|
|
19127
|
+
throw new Error(`sandbox: ${s.network} exists with subnet ${existing.subnet}, expected ${s.subnet}`);
|
|
19128
|
+
}
|
|
19129
|
+
continue;
|
|
19130
|
+
}
|
|
19131
|
+
const r = await this.exec(buildEnsureNetworkArgs(s));
|
|
19132
|
+
if (r.exitCode !== 0) {
|
|
19133
|
+
nets = await this.listNetworks();
|
|
19134
|
+
const now = nets.find((x) => x.name === s.network);
|
|
19135
|
+
if (!now || now.subnet !== s.subnet) {
|
|
19136
|
+
throw new Error(`sandbox: creating ${s.network} failed: ${r.stderr.trim()}`);
|
|
19137
|
+
}
|
|
19138
|
+
}
|
|
19139
|
+
}
|
|
19140
|
+
return slots;
|
|
19141
|
+
}
|
|
19142
|
+
async createVolume(name, sizeGiB, labels) {
|
|
19143
|
+
await this.must(buildCreateVolumeArgs(name, sizeGiB, labels), `volume create ${name}`);
|
|
19144
|
+
}
|
|
19145
|
+
async initVolume(name, uid, labels, image, slot) {
|
|
19146
|
+
await this.must(buildInitVolumeArgs(name, uid, labels, image, slot), `volume init ${name}`);
|
|
19147
|
+
}
|
|
19148
|
+
async runStep(spec) {
|
|
19149
|
+
const args = buildRunArgs(spec, this.opts.homeDir);
|
|
19150
|
+
const name = spec.name;
|
|
19151
|
+
this.running.add(name);
|
|
19152
|
+
this.killedNames.delete(name);
|
|
19153
|
+
const out = new TailBuffer(STDOUT_CAP_BYTES);
|
|
19154
|
+
const err4 = new TailBuffer(STDERR_CAP_BYTES);
|
|
19155
|
+
const nonce = randomBytes2(16).toString("hex");
|
|
19156
|
+
const proc = this.spawn(args);
|
|
19157
|
+
if (proc.stdin) {
|
|
19158
|
+
proc.stdin.on("error", () => {
|
|
19159
|
+
});
|
|
19160
|
+
proc.stdin.end(`${nonce}
|
|
19161
|
+
`);
|
|
19162
|
+
}
|
|
19163
|
+
const tee = (stream, buf, which) => new Promise((resolve7) => {
|
|
19164
|
+
if (!stream) return resolve7();
|
|
19165
|
+
stream.on("data", (c3) => {
|
|
19166
|
+
const b = typeof c3 === "string" ? Buffer.from(c3) : c3;
|
|
19167
|
+
buf.push(b);
|
|
19168
|
+
this.opts.onOutput?.(name, which, b);
|
|
19169
|
+
});
|
|
19170
|
+
stream.on("end", () => resolve7());
|
|
19171
|
+
stream.on("close", () => resolve7());
|
|
19172
|
+
stream.on("error", () => resolve7());
|
|
19173
|
+
});
|
|
19174
|
+
const done = (async () => {
|
|
19175
|
+
const [exit] = await Promise.all([proc.exited, tee(proc.stdout, out, "stdout"), tee(proc.stderr, err4, "stderr")]);
|
|
19176
|
+
const killed = this.killedNames.has(name);
|
|
19177
|
+
this.killedNames.delete(name);
|
|
19178
|
+
this.running.delete(name);
|
|
19179
|
+
const stderr = err4.toString();
|
|
19180
|
+
return {
|
|
19181
|
+
exitCode: exit.exitCode,
|
|
19182
|
+
signal: exit.signal,
|
|
19183
|
+
stdout: out.toString(),
|
|
19184
|
+
stderr: exit.error ? `${stderr}
|
|
19185
|
+
${exit.error}`.trim() : stderr,
|
|
19186
|
+
killed,
|
|
19187
|
+
nonce
|
|
19188
|
+
};
|
|
19189
|
+
})();
|
|
19190
|
+
return { name, done };
|
|
19191
|
+
}
|
|
19192
|
+
/** Not-found and already-stopped both count as success: the box is not running. */
|
|
19193
|
+
async kill(name) {
|
|
19194
|
+
assertName("box", name);
|
|
19195
|
+
if (this.running.has(name)) this.killedNames.add(name);
|
|
19196
|
+
const r = await this.exec(["kill", name]);
|
|
19197
|
+
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr) && !isBoxStopped(r.stderr)) {
|
|
19198
|
+
throw new Error(`sandbox: kill ${name} failed: ${r.stderr.trim()}`);
|
|
19199
|
+
}
|
|
19200
|
+
}
|
|
19201
|
+
async stop(name, graceSeconds) {
|
|
19202
|
+
assertName("box", name);
|
|
19203
|
+
const t = Math.max(0, Math.floor(graceSeconds));
|
|
19204
|
+
const r = await this.exec(["stop", "-t", String(t), name]);
|
|
19205
|
+
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr) && !isBoxStopped(r.stderr)) {
|
|
19206
|
+
throw new Error(`sandbox: stop ${name} failed: ${r.stderr.trim()}`);
|
|
19207
|
+
}
|
|
19208
|
+
}
|
|
19209
|
+
/** `container rm`; not-found counts as removed (a `--rm` box is gone once stopped). */
|
|
19210
|
+
async remove(name) {
|
|
19211
|
+
assertName("box", name);
|
|
19212
|
+
const r = await this.exec(["rm", name]);
|
|
19213
|
+
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr)) {
|
|
19214
|
+
throw new Error(`sandbox: rm ${name} failed: ${r.stderr.trim()}`);
|
|
19215
|
+
}
|
|
19216
|
+
}
|
|
19217
|
+
async removeVolume(name) {
|
|
19218
|
+
assertName("volume", name);
|
|
19219
|
+
const r = await this.exec(["volume", "rm", name]);
|
|
19220
|
+
if (r.exitCode === 0) return;
|
|
19221
|
+
const still = (await this.listVolumes()).some((v) => v.name === name);
|
|
19222
|
+
if (still) throw new Error(`sandbox: volume rm ${name} failed: ${r.stderr.trim()}`);
|
|
19223
|
+
}
|
|
19224
|
+
async list() {
|
|
19225
|
+
const r = await this.must(["ls", "-a", "--format", "json"], "ls");
|
|
19226
|
+
return parseBoxList(r.stdout);
|
|
19227
|
+
}
|
|
19228
|
+
async listVolumes() {
|
|
19229
|
+
const r = await this.must(["volume", "ls", "--format", "json"], "volume ls");
|
|
19230
|
+
return parseVolumeList(r.stdout);
|
|
19231
|
+
}
|
|
19232
|
+
sweep() {
|
|
19233
|
+
return sweepJobs(this);
|
|
19234
|
+
}
|
|
18870
19235
|
};
|
|
18871
19236
|
}
|
|
18872
|
-
|
|
18873
|
-
|
|
19237
|
+
});
|
|
19238
|
+
|
|
19239
|
+
// src/daemon/sandbox/slotMap.ts
|
|
19240
|
+
function parseIpv4(addr) {
|
|
19241
|
+
const m = IPV4_RE.exec(addr);
|
|
19242
|
+
if (!m) return null;
|
|
19243
|
+
let v = 0;
|
|
19244
|
+
for (let i2 = 1; i2 <= 4; i2++) {
|
|
19245
|
+
const o2 = Number(m[i2]);
|
|
19246
|
+
if (o2 > 255) return null;
|
|
19247
|
+
v = v * 256 + o2;
|
|
18874
19248
|
}
|
|
18875
|
-
return
|
|
19249
|
+
return v >>> 0;
|
|
18876
19250
|
}
|
|
18877
|
-
function
|
|
18878
|
-
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
|
|
18885
|
-
};
|
|
18886
|
-
}
|
|
18887
|
-
return { kind: "fail" };
|
|
19251
|
+
function parseCidr(cidr) {
|
|
19252
|
+
const [ip, bits, extra] = cidr.split("/");
|
|
19253
|
+
if (ip === void 0 || bits === void 0 || extra !== void 0 || !/^\d{1,2}$/.test(bits)) return null;
|
|
19254
|
+
const n2 = Number(bits);
|
|
19255
|
+
const base = parseIpv4(ip);
|
|
19256
|
+
if (base === null || n2 > 32) return null;
|
|
19257
|
+
const mask2 = n2 === 0 ? 0 : ~0 << 32 - n2 >>> 0;
|
|
19258
|
+
return { base: (base & mask2) >>> 0, mask: mask2 };
|
|
18888
19259
|
}
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
|
|
19260
|
+
function stripMapped(addr) {
|
|
19261
|
+
return (addr ?? "").replace(/^::ffff:/i, "");
|
|
19262
|
+
}
|
|
19263
|
+
var IPV4_RE, SlotMap;
|
|
19264
|
+
var init_slotMap = __esm({
|
|
19265
|
+
"src/daemon/sandbox/slotMap.ts"() {
|
|
18892
19266
|
"use strict";
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
this.
|
|
19267
|
+
init_appleContainer();
|
|
19268
|
+
IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
19269
|
+
SlotMap = class {
|
|
19270
|
+
byIndex = /* @__PURE__ */ new Map();
|
|
19271
|
+
byGateway = /* @__PURE__ */ new Map();
|
|
19272
|
+
allowGatewayAsPeer;
|
|
19273
|
+
constructor(slots, opts = {}) {
|
|
19274
|
+
this.allowGatewayAsPeer = opts.allowGatewayAsPeer ?? false;
|
|
19275
|
+
for (const slot of slots) {
|
|
19276
|
+
const gateway = parseIpv4(slot.gateway);
|
|
19277
|
+
const cidr = parseCidr(slot.subnet);
|
|
19278
|
+
if (gateway === null) throw new Error(`slot ${slot.index}: gateway is not an IPv4 address`);
|
|
19279
|
+
if (cidr === null) throw new Error(`slot ${slot.index}: subnet is not an IPv4 CIDR`);
|
|
19280
|
+
if (this.byIndex.has(slot.index)) throw new Error(`slot ${slot.index}: duplicate index`);
|
|
19281
|
+
if (this.byGateway.has(slot.gateway)) throw new Error(`slot ${slot.index}: duplicate gateway`);
|
|
19282
|
+
const entry = { slot, gateway, cidr, binding: null };
|
|
19283
|
+
this.byIndex.set(slot.index, entry);
|
|
19284
|
+
this.byGateway.set(slot.gateway, entry);
|
|
19285
|
+
}
|
|
19286
|
+
}
|
|
19287
|
+
/**
|
|
19288
|
+
* The slot a connection belongs to, or null (drop it). `local` must be a slot
|
|
19289
|
+
* gateway; `remote` must be a plain IPv4 inside THAT slot's subnet — never
|
|
19290
|
+
* IPv6, never another slot's range.
|
|
19291
|
+
*/
|
|
19292
|
+
resolve(localAddress, remoteAddress) {
|
|
19293
|
+
const entry = this.byGateway.get(stripMapped(localAddress));
|
|
19294
|
+
if (!entry) return null;
|
|
19295
|
+
const remoteStr = stripMapped(remoteAddress);
|
|
19296
|
+
const remote = parseIpv4(remoteStr);
|
|
19297
|
+
if (remote === null) return null;
|
|
19298
|
+
if ((remote & entry.cidr.mask) >>> 0 !== entry.cidr.base) return null;
|
|
19299
|
+
if (remote === entry.gateway && !this.allowGatewayAsPeer) return null;
|
|
19300
|
+
return entry.slot;
|
|
19301
|
+
}
|
|
19302
|
+
slots() {
|
|
19303
|
+
return [...this.byIndex.values()].map((e) => e.slot);
|
|
19304
|
+
}
|
|
19305
|
+
slot(index) {
|
|
19306
|
+
return this.byIndex.get(index)?.slot ?? null;
|
|
19307
|
+
}
|
|
19308
|
+
bind(index, binding) {
|
|
19309
|
+
const entry = this.entry(index);
|
|
19310
|
+
if (entry.binding && entry.binding.jobId !== binding.jobId) {
|
|
19311
|
+
throw new Error(`slot ${index} is bound to another job`);
|
|
19312
|
+
}
|
|
19313
|
+
entry.binding = binding;
|
|
19314
|
+
}
|
|
19315
|
+
clear(index) {
|
|
19316
|
+
this.entry(index).binding = null;
|
|
19317
|
+
}
|
|
19318
|
+
binding(index) {
|
|
19319
|
+
return this.byIndex.get(index)?.binding ?? null;
|
|
19320
|
+
}
|
|
19321
|
+
bindingForJob(jobId) {
|
|
19322
|
+
for (const e of this.byIndex.values()) {
|
|
19323
|
+
if (e.binding?.jobId === jobId) return { slot: e.slot, binding: e.binding };
|
|
19324
|
+
}
|
|
19325
|
+
return null;
|
|
19326
|
+
}
|
|
19327
|
+
entry(index) {
|
|
19328
|
+
const e = this.byIndex.get(index);
|
|
19329
|
+
if (!e) throw new Error(`unknown slot ${index}`);
|
|
19330
|
+
return e;
|
|
18901
19331
|
}
|
|
18902
19332
|
};
|
|
18903
19333
|
}
|
|
18904
19334
|
});
|
|
18905
19335
|
|
|
18906
|
-
// src/daemon/sandbox/
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
const
|
|
19336
|
+
// src/daemon/sandbox/confinement.ts
|
|
19337
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
19338
|
+
function probeTargets(slot, lanIp) {
|
|
19339
|
+
const gw = slot.gateway;
|
|
19340
|
+
return [
|
|
19341
|
+
...HOST_SERVICE_PORTS.map((port) => ({ id: `host_${port}`, kind: "tcp", host: gw, port })),
|
|
19342
|
+
...lanIp ? LAN_PORTS.map((port) => ({ id: `lan_${port}`, kind: "tcp", host: lanIp, port })) : [],
|
|
19343
|
+
{ id: "public_443", kind: "tcp", host: PUBLIC_PROBE_IP, port: 443 },
|
|
19344
|
+
{ id: "public_dns", kind: "udp_dns", host: PUBLIC_PROBE_IP, port: 53 },
|
|
19345
|
+
{ id: "ipv6", kind: "ipv6_socket", host: "::", port: 0 },
|
|
19346
|
+
{ id: "gate_anthropic", kind: "http", host: gw, port: GATE_PORTS.anthropic },
|
|
19347
|
+
{ id: "gate_mcp", kind: "http", host: gw, port: GATE_PORTS.mcp },
|
|
19348
|
+
{ id: "gate_github", kind: "connect", host: gw, port: GATE_PORTS.github }
|
|
19349
|
+
];
|
|
19350
|
+
}
|
|
19351
|
+
function expectation(id) {
|
|
19352
|
+
if (id.startsWith("host_") || id.startsWith("lan_")) return { want: "timeout", ok: (o2) => o2 === "timeout" };
|
|
19353
|
+
if (id === "public_dns") return { want: "silent", ok: (o2) => o2 === "silent" };
|
|
19354
|
+
if (id.startsWith("public_")) return { want: "not connected", ok: (o2) => o2 !== "connected" };
|
|
19355
|
+
if (id === "ipv6") return { want: "unsupported", ok: (o2) => o2 === "unsupported" };
|
|
19356
|
+
if (id.startsWith("gate_")) return { want: "an HTTP status", ok: (o2) => /^\d{3}$/.test(o2) };
|
|
19357
|
+
return { want: "a known target", ok: () => false };
|
|
19358
|
+
}
|
|
19359
|
+
function judgeProbe(outcomes, targets) {
|
|
19360
|
+
const ids = targets ? targets.map((t) => t.id) : Object.keys(outcomes);
|
|
19361
|
+
for (const id of ["host_22", "public_443", "public_dns", "ipv6", "gate_anthropic", "gate_mcp", "gate_github"]) {
|
|
19362
|
+
if (!ids.includes(id)) ids.push(id);
|
|
19363
|
+
}
|
|
18910
19364
|
const failures = [];
|
|
18911
|
-
const
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
18920
|
-
|
|
19365
|
+
for (const id of ids) {
|
|
19366
|
+
const o2 = outcomes[id];
|
|
19367
|
+
const e = expectation(id);
|
|
19368
|
+
if (o2 === void 0) failures.push(`${id}: no outcome (want: ${e.want})`);
|
|
19369
|
+
else if (!e.ok(o2)) failures.push(`${id}: ${o2} (want: ${e.want})`);
|
|
19370
|
+
}
|
|
19371
|
+
return { ok: failures.length === 0, failures };
|
|
19372
|
+
}
|
|
19373
|
+
function primaryLanIpv4(ifaces) {
|
|
19374
|
+
const lo = parseIpv4(`192.168.${SLOT_RANGE_BASE_THIRD_OCTET}.0`);
|
|
19375
|
+
for (const list2 of Object.values(ifaces)) {
|
|
19376
|
+
for (const i2 of list2 ?? []) {
|
|
19377
|
+
if (i2.internal || i2.family !== "IPv4") continue;
|
|
19378
|
+
const v = parseIpv4(i2.address);
|
|
19379
|
+
if (v === null || (v & 4294965248) >>> 0 === lo) continue;
|
|
19380
|
+
return i2.address;
|
|
18921
19381
|
}
|
|
18922
|
-
}
|
|
18923
|
-
|
|
18924
|
-
|
|
18925
|
-
|
|
18926
|
-
|
|
18927
|
-
|
|
18928
|
-
|
|
18929
|
-
|
|
19382
|
+
}
|
|
19383
|
+
return null;
|
|
19384
|
+
}
|
|
19385
|
+
async function runProbeBox(deps, slot, targets, targetTimeoutMs = TARGET_TIMEOUT_MS) {
|
|
19386
|
+
const id = randomBytes3(6).toString("hex");
|
|
19387
|
+
const name = `archme-probe-${id}`;
|
|
19388
|
+
const input = { kind: "probe", targets, timeoutMs: targetTimeoutMs };
|
|
19389
|
+
let cleanup = null;
|
|
19390
|
+
let timer;
|
|
19391
|
+
try {
|
|
19392
|
+
const dir = await (deps.writeInputDir ?? writeInputDir)("probe", input);
|
|
19393
|
+
cleanup = dir.cleanup;
|
|
19394
|
+
const handle = await deps.runtime.runStep({
|
|
19395
|
+
name,
|
|
19396
|
+
image: deps.image,
|
|
19397
|
+
args: ["probe"],
|
|
19398
|
+
network: slot,
|
|
19399
|
+
env: {},
|
|
19400
|
+
inputDir: dir.dir,
|
|
19401
|
+
limits: { cpus: 1, memoryMiB: 256 },
|
|
19402
|
+
labels: { [JOB_LABEL]: `probe-${id}`, [SLOT_LABEL]: String(slot.index) }
|
|
19403
|
+
});
|
|
19404
|
+
const timeout = new Promise((r) => {
|
|
19405
|
+
timer = setTimeout(() => r("timeout"), deps.boxTimeoutMs ?? 6e4);
|
|
19406
|
+
});
|
|
19407
|
+
const exit = await Promise.race([handle.done, timeout]);
|
|
19408
|
+
if (exit === "timeout") return { outcomes: {}, error: "probe box timed out" };
|
|
19409
|
+
const parsed = (deps.parseResultLine ?? parseResultLine)(exit.stdout, exit.nonce);
|
|
19410
|
+
if (!parsed.ok || parsed.result.kind !== "probe") {
|
|
19411
|
+
return { outcomes: {}, error: `probe result: ${parsed.ok ? `unexpected kind ${parsed.result.kind}` : parsed.reason}` };
|
|
18930
19412
|
}
|
|
19413
|
+
return { outcomes: parsed.result.outcomes, error: parsed.result.error ? `probe box: ${parsed.result.error}` : null };
|
|
19414
|
+
} catch (e) {
|
|
19415
|
+
return { outcomes: {}, error: `probe box failed: ${e.message}` };
|
|
19416
|
+
} finally {
|
|
19417
|
+
clearTimeout(timer);
|
|
19418
|
+
await deps.runtime.kill(name).catch((e) => deps.log?.(`kill ${name} failed: ${e.message}`));
|
|
19419
|
+
await cleanup?.();
|
|
18931
19420
|
}
|
|
18932
|
-
const report = { boxes: boxes.sort(), volumes, ms: now() - t0 };
|
|
18933
|
-
if (failures.length > 0) throw new SweepError(report, failures);
|
|
18934
|
-
return report;
|
|
18935
19421
|
}
|
|
18936
|
-
|
|
18937
|
-
|
|
18938
|
-
|
|
19422
|
+
async function runConfinementProbe(deps, slot, lanIp) {
|
|
19423
|
+
const now = deps.now ?? Date.now;
|
|
19424
|
+
const started = now();
|
|
19425
|
+
const targets = probeTargets(slot, lanIp);
|
|
19426
|
+
const { outcomes, error } = await runProbeBox(deps, slot, targets);
|
|
19427
|
+
const j = judgeProbe(outcomes, targets);
|
|
19428
|
+
const failures = error ? [error, ...Object.keys(outcomes).length ? j.failures : []] : j.failures;
|
|
19429
|
+
return { ok: !error && j.ok, slot: slot.index, failures, outcomes, ms: now() - started };
|
|
19430
|
+
}
|
|
19431
|
+
function formatConfinementReport(r) {
|
|
19432
|
+
return r.ok ? `confinement probe passed on slot ${r.slot} (${r.ms}ms)` : `confinement probe failed on slot ${r.slot}: ${r.failures.join("; ")}`;
|
|
19433
|
+
}
|
|
19434
|
+
var PUBLIC_PROBE_IP, HOST_SERVICE_PORTS, LAN_PORTS, TARGET_TIMEOUT_MS, CONFINEMENT_PROBE_INTERVAL_MS, CONFINEMENT_RETRY_MS, realTimers, ConfinementMonitor;
|
|
19435
|
+
var init_confinement = __esm({
|
|
19436
|
+
"src/daemon/sandbox/confinement.ts"() {
|
|
18939
19437
|
"use strict";
|
|
19438
|
+
init_result2();
|
|
19439
|
+
init_inputDir();
|
|
19440
|
+
init_slotMap();
|
|
18940
19441
|
init_types();
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
|
|
18944
|
-
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
19442
|
+
PUBLIC_PROBE_IP = "1.1.1.1";
|
|
19443
|
+
HOST_SERVICE_PORTS = [22, 88, 5e3, 7e3];
|
|
19444
|
+
LAN_PORTS = [22, 5e3];
|
|
19445
|
+
TARGET_TIMEOUT_MS = 2e3;
|
|
19446
|
+
CONFINEMENT_PROBE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
19447
|
+
CONFINEMENT_RETRY_MS = 5 * 60 * 1e3;
|
|
19448
|
+
realTimers = {
|
|
19449
|
+
setTimeout: (fn, ms) => {
|
|
19450
|
+
const t = setTimeout(fn, ms);
|
|
19451
|
+
t.unref?.();
|
|
19452
|
+
return t;
|
|
19453
|
+
},
|
|
19454
|
+
clearTimeout: (h2) => clearTimeout(h2)
|
|
19455
|
+
};
|
|
19456
|
+
ConfinementMonitor = class {
|
|
19457
|
+
constructor(opts) {
|
|
19458
|
+
this.opts = opts;
|
|
19459
|
+
this.timers = opts.timers ?? realTimers;
|
|
19460
|
+
}
|
|
19461
|
+
timers;
|
|
19462
|
+
handle = null;
|
|
19463
|
+
failing = false;
|
|
19464
|
+
probing = false;
|
|
19465
|
+
stopped = false;
|
|
19466
|
+
/** The boot probe already ran; the next is an hour out. */
|
|
19467
|
+
start() {
|
|
19468
|
+
this.stopped = false;
|
|
19469
|
+
this.schedule(CONFINEMENT_PROBE_INTERVAL_MS);
|
|
19470
|
+
}
|
|
19471
|
+
stop() {
|
|
19472
|
+
this.stopped = true;
|
|
19473
|
+
if (this.handle !== null) this.timers.clearTimeout(this.handle);
|
|
19474
|
+
this.handle = null;
|
|
19475
|
+
}
|
|
19476
|
+
allowsClaims() {
|
|
19477
|
+
return this.opts.mode !== "required" || !this.failing && !this.probing;
|
|
19478
|
+
}
|
|
19479
|
+
schedule(ms) {
|
|
19480
|
+
if (this.stopped) return;
|
|
19481
|
+
if (this.handle !== null) this.timers.clearTimeout(this.handle);
|
|
19482
|
+
this.handle = this.timers.setTimeout(() => {
|
|
19483
|
+
this.handle = null;
|
|
19484
|
+
void this.runOnce();
|
|
19485
|
+
}, ms);
|
|
19486
|
+
}
|
|
19487
|
+
async runOnce() {
|
|
19488
|
+
const slot = this.opts.pool.lease();
|
|
19489
|
+
if (!slot) {
|
|
19490
|
+
this.opts.log("confinement probe: no free slot, retrying in 5 minutes");
|
|
19491
|
+
return this.schedule(CONFINEMENT_RETRY_MS);
|
|
19492
|
+
}
|
|
19493
|
+
this.probing = true;
|
|
19494
|
+
let report;
|
|
19495
|
+
try {
|
|
19496
|
+
report = await this.opts.probe(slot);
|
|
19497
|
+
} catch (e) {
|
|
19498
|
+
report = { ok: false, slot: slot.index, failures: [`probe threw: ${e.message}`], outcomes: {}, ms: 0 };
|
|
19499
|
+
} finally {
|
|
19500
|
+
this.opts.pool.free(slot.index);
|
|
19501
|
+
this.probing = false;
|
|
19502
|
+
}
|
|
19503
|
+
this.opts.log(formatConfinementReport(report));
|
|
19504
|
+
if (report.ok) {
|
|
19505
|
+
if (this.failing) this.opts.log("confinement holds again; claiming resumes");
|
|
19506
|
+
this.failing = false;
|
|
19507
|
+
return this.schedule(CONFINEMENT_PROBE_INTERVAL_MS);
|
|
19508
|
+
}
|
|
19509
|
+
if (this.opts.mode === "required") {
|
|
19510
|
+
if (!this.failing) this.opts.log("ARCHME_SANDBOX=required: not claiming jobs until confinement holds (re-probing every 5 minutes)");
|
|
19511
|
+
this.failing = true;
|
|
19512
|
+
return this.schedule(CONFINEMENT_RETRY_MS);
|
|
19513
|
+
}
|
|
19514
|
+
this.schedule(CONFINEMENT_PROBE_INTERVAL_MS);
|
|
18948
19515
|
}
|
|
18949
19516
|
};
|
|
18950
|
-
isJobBox = (b) => JOB_LABEL in b.labels;
|
|
18951
|
-
isJobVolume = (v) => v.name.startsWith(JOB_VOLUME_PREFIX) || JOB_LABEL in v.labels;
|
|
18952
19517
|
}
|
|
18953
19518
|
});
|
|
18954
19519
|
|
|
18955
|
-
// src/daemon/sandbox/
|
|
18956
|
-
import {
|
|
18957
|
-
import {
|
|
18958
|
-
import {
|
|
18959
|
-
function
|
|
18960
|
-
|
|
18961
|
-
}
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
throw new Error(`sandbox: invalid image ${JSON.stringify(image)}`);
|
|
19520
|
+
// src/daemon/sandbox/claudeToken.ts
|
|
19521
|
+
import { constants as fsConstants } from "node:fs";
|
|
19522
|
+
import { chmod as chmod5, mkdir as mkdir11, open as open3 } from "node:fs/promises";
|
|
19523
|
+
import { dirname as dirname15 } from "node:path";
|
|
19524
|
+
function assertTokenShape(raw, where) {
|
|
19525
|
+
const token = raw.trim();
|
|
19526
|
+
if (!token) throw new SandboxBootError(`sandbox Claude token ${where} is empty`);
|
|
19527
|
+
if (!/^[\x21-\x7e]+$/.test(token)) {
|
|
19528
|
+
throw new SandboxBootError(`sandbox Claude token ${where} contains whitespace or control characters; save it as one line`);
|
|
18965
19529
|
}
|
|
19530
|
+
return token;
|
|
18966
19531
|
}
|
|
18967
|
-
function
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
19532
|
+
async function readClaudeTokenFile(path7) {
|
|
19533
|
+
let fh;
|
|
19534
|
+
try {
|
|
19535
|
+
fh = await open3(path7, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
19536
|
+
} catch (e) {
|
|
19537
|
+
throw new SandboxBootError(`cannot read the sandbox Claude token at ${path7}: ${e.message}`);
|
|
18973
19538
|
}
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
|
|
18978
|
-
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
19539
|
+
try {
|
|
19540
|
+
const st = await fh.stat();
|
|
19541
|
+
if (!st.isFile()) throw new SandboxBootError(`sandbox Claude token ${path7} is not a regular file`);
|
|
19542
|
+
if ((st.mode & 63) !== 0) {
|
|
19543
|
+
throw new SandboxBootError(
|
|
19544
|
+
`sandbox Claude token ${path7} is readable by group or others (mode ${(st.mode & 511).toString(8)}); chmod 600 it`
|
|
19545
|
+
);
|
|
19546
|
+
}
|
|
19547
|
+
return assertTokenShape(await fh.readFile("utf8"), path7);
|
|
19548
|
+
} finally {
|
|
19549
|
+
await fh.close();
|
|
18985
19550
|
}
|
|
18986
19551
|
}
|
|
18987
|
-
function
|
|
18988
|
-
|
|
18989
|
-
|
|
18990
|
-
if (!ENV_KEY_RE.test(k)) throw new Error(`sandbox: invalid env key ${JSON.stringify(k)}`);
|
|
18991
|
-
if (CONTROL_RE.test(v)) throw new Error(`sandbox: env ${k} value contains a newline or NUL`);
|
|
18992
|
-
out.push("-e", `${k}=${v}`);
|
|
19552
|
+
async function readSandboxClaudeToken(settings, deps = {}) {
|
|
19553
|
+
if (settings.claudeTokenOverride) {
|
|
19554
|
+
return { token: await readClaudeTokenFile(settings.claudeTokenPath), source: "file-override" };
|
|
18993
19555
|
}
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
19556
|
+
const keytar = await (deps.loadKeytar ?? loadKeytar)();
|
|
19557
|
+
const stored = keytar ? await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT).catch(() => null) : null;
|
|
19558
|
+
if (stored) return { token: assertTokenShape(stored, "in the keychain"), source: "keychain" };
|
|
19559
|
+
try {
|
|
19560
|
+
return { token: await readClaudeTokenFile(settings.claudeTokenPath), source: "file" };
|
|
19561
|
+
} catch (e) {
|
|
19562
|
+
throw new SandboxBootError(
|
|
19563
|
+
`${e.message}; no token in the keychain either \u2014 run \`archme daemon sandbox setup\` to store one`
|
|
19564
|
+
);
|
|
18999
19565
|
}
|
|
19000
|
-
const third = SLOT_RANGE_BASE_THIRD_OCTET + index;
|
|
19001
|
-
return {
|
|
19002
|
-
index,
|
|
19003
|
-
network: `archme-slot-${index}`,
|
|
19004
|
-
subnet: `192.168.${third}.0/24`,
|
|
19005
|
-
gateway: `192.168.${third}.1`
|
|
19006
|
-
};
|
|
19007
19566
|
}
|
|
19008
|
-
function
|
|
19009
|
-
|
|
19010
|
-
|
|
19011
|
-
|
|
19012
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19567
|
+
async function storeSandboxClaudeToken(raw, opts) {
|
|
19568
|
+
const token = assertTokenShape(raw, "(entered)");
|
|
19569
|
+
let note;
|
|
19570
|
+
const keytar = opts.override ? null : await (opts.loadKeytar ?? loadKeytar)();
|
|
19571
|
+
if (opts.override) {
|
|
19572
|
+
note = `ARCHME_SANDBOX_CLAUDE_TOKEN_FILE is set: ${opts.path}`;
|
|
19573
|
+
} else if (keytar) {
|
|
19574
|
+
try {
|
|
19575
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, token);
|
|
19576
|
+
return { source: "keychain" };
|
|
19577
|
+
} catch (e) {
|
|
19578
|
+
note = `the keychain refused it (${e.message})`;
|
|
19579
|
+
}
|
|
19580
|
+
} else {
|
|
19581
|
+
note = "no keychain access on this machine";
|
|
19018
19582
|
}
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
"--tmpfs",
|
|
19034
|
-
"/tmp",
|
|
19035
|
-
"--no-dns",
|
|
19036
|
-
"--kernel-arg",
|
|
19037
|
-
"ipv6.disable=1",
|
|
19038
|
-
// Progress lines otherwise land on stderr ahead of the box's own output.
|
|
19039
|
-
"--progress",
|
|
19040
|
-
"none",
|
|
19041
|
-
"--cpus",
|
|
19042
|
-
String(cpus),
|
|
19043
|
-
"--memory",
|
|
19044
|
-
`${memoryMiB}M`,
|
|
19045
|
-
"--name",
|
|
19046
|
-
spec.name,
|
|
19047
|
-
...labelArgs(spec.labels),
|
|
19048
|
-
"--network",
|
|
19049
|
-
network,
|
|
19050
|
-
// Verified: `:ro` on a directory bind is a read-only virtiofs mount — even
|
|
19051
|
-
// root in the box gets EROFS on create/mkdir.
|
|
19052
|
-
"-v",
|
|
19053
|
-
`${spec.inputDir}:/job:ro`,
|
|
19054
|
-
"-v",
|
|
19055
|
-
`${spec.volume}:/work`,
|
|
19056
|
-
...envArgs(spec.env),
|
|
19057
|
-
spec.image,
|
|
19058
|
-
...spec.args
|
|
19059
|
-
];
|
|
19060
|
-
}
|
|
19061
|
-
function buildInitVolumeArgs(volume, uid, labels, image) {
|
|
19062
|
-
assertName("volume", volume);
|
|
19063
|
-
assertImage(image);
|
|
19064
|
-
if (!Number.isInteger(uid) || uid <= 0) throw new Error(`sandbox: invalid uid ${uid}`);
|
|
19065
|
-
return [
|
|
19066
|
-
"run",
|
|
19067
|
-
"--rm",
|
|
19068
|
-
"--init",
|
|
19069
|
-
"--read-only",
|
|
19070
|
-
"--cap-drop",
|
|
19071
|
-
"ALL",
|
|
19072
|
-
"--cap-add",
|
|
19073
|
-
"CHOWN",
|
|
19074
|
-
"--user",
|
|
19075
|
-
"0:0",
|
|
19076
|
-
"--no-dns",
|
|
19077
|
-
"--kernel-arg",
|
|
19078
|
-
"ipv6.disable=1",
|
|
19079
|
-
"--progress",
|
|
19080
|
-
"none",
|
|
19081
|
-
"--network",
|
|
19082
|
-
NAT_NETWORK,
|
|
19083
|
-
"--memory",
|
|
19084
|
-
`${INIT_MEMORY_MIB}M`,
|
|
19085
|
-
...labelArgs(labels),
|
|
19086
|
-
"-v",
|
|
19087
|
-
`${volume}:/work`,
|
|
19088
|
-
"--entrypoint",
|
|
19089
|
-
"chown",
|
|
19090
|
-
image,
|
|
19091
|
-
`${uid}:${uid}`,
|
|
19092
|
-
"/work"
|
|
19093
|
-
];
|
|
19094
|
-
}
|
|
19095
|
-
function buildCreateVolumeArgs(name, sizeGiB, labels) {
|
|
19096
|
-
assertName("volume", name);
|
|
19097
|
-
if (!Number.isInteger(sizeGiB) || sizeGiB < 1) throw new Error(`sandbox: invalid volume size ${sizeGiB}`);
|
|
19098
|
-
return ["volume", "create", "-s", `${sizeGiB}G`, ...labelArgs(labels), name];
|
|
19099
|
-
}
|
|
19100
|
-
function buildEnsureNetworkArgs(slot) {
|
|
19101
|
-
return [
|
|
19102
|
-
"network",
|
|
19103
|
-
"create",
|
|
19104
|
-
"--internal",
|
|
19105
|
-
"--subnet",
|
|
19106
|
-
slot.subnet,
|
|
19107
|
-
"--label",
|
|
19108
|
-
`${SLOT_LABEL}=${slot.index}`,
|
|
19109
|
-
slot.network
|
|
19110
|
-
];
|
|
19111
|
-
}
|
|
19112
|
-
function labelsOf(v) {
|
|
19113
|
-
const out = {};
|
|
19114
|
-
for (const [k, x] of Object.entries(obj(v))) if (typeof x === "string") out[k] = x;
|
|
19115
|
-
return out;
|
|
19116
|
-
}
|
|
19117
|
-
function parseArray(json, what) {
|
|
19118
|
-
const parsed = JSON.parse(json.trim() || "[]");
|
|
19119
|
-
if (!Array.isArray(parsed)) throw new Error(`sandbox: ${what} is not a JSON array`);
|
|
19120
|
-
return parsed;
|
|
19121
|
-
}
|
|
19122
|
-
function parseBoxList(json) {
|
|
19123
|
-
return parseArray(json, "container ls").flatMap((raw) => {
|
|
19124
|
-
const r = obj(raw);
|
|
19125
|
-
const id = str(r.id) ?? str(obj(r.configuration).id);
|
|
19126
|
-
if (!id) return [];
|
|
19127
|
-
return [{ id, state: str(obj(r.status).state) ?? "unknown", labels: labelsOf(obj(r.configuration).labels) }];
|
|
19128
|
-
});
|
|
19129
|
-
}
|
|
19130
|
-
function parseVolumeList(json) {
|
|
19131
|
-
return parseArray(json, "container volume ls").flatMap((raw) => {
|
|
19132
|
-
const r = obj(raw);
|
|
19133
|
-
const name = str(obj(r.configuration).name) ?? str(r.id);
|
|
19134
|
-
if (!name) return [];
|
|
19135
|
-
return [{ name, labels: labelsOf(obj(r.configuration).labels) }];
|
|
19136
|
-
});
|
|
19583
|
+
await mkdir11(dirname15(opts.path), { recursive: true, mode: 448 });
|
|
19584
|
+
const fh = await open3(
|
|
19585
|
+
opts.path,
|
|
19586
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | fsConstants.O_NOFOLLOW,
|
|
19587
|
+
384
|
|
19588
|
+
);
|
|
19589
|
+
try {
|
|
19590
|
+
await fh.writeFile(`${token}
|
|
19591
|
+
`);
|
|
19592
|
+
} finally {
|
|
19593
|
+
await fh.close();
|
|
19594
|
+
}
|
|
19595
|
+
await chmod5(opts.path, 384);
|
|
19596
|
+
return { source: "file", note };
|
|
19137
19597
|
}
|
|
19138
|
-
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
19145
|
-
|
|
19146
|
-
|
|
19147
|
-
|
|
19148
|
-
|
|
19149
|
-
|
|
19598
|
+
var SandboxBootError, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT;
|
|
19599
|
+
var init_claudeToken = __esm({
|
|
19600
|
+
"src/daemon/sandbox/claudeToken.ts"() {
|
|
19601
|
+
"use strict";
|
|
19602
|
+
init_credentials();
|
|
19603
|
+
SandboxBootError = class extends Error {
|
|
19604
|
+
constructor(message2) {
|
|
19605
|
+
super(message2);
|
|
19606
|
+
this.name = "SandboxBootError";
|
|
19607
|
+
}
|
|
19608
|
+
};
|
|
19609
|
+
KEYCHAIN_SERVICE = "archme-daemon";
|
|
19610
|
+
KEYCHAIN_ACCOUNT = "sandbox-claude-token";
|
|
19611
|
+
}
|
|
19612
|
+
});
|
|
19613
|
+
|
|
19614
|
+
// src/daemon/sandbox/host.ts
|
|
19615
|
+
import { networkInterfaces } from "node:os";
|
|
19616
|
+
function sandboxBootFailurePolicy(mode) {
|
|
19617
|
+
return mode === "shadow" ? "run_unboxed" : "refuse";
|
|
19618
|
+
}
|
|
19619
|
+
function shutdownKillTargets(boxedJobs2, registry) {
|
|
19620
|
+
return [...boxedJobs2].filter((id) => registry.hasLiveBoxes(id));
|
|
19621
|
+
}
|
|
19622
|
+
async function bootSandbox(settings, parts, deps) {
|
|
19623
|
+
const sweep = await parts.runtime.sweep();
|
|
19624
|
+
const inputDirs = await (deps.sweepInputDirs ?? sweepInputDirs)();
|
|
19625
|
+
deps.log(
|
|
19626
|
+
` sandbox: swept ${sweep.boxes.length} box(es), ${sweep.volumes.length} volume(s) in ${sweep.ms}ms, ${inputDirs} input dir(s)`
|
|
19627
|
+
);
|
|
19628
|
+
const slots = await parts.runtime.ensureSlots(settings.slots);
|
|
19629
|
+
const pool = parts.makePool(slots);
|
|
19630
|
+
const { token: claudeToken, source } = await (deps.readClaudeToken ?? ((s) => readSandboxClaudeToken(s)))(settings);
|
|
19631
|
+
deps.log(` sandbox: Claude token from ${source === "keychain" ? "the macOS keychain" : source === "file-override" ? `ARCHME_SANDBOX_CLAUDE_TOKEN_FILE (${settings.claudeTokenPath})` : settings.claudeTokenPath}`);
|
|
19632
|
+
const allSlots = Array.from({ length: MAX_SLOTS }, (_, i2) => slotFor(i2));
|
|
19633
|
+
const gates = await parts.startGates(allSlots, {
|
|
19634
|
+
mcpBaseUrl: deps.mcpBaseUrl,
|
|
19635
|
+
claudeToken,
|
|
19636
|
+
remint: deps.remint,
|
|
19637
|
+
log: deps.log
|
|
19150
19638
|
});
|
|
19639
|
+
let preflight;
|
|
19640
|
+
try {
|
|
19641
|
+
preflight = await parts.runtime.preflight();
|
|
19642
|
+
} catch (e) {
|
|
19643
|
+
await gates.close().catch(() => {
|
|
19644
|
+
});
|
|
19645
|
+
throw e;
|
|
19646
|
+
}
|
|
19647
|
+
if (!preflight.ok) {
|
|
19648
|
+
await gates.close().catch(() => {
|
|
19649
|
+
});
|
|
19650
|
+
throw new SandboxBootError(`sandbox preflight failed: ${failedChecks(preflight)}`);
|
|
19651
|
+
}
|
|
19652
|
+
const probe = deps.probe ?? ((slot) => runConfinementProbe({ runtime: parts.runtime, image: settings.image, log: deps.log }, slot, primaryLanIpv4(networkInterfaces())));
|
|
19653
|
+
let report;
|
|
19654
|
+
try {
|
|
19655
|
+
report = await probe(slots[0]);
|
|
19656
|
+
} catch (e) {
|
|
19657
|
+
report = { ok: false, slot: 0, failures: [`probe threw: ${e.message}`], outcomes: {}, ms: 0 };
|
|
19658
|
+
}
|
|
19659
|
+
preflight = {
|
|
19660
|
+
ok: preflight.ok && report.ok,
|
|
19661
|
+
checks: [...preflight.checks, { name: "confinement_probe", ok: report.ok, detail: report.ok ? void 0 : report.failures.join("; ") }]
|
|
19662
|
+
};
|
|
19663
|
+
deps.log(` sandbox: ${formatConfinementReport(report)}`);
|
|
19664
|
+
if (!report.ok) {
|
|
19665
|
+
if (settings.mode === "required") {
|
|
19666
|
+
await gates.close().catch(() => {
|
|
19667
|
+
});
|
|
19668
|
+
throw new SandboxBootError(`sandbox preflight failed: ${failedChecks(preflight)}`);
|
|
19669
|
+
}
|
|
19670
|
+
deps.log(" sandbox: shadow mode boots anyway; run `archme daemon sandbox setup` to install the confinement");
|
|
19671
|
+
}
|
|
19672
|
+
const monitor = new ConfinementMonitor({ mode: settings.mode, probe, pool, log: deps.log });
|
|
19673
|
+
return {
|
|
19674
|
+
host: { settings, runtime: parts.runtime, registry: parts.registry, gates, pool, io: parts.io },
|
|
19675
|
+
sweep,
|
|
19676
|
+
preflight,
|
|
19677
|
+
monitor
|
|
19678
|
+
};
|
|
19151
19679
|
}
|
|
19152
|
-
function
|
|
19153
|
-
return
|
|
19680
|
+
function failedChecks(p) {
|
|
19681
|
+
return p.checks.filter((c3) => !c3.ok).map((c3) => `${c3.name}${c3.detail ? ` (${c3.detail})` : ""}`).join(", ") || "unknown";
|
|
19154
19682
|
}
|
|
19155
|
-
|
|
19156
|
-
|
|
19683
|
+
var init_host = __esm({
|
|
19684
|
+
"src/daemon/sandbox/host.ts"() {
|
|
19685
|
+
"use strict";
|
|
19686
|
+
init_confinement();
|
|
19687
|
+
init_appleContainer();
|
|
19688
|
+
init_claudeToken();
|
|
19689
|
+
init_inputDir();
|
|
19690
|
+
init_types();
|
|
19691
|
+
init_claudeToken();
|
|
19692
|
+
}
|
|
19693
|
+
});
|
|
19694
|
+
|
|
19695
|
+
// src/daemon/sandbox/runSandboxedJob.ts
|
|
19696
|
+
function sandboxJobOf(job) {
|
|
19697
|
+
return {
|
|
19698
|
+
id: job.id,
|
|
19699
|
+
project_id: job.project_id,
|
|
19700
|
+
repo_owner: job.repo_owner,
|
|
19701
|
+
repo_name: job.repo_name,
|
|
19702
|
+
repo_branch: job.repo_branch,
|
|
19703
|
+
archme_branch: job.archme_branch,
|
|
19704
|
+
before_sha: job.before_sha,
|
|
19705
|
+
after_sha: job.after_sha,
|
|
19706
|
+
forced: job.forced,
|
|
19707
|
+
pr_number: job.pr_number ?? null,
|
|
19708
|
+
source_mode: job.source_mode,
|
|
19709
|
+
commit_message: job.commit_message ?? null,
|
|
19710
|
+
head_commit_timestamp: job.head_commit_timestamp ?? null,
|
|
19711
|
+
author_name: job.author_name ?? null,
|
|
19712
|
+
author_login: job.author_login ?? null
|
|
19713
|
+
};
|
|
19157
19714
|
}
|
|
19158
|
-
function
|
|
19159
|
-
|
|
19160
|
-
|
|
19161
|
-
|
|
19162
|
-
|
|
19163
|
-
|
|
19164
|
-
|
|
19165
|
-
|
|
19166
|
-
|
|
19715
|
+
async function runSandboxedJob(deps, job, opts) {
|
|
19716
|
+
const { slot } = opts;
|
|
19717
|
+
const t = { ...DEFAULT_SANDBOX_TIMEOUTS, ...opts.timeouts };
|
|
19718
|
+
const pollMs = opts.pollMs ?? 1e3;
|
|
19719
|
+
const now = deps.now ?? Date.now;
|
|
19720
|
+
const { settings } = deps;
|
|
19721
|
+
const volume = `archme-job-${job.id}`;
|
|
19722
|
+
const labels = { [JOB_LABEL]: job.id, [SLOT_LABEL]: String(slot.index) };
|
|
19723
|
+
const sjob = sandboxJobOf(job);
|
|
19724
|
+
const sandbox = {
|
|
19725
|
+
slot: slot.index,
|
|
19726
|
+
memory_peak_bytes: { clone: null, agent: null },
|
|
19727
|
+
model_requests: 0,
|
|
19728
|
+
upstream_429: 0,
|
|
19729
|
+
last_upstream_status: null,
|
|
19730
|
+
cap_hits: 0,
|
|
19731
|
+
mcp_requests: 0,
|
|
19732
|
+
remints: 0,
|
|
19733
|
+
egress_denied: 0,
|
|
19734
|
+
first_egress_denied: null,
|
|
19735
|
+
clone_ms: null,
|
|
19736
|
+
box_ms: 0
|
|
19737
|
+
};
|
|
19738
|
+
const durations = {};
|
|
19739
|
+
const metrics = { sandbox, durations_ms: durations };
|
|
19740
|
+
const liveBoxes = /* @__PURE__ */ new Set();
|
|
19741
|
+
const inputCleanups = [];
|
|
19742
|
+
let volumeTouched = false;
|
|
19743
|
+
let agentStarted = false;
|
|
19744
|
+
const readCounters = () => {
|
|
19745
|
+
const c3 = deps.gates.countersFor(slot);
|
|
19746
|
+
if (!c3) return;
|
|
19747
|
+
sandbox.model_requests = c3.model_requests;
|
|
19748
|
+
sandbox.upstream_429 = c3.upstream_429;
|
|
19749
|
+
sandbox.last_upstream_status = c3.last_upstream_status;
|
|
19750
|
+
sandbox.cap_hits = c3.cap_hits;
|
|
19751
|
+
sandbox.mcp_requests = c3.mcp_requests;
|
|
19752
|
+
sandbox.remints = c3.remints;
|
|
19753
|
+
sandbox.egress_denied = c3.egress_denied;
|
|
19754
|
+
sandbox.first_egress_denied = c3.first_egress_denied;
|
|
19755
|
+
};
|
|
19756
|
+
const failed = (step, reason, stderr = "") => {
|
|
19757
|
+
deps.log(`sandbox ${step} step failed: ${reason}`);
|
|
19758
|
+
return { kind: "step_failed", step, reason, stderr, metrics };
|
|
19759
|
+
};
|
|
19760
|
+
const currentKillCause = () => {
|
|
19761
|
+
if (deps.isCancelled(job.id)) return "cancel";
|
|
19762
|
+
if (deps.killReason(job.id) === "shutdown") return "shutdown";
|
|
19763
|
+
if (deps.isLeaseLost(job.id)) return "lease_lost";
|
|
19764
|
+
return null;
|
|
19765
|
+
};
|
|
19766
|
+
const writeInput = async (step, input) => {
|
|
19767
|
+
const w = await deps.writeInputDir(step, input);
|
|
19768
|
+
let done = false;
|
|
19769
|
+
const cleanup = async () => {
|
|
19770
|
+
if (done) return;
|
|
19771
|
+
done = true;
|
|
19772
|
+
await w.cleanup().catch(() => {
|
|
19773
|
+
});
|
|
19167
19774
|
};
|
|
19775
|
+
inputCleanups.push(cleanup);
|
|
19776
|
+
return { dir: w.dir, cleanup };
|
|
19168
19777
|
};
|
|
19169
|
-
|
|
19170
|
-
|
|
19171
|
-
|
|
19172
|
-
|
|
19778
|
+
const runBox = async (spec2, timeoutMs) => {
|
|
19779
|
+
if (deps.isCancelled(job.id)) return { kind: "refused", reason: "cancel" };
|
|
19780
|
+
if (!await deps.registry.beginStep(job.id, spec2.name)) return { kind: "refused", reason: "shutting_down" };
|
|
19781
|
+
liveBoxes.add(spec2.name);
|
|
19782
|
+
const started = now();
|
|
19783
|
+
let killCause = null;
|
|
19784
|
+
const kill = (cause) => {
|
|
19785
|
+
if (killCause) return;
|
|
19786
|
+
killCause = cause;
|
|
19787
|
+
deps.log(`killing ${spec2.name} (${cause})`);
|
|
19788
|
+
if (cause === "lease_lost") deps.gates.clearJob(slot);
|
|
19789
|
+
void deps.runtime.kill(spec2.name).catch((e) => deps.log(`kill ${spec2.name} failed: ${e.message}`));
|
|
19790
|
+
};
|
|
19791
|
+
if (spec2.args[0] === "agent") agentStarted = true;
|
|
19792
|
+
const handle = await deps.runtime.runStep(spec2);
|
|
19793
|
+
const timer = setTimeout(() => kill("timeout"), timeoutMs);
|
|
19794
|
+
const poll = setInterval(() => {
|
|
19795
|
+
const c3 = currentKillCause();
|
|
19796
|
+
if (c3) kill(c3);
|
|
19797
|
+
}, pollMs);
|
|
19798
|
+
try {
|
|
19799
|
+
const exit = await handle.done;
|
|
19800
|
+
liveBoxes.delete(spec2.name);
|
|
19801
|
+
deps.registry.endStep(job.id, spec2.name);
|
|
19802
|
+
if (!killCause && exit.killed) killCause = currentKillCause() ?? "shutdown";
|
|
19803
|
+
return { kind: "ran", exit, killCause, ms: now() - started };
|
|
19804
|
+
} finally {
|
|
19805
|
+
clearTimeout(timer);
|
|
19806
|
+
clearInterval(poll);
|
|
19807
|
+
}
|
|
19808
|
+
};
|
|
19809
|
+
const spec = (step, env, inputDir) => ({
|
|
19810
|
+
name: `archme-job-${job.id}-${step}`,
|
|
19811
|
+
image: settings.image,
|
|
19812
|
+
args: [step],
|
|
19813
|
+
network: slot,
|
|
19814
|
+
env,
|
|
19815
|
+
inputDir,
|
|
19816
|
+
volume,
|
|
19817
|
+
limits: { cpus: settings.boxCpus, memoryMiB: settings.boxMemoryMiB },
|
|
19818
|
+
labels: { ...labels }
|
|
19819
|
+
});
|
|
19820
|
+
const runBoxes = async (pre) => {
|
|
19821
|
+
deps.registry.setVolume(job.id, volume);
|
|
19822
|
+
volumeTouched = true;
|
|
19823
|
+
await deps.runtime.createVolume(volume, settings.volumeGiB, labels);
|
|
19824
|
+
await deps.runtime.initVolume(volume, BOX_UID, labels, settings.image, slot);
|
|
19825
|
+
deps.gates.bindJob(slot, {
|
|
19826
|
+
jobId: job.id,
|
|
19827
|
+
maxModelRequests: settings.maxModelRequests,
|
|
19828
|
+
mcp: {
|
|
19829
|
+
token: pre.jobToken.token,
|
|
19830
|
+
expiresAt: pre.jobToken.expires_at ? Date.parse(pre.jobToken.expires_at) || null : null,
|
|
19831
|
+
projectSlug: pre.projectSlug,
|
|
19832
|
+
branch: job.archme_branch
|
|
19833
|
+
}
|
|
19834
|
+
});
|
|
19835
|
+
const cloneIn = await writeInput("clone", {
|
|
19836
|
+
kind: "clone",
|
|
19837
|
+
job: sjob,
|
|
19838
|
+
marker: pre.marker,
|
|
19839
|
+
credentials: {
|
|
19840
|
+
https_url: pre.credentials.https_url,
|
|
19841
|
+
username: pre.credentials.username,
|
|
19842
|
+
password: pre.credentials.password
|
|
19843
|
+
}
|
|
19844
|
+
});
|
|
19845
|
+
deps.gates.setStep(slot, "clone");
|
|
19846
|
+
const clone = await runBox(spec("clone", deps.cloneBoxEnv(slot.gateway), cloneIn.dir), t.cloneMs);
|
|
19847
|
+
await cloneIn.cleanup();
|
|
19848
|
+
if (clone.kind === "refused") return failed("clone", clone.reason);
|
|
19849
|
+
sandbox.clone_ms = clone.ms;
|
|
19850
|
+
sandbox.box_ms += clone.ms;
|
|
19851
|
+
durations.sandbox_clone = clone.ms;
|
|
19852
|
+
if (clone.killCause) return failed("clone", clone.killCause, clone.exit.stderr);
|
|
19853
|
+
const cp = deps.parseResultLine(clone.exit.stdout, clone.exit.nonce);
|
|
19854
|
+
if (!cp.ok || cp.result.kind !== "clone") {
|
|
19855
|
+
deps.log(`clone result line: ${cp.ok ? `unexpected kind ${cp.result.kind}` : cp.reason}`);
|
|
19856
|
+
return failed("clone", "result_malformed", clone.exit.stderr);
|
|
19857
|
+
}
|
|
19858
|
+
const cr = cp.result;
|
|
19859
|
+
sandbox.memory_peak_bytes.clone = cr.memoryPeakBytes;
|
|
19860
|
+
for (const [k, v] of Object.entries(cr.durations_ms ?? {})) durations[`clone_${k}`] = v;
|
|
19861
|
+
if (cr.outcome === "skip") return { kind: "skip", metrics };
|
|
19862
|
+
if (cr.outcome === "rejected") return { kind: "rejected", message: cr.error ?? "job rejected by the clone step", metrics };
|
|
19863
|
+
if (cr.error || clone.exit.exitCode !== 0 || !cr.diff) {
|
|
19864
|
+
return failed("clone", cr.error ?? `clone step exited ${clone.exit.exitCode}`, clone.exit.stderr);
|
|
19865
|
+
}
|
|
19866
|
+
Object.assign(metrics, {
|
|
19867
|
+
mode: job.source_mode === "pr_update" ? "pr_update" : "diff",
|
|
19868
|
+
files: cr.diff.files,
|
|
19869
|
+
bytes: cr.diff.bytes,
|
|
19870
|
+
materialized: cr.diff.materialized,
|
|
19871
|
+
range: cr.diff.rangeLabel,
|
|
19872
|
+
model: pre.model
|
|
19873
|
+
});
|
|
19874
|
+
const claudeMs = cr.diff.materialized ? t.largeDiffClaudeMs : t.claudeMs;
|
|
19875
|
+
const agentIn = await writeInput("agent", {
|
|
19876
|
+
kind: "agent",
|
|
19877
|
+
job: sjob,
|
|
19878
|
+
projectSlug: pre.projectSlug,
|
|
19879
|
+
seedArch: pre.seedArch,
|
|
19880
|
+
repoScope: pre.repoScope,
|
|
19881
|
+
repoContainer: pre.repoContainer,
|
|
19882
|
+
bugsFolderId: pre.bugsFolderId,
|
|
19883
|
+
model: pre.model,
|
|
19884
|
+
timeoutMs: claudeMs,
|
|
19885
|
+
gateway: slot.gateway
|
|
19886
|
+
});
|
|
19887
|
+
deps.gates.setStep(slot, "agent");
|
|
19888
|
+
const agent = await runBox(spec("agent", deps.agentBoxEnv(slot.gateway), agentIn.dir), claudeMs + t.graceMs);
|
|
19889
|
+
await agentIn.cleanup();
|
|
19890
|
+
if (agent.kind === "refused") return failed("agent", agent.reason);
|
|
19891
|
+
sandbox.box_ms += agent.ms;
|
|
19892
|
+
durations.sandbox_agent = agent.ms;
|
|
19893
|
+
readCounters();
|
|
19894
|
+
if (agent.killCause) return failed("agent", agent.killCause, agent.exit.stderr);
|
|
19895
|
+
const ap = deps.parseResultLine(agent.exit.stdout, agent.exit.nonce);
|
|
19896
|
+
if (!ap.ok || ap.result.kind !== "agent") {
|
|
19897
|
+
deps.log(`agent result line: ${ap.ok ? `unexpected kind ${ap.result.kind}` : ap.reason}`);
|
|
19898
|
+
return failed("agent", "result_malformed", agent.exit.stderr);
|
|
19899
|
+
}
|
|
19900
|
+
const ar = ap.result;
|
|
19901
|
+
sandbox.memory_peak_bytes.agent = ar.memoryPeakBytes;
|
|
19902
|
+
for (const [k, v] of Object.entries(ar.durations_ms ?? {})) durations[`agent_${k}`] = v;
|
|
19903
|
+
if (ar.seed) metrics.seed = ar.seed;
|
|
19904
|
+
if (ar.error) return failed("agent", ar.error, agent.exit.stderr);
|
|
19173
19905
|
return {
|
|
19174
|
-
|
|
19175
|
-
|
|
19176
|
-
|
|
19177
|
-
|
|
19178
|
-
|
|
19179
|
-
|
|
19180
|
-
|
|
19181
|
-
}))
|
|
19906
|
+
kind: "claude",
|
|
19907
|
+
exitCode: ar.exitCode,
|
|
19908
|
+
signal: ar.signal,
|
|
19909
|
+
claudeStdout: ar.claudeStdout,
|
|
19910
|
+
stderr: agent.exit.stderr,
|
|
19911
|
+
affected: ar.affected,
|
|
19912
|
+
metrics
|
|
19182
19913
|
};
|
|
19183
19914
|
};
|
|
19915
|
+
try {
|
|
19916
|
+
const pre = await deps.prefetch(job);
|
|
19917
|
+
try {
|
|
19918
|
+
return await runBoxes(pre);
|
|
19919
|
+
} catch (e) {
|
|
19920
|
+
if (e instanceof SandboxInfraError) throw e;
|
|
19921
|
+
throw new SandboxInfraError(e instanceof Error ? e.message : String(e), agentStarted);
|
|
19922
|
+
}
|
|
19923
|
+
} finally {
|
|
19924
|
+
const step = async (what, fn) => {
|
|
19925
|
+
try {
|
|
19926
|
+
await fn();
|
|
19927
|
+
} catch (e) {
|
|
19928
|
+
deps.log(`sandbox cleanup (${what}) failed: ${e.message}`);
|
|
19929
|
+
}
|
|
19930
|
+
};
|
|
19931
|
+
for (const name of liveBoxes) await step(`kill ${name}`, () => deps.runtime.kill(name));
|
|
19932
|
+
if (volumeTouched) await step("remove volume", () => deps.runtime.removeVolume(volume));
|
|
19933
|
+
await step("counters", readCounters);
|
|
19934
|
+
await step("clear gates", () => deps.gates.clearJob(slot));
|
|
19935
|
+
await step("free slot", () => deps.pool.free(slot.index));
|
|
19936
|
+
for (const c3 of inputCleanups) await step("input dir", c3);
|
|
19937
|
+
await step("release", () => deps.registry.release(job.id));
|
|
19938
|
+
}
|
|
19184
19939
|
}
|
|
19185
|
-
var
|
|
19186
|
-
var
|
|
19187
|
-
"src/daemon/sandbox/
|
|
19940
|
+
var DEFAULT_SANDBOX_TIMEOUTS, SandboxInfraError;
|
|
19941
|
+
var init_runSandboxedJob = __esm({
|
|
19942
|
+
"src/daemon/sandbox/runSandboxedJob.ts"() {
|
|
19188
19943
|
"use strict";
|
|
19189
|
-
init_execa();
|
|
19190
|
-
init_sweep();
|
|
19191
19944
|
init_types();
|
|
19192
|
-
|
|
19193
|
-
|
|
19194
|
-
|
|
19195
|
-
|
|
19196
|
-
|
|
19197
|
-
|
|
19198
|
-
|
|
19199
|
-
|
|
19200
|
-
|
|
19201
|
-
|
|
19202
|
-
|
|
19203
|
-
TailBuffer = class {
|
|
19204
|
-
constructor(cap) {
|
|
19205
|
-
this.cap = cap;
|
|
19206
|
-
}
|
|
19207
|
-
chunks = [];
|
|
19208
|
-
size = 0;
|
|
19209
|
-
truncated = false;
|
|
19210
|
-
push(chunk) {
|
|
19211
|
-
this.chunks.push(chunk);
|
|
19212
|
-
this.size += chunk.length;
|
|
19213
|
-
while (this.size > this.cap && this.chunks.length > 0) {
|
|
19214
|
-
const head = this.chunks[0];
|
|
19215
|
-
const over = this.size - this.cap;
|
|
19216
|
-
this.truncated = true;
|
|
19217
|
-
if (head.length <= over) {
|
|
19218
|
-
this.chunks.shift();
|
|
19219
|
-
this.size -= head.length;
|
|
19220
|
-
} else {
|
|
19221
|
-
this.chunks[0] = head.subarray(over);
|
|
19222
|
-
this.size -= over;
|
|
19223
|
-
}
|
|
19224
|
-
}
|
|
19225
|
-
}
|
|
19226
|
-
toString() {
|
|
19227
|
-
let buf = Buffer.concat(this.chunks);
|
|
19228
|
-
if (this.truncated) {
|
|
19229
|
-
let i2 = 0;
|
|
19230
|
-
while (i2 < buf.length && i2 < 3 && (buf[i2] & 192) === 128) i2++;
|
|
19231
|
-
buf = buf.subarray(i2);
|
|
19232
|
-
}
|
|
19233
|
-
return buf.toString("utf8");
|
|
19234
|
-
}
|
|
19235
|
-
};
|
|
19236
|
-
AppleContainerRuntime = class {
|
|
19237
|
-
constructor(opts = {}) {
|
|
19238
|
-
this.opts = opts;
|
|
19239
|
-
this.exec = opts.exec ?? defaultExec2(opts.bin);
|
|
19240
|
-
this.spawn = opts.spawn ?? defaultSpawn(opts.bin);
|
|
19241
|
-
}
|
|
19242
|
-
name = "apple-container";
|
|
19243
|
-
exec;
|
|
19244
|
-
spawn;
|
|
19245
|
-
/** Boxes `kill()` was called for while their step was running. */
|
|
19246
|
-
killedNames = /* @__PURE__ */ new Set();
|
|
19247
|
-
running = /* @__PURE__ */ new Set();
|
|
19248
|
-
async must(args, what) {
|
|
19249
|
-
const r = await this.exec(args);
|
|
19250
|
-
if (r.exitCode !== 0) throw new Error(`sandbox: ${what} failed (exit ${r.exitCode}): ${r.stderr.trim()}`);
|
|
19251
|
-
return r;
|
|
19252
|
-
}
|
|
19253
|
-
async preflight() {
|
|
19254
|
-
const checks = [];
|
|
19255
|
-
const status = await this.exec(["system", "status"]);
|
|
19256
|
-
const up = status.exitCode === 0 && /^status\s+running\b/m.test(status.stdout);
|
|
19257
|
-
checks.push({ name: "runtime_status", ok: up, detail: up ? void 0 : (status.stderr || status.stdout).trim() });
|
|
19258
|
-
if (this.opts.image) {
|
|
19259
|
-
let img = await this.exec(["image", "inspect", this.opts.image]);
|
|
19260
|
-
if (img.exitCode !== 0 && up && this.opts.image.includes("@sha256:")) {
|
|
19261
|
-
const pull = await this.exec(["image", "pull", this.opts.image]);
|
|
19262
|
-
img = pull.exitCode === 0 ? await this.exec(["image", "inspect", this.opts.image]) : pull;
|
|
19263
|
-
}
|
|
19264
|
-
checks.push({ name: "image_present", ok: img.exitCode === 0, detail: img.exitCode === 0 ? void 0 : img.stderr.trim() });
|
|
19265
|
-
} else {
|
|
19266
|
-
checks.push({ name: "image_present", ok: false, detail: "no image configured" });
|
|
19267
|
-
}
|
|
19268
|
-
const want = this.opts.slots ?? 0;
|
|
19269
|
-
if (up) {
|
|
19270
|
-
const nets = await this.listNetworks();
|
|
19271
|
-
const missing = [];
|
|
19272
|
-
for (let i2 = 0; i2 < want; i2++) {
|
|
19273
|
-
const s = slotFor(i2);
|
|
19274
|
-
const n2 = nets.find((x) => x.name === s.network);
|
|
19275
|
-
if (!n2 || n2.subnet !== s.subnet) missing.push(n2 ? `${s.network} (subnet ${n2.subnet})` : s.network);
|
|
19276
|
-
}
|
|
19277
|
-
checks.push({ name: "slot_networks", ok: missing.length === 0, detail: missing.length ? `missing/wrong: ${missing.join(", ")}` : void 0 });
|
|
19278
|
-
} else {
|
|
19279
|
-
checks.push({ name: "slot_networks", ok: false, detail: "runtime not running" });
|
|
19280
|
-
}
|
|
19281
|
-
checks.push({ name: "pf_anchor", ok: true, detail: "skipped (19.3)" });
|
|
19282
|
-
checks.push({ name: "ipv6_probe", ok: true, detail: "skipped (19.3)" });
|
|
19283
|
-
return { ok: checks.every((c3) => c3.ok), checks };
|
|
19284
|
-
}
|
|
19285
|
-
async listNetworks() {
|
|
19286
|
-
const r = await this.must(["network", "ls", "--format", "json"], "network ls");
|
|
19287
|
-
return parseNetworkList(r.stdout);
|
|
19288
|
-
}
|
|
19289
|
-
async ensureSlots(n2) {
|
|
19290
|
-
if (!Number.isInteger(n2) || n2 < 1 || n2 > MAX_SLOTS) throw new Error(`sandbox: slot count out of range: ${n2}`);
|
|
19291
|
-
const slots = Array.from({ length: n2 }, (_, i2) => slotFor(i2));
|
|
19292
|
-
let nets = await this.listNetworks();
|
|
19293
|
-
for (const s of slots) {
|
|
19294
|
-
const existing = nets.find((x) => x.name === s.network);
|
|
19295
|
-
if (existing) {
|
|
19296
|
-
if (existing.subnet !== s.subnet) {
|
|
19297
|
-
throw new Error(`sandbox: ${s.network} exists with subnet ${existing.subnet}, expected ${s.subnet}`);
|
|
19298
|
-
}
|
|
19299
|
-
continue;
|
|
19300
|
-
}
|
|
19301
|
-
const r = await this.exec(buildEnsureNetworkArgs(s));
|
|
19302
|
-
if (r.exitCode !== 0) {
|
|
19303
|
-
nets = await this.listNetworks();
|
|
19304
|
-
const now = nets.find((x) => x.name === s.network);
|
|
19305
|
-
if (!now || now.subnet !== s.subnet) {
|
|
19306
|
-
throw new Error(`sandbox: creating ${s.network} failed: ${r.stderr.trim()}`);
|
|
19307
|
-
}
|
|
19308
|
-
}
|
|
19309
|
-
}
|
|
19310
|
-
return slots;
|
|
19311
|
-
}
|
|
19312
|
-
async createVolume(name, sizeGiB, labels) {
|
|
19313
|
-
await this.must(buildCreateVolumeArgs(name, sizeGiB, labels), `volume create ${name}`);
|
|
19314
|
-
}
|
|
19315
|
-
async initVolume(name, uid, labels, image) {
|
|
19316
|
-
await this.must(buildInitVolumeArgs(name, uid, labels, image), `volume init ${name}`);
|
|
19317
|
-
}
|
|
19318
|
-
async runStep(spec) {
|
|
19319
|
-
const args = buildRunArgs(spec, this.opts.homeDir);
|
|
19320
|
-
const name = spec.name;
|
|
19321
|
-
this.running.add(name);
|
|
19322
|
-
this.killedNames.delete(name);
|
|
19323
|
-
const out = new TailBuffer(STDOUT_CAP_BYTES);
|
|
19324
|
-
const err4 = new TailBuffer(STDERR_CAP_BYTES);
|
|
19325
|
-
const nonce = randomBytes2(16).toString("hex");
|
|
19326
|
-
const proc = this.spawn(args);
|
|
19327
|
-
if (proc.stdin) {
|
|
19328
|
-
proc.stdin.on("error", () => {
|
|
19329
|
-
});
|
|
19330
|
-
proc.stdin.end(`${nonce}
|
|
19331
|
-
`);
|
|
19332
|
-
}
|
|
19333
|
-
const tee = (stream, buf, which) => new Promise((resolve7) => {
|
|
19334
|
-
if (!stream) return resolve7();
|
|
19335
|
-
stream.on("data", (c3) => {
|
|
19336
|
-
const b = typeof c3 === "string" ? Buffer.from(c3) : c3;
|
|
19337
|
-
buf.push(b);
|
|
19338
|
-
this.opts.onOutput?.(name, which, b);
|
|
19339
|
-
});
|
|
19340
|
-
stream.on("end", () => resolve7());
|
|
19341
|
-
stream.on("close", () => resolve7());
|
|
19342
|
-
stream.on("error", () => resolve7());
|
|
19343
|
-
});
|
|
19344
|
-
const done = (async () => {
|
|
19345
|
-
const [exit] = await Promise.all([proc.exited, tee(proc.stdout, out, "stdout"), tee(proc.stderr, err4, "stderr")]);
|
|
19346
|
-
const killed = this.killedNames.has(name);
|
|
19347
|
-
this.killedNames.delete(name);
|
|
19348
|
-
this.running.delete(name);
|
|
19349
|
-
const stderr = err4.toString();
|
|
19350
|
-
return {
|
|
19351
|
-
exitCode: exit.exitCode,
|
|
19352
|
-
signal: exit.signal,
|
|
19353
|
-
stdout: out.toString(),
|
|
19354
|
-
stderr: exit.error ? `${stderr}
|
|
19355
|
-
${exit.error}`.trim() : stderr,
|
|
19356
|
-
killed,
|
|
19357
|
-
nonce
|
|
19358
|
-
};
|
|
19359
|
-
})();
|
|
19360
|
-
return { name, done };
|
|
19361
|
-
}
|
|
19362
|
-
/** Not-found and already-stopped both count as success: the box is not running. */
|
|
19363
|
-
async kill(name) {
|
|
19364
|
-
assertName("box", name);
|
|
19365
|
-
if (this.running.has(name)) this.killedNames.add(name);
|
|
19366
|
-
const r = await this.exec(["kill", name]);
|
|
19367
|
-
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr) && !isBoxStopped(r.stderr)) {
|
|
19368
|
-
throw new Error(`sandbox: kill ${name} failed: ${r.stderr.trim()}`);
|
|
19369
|
-
}
|
|
19370
|
-
}
|
|
19371
|
-
async stop(name, graceSeconds) {
|
|
19372
|
-
assertName("box", name);
|
|
19373
|
-
const t = Math.max(0, Math.floor(graceSeconds));
|
|
19374
|
-
const r = await this.exec(["stop", "-t", String(t), name]);
|
|
19375
|
-
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr) && !isBoxStopped(r.stderr)) {
|
|
19376
|
-
throw new Error(`sandbox: stop ${name} failed: ${r.stderr.trim()}`);
|
|
19377
|
-
}
|
|
19378
|
-
}
|
|
19379
|
-
/** `container rm`; not-found counts as removed (a `--rm` box is gone once stopped). */
|
|
19380
|
-
async remove(name) {
|
|
19381
|
-
assertName("box", name);
|
|
19382
|
-
const r = await this.exec(["rm", name]);
|
|
19383
|
-
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr)) {
|
|
19384
|
-
throw new Error(`sandbox: rm ${name} failed: ${r.stderr.trim()}`);
|
|
19385
|
-
}
|
|
19386
|
-
}
|
|
19387
|
-
async removeVolume(name) {
|
|
19388
|
-
assertName("volume", name);
|
|
19389
|
-
const r = await this.exec(["volume", "rm", name]);
|
|
19390
|
-
if (r.exitCode === 0) return;
|
|
19391
|
-
const still = (await this.listVolumes()).some((v) => v.name === name);
|
|
19392
|
-
if (still) throw new Error(`sandbox: volume rm ${name} failed: ${r.stderr.trim()}`);
|
|
19393
|
-
}
|
|
19394
|
-
async list() {
|
|
19395
|
-
const r = await this.must(["ls", "-a", "--format", "json"], "ls");
|
|
19396
|
-
return parseBoxList(r.stdout);
|
|
19397
|
-
}
|
|
19398
|
-
async listVolumes() {
|
|
19399
|
-
const r = await this.must(["volume", "ls", "--format", "json"], "volume ls");
|
|
19400
|
-
return parseVolumeList(r.stdout);
|
|
19945
|
+
DEFAULT_SANDBOX_TIMEOUTS = {
|
|
19946
|
+
claudeMs: 30 * 60 * 1e3,
|
|
19947
|
+
largeDiffClaudeMs: 45 * 60 * 1e3,
|
|
19948
|
+
graceMs: 2 * 60 * 1e3,
|
|
19949
|
+
cloneMs: 10 * 60 * 1e3
|
|
19950
|
+
};
|
|
19951
|
+
SandboxInfraError = class extends Error {
|
|
19952
|
+
constructor(message2, agentStarted) {
|
|
19953
|
+
super(message2);
|
|
19954
|
+
this.agentStarted = agentStarted;
|
|
19955
|
+
this.name = "SandboxInfraError";
|
|
19401
19956
|
}
|
|
19402
|
-
|
|
19403
|
-
|
|
19957
|
+
};
|
|
19958
|
+
}
|
|
19959
|
+
});
|
|
19960
|
+
|
|
19961
|
+
// src/daemon/sandbox/classifySandboxExit.ts
|
|
19962
|
+
function classifySandboxExit(input) {
|
|
19963
|
+
const { counters, exitCode, stderr, stepFailure } = input;
|
|
19964
|
+
if ((counters?.cap_hits ?? 0) > 0 || stderr.includes(SANDBOX_CAP_BODY)) {
|
|
19965
|
+
return {
|
|
19966
|
+
kind: "terminal",
|
|
19967
|
+
reason: SANDBOX_REQUEST_CAP_REASON,
|
|
19968
|
+
message: `${SANDBOX_REQUEST_CAP_REASON}: the job exceeded the sandbox model-request cap` + (counters ? ` (${counters.model_requests} requests, ${counters.cap_hits} refused)` : "")
|
|
19969
|
+
};
|
|
19970
|
+
}
|
|
19971
|
+
if (stepFailure == null && exitCode !== null && exitCode !== 0 && counters?.last_upstream_status === 429) {
|
|
19972
|
+
return { kind: "upstream_rate_limit" };
|
|
19973
|
+
}
|
|
19974
|
+
return { kind: "normal" };
|
|
19975
|
+
}
|
|
19976
|
+
function routeSandboxFailure(verdict, planFor, nowMs = Date.now()) {
|
|
19977
|
+
if (verdict?.kind === "terminal") return { kind: "terminal", message: verdict.message };
|
|
19978
|
+
const plan = planFor();
|
|
19979
|
+
if (plan) return { kind: "defer", plan };
|
|
19980
|
+
if (verdict?.kind === "upstream_rate_limit") {
|
|
19981
|
+
return {
|
|
19982
|
+
kind: "defer",
|
|
19983
|
+
plan: { parsed: false, pauseUntil: new Date(nowMs + RATE_LIMIT_TENTATIVE_PAUSE_MS), info: null }
|
|
19984
|
+
};
|
|
19985
|
+
}
|
|
19986
|
+
return { kind: "fail" };
|
|
19987
|
+
}
|
|
19988
|
+
var SANDBOX_REQUEST_CAP_REASON, SandboxExitError;
|
|
19989
|
+
var init_classifySandboxExit = __esm({
|
|
19990
|
+
"src/daemon/sandbox/classifySandboxExit.ts"() {
|
|
19991
|
+
"use strict";
|
|
19992
|
+
init_rateLimit();
|
|
19993
|
+
init_types();
|
|
19994
|
+
SANDBOX_REQUEST_CAP_REASON = "sandbox_request_cap";
|
|
19995
|
+
SandboxExitError = class extends Error {
|
|
19996
|
+
constructor(message2, verdict) {
|
|
19997
|
+
super(message2);
|
|
19998
|
+
this.verdict = verdict;
|
|
19999
|
+
this.name = "SandboxExitError";
|
|
19404
20000
|
}
|
|
19405
20001
|
};
|
|
19406
20002
|
}
|
|
@@ -19540,6 +20136,47 @@ function createGateServer(opts) {
|
|
|
19540
20136
|
fail2(e);
|
|
19541
20137
|
}
|
|
19542
20138
|
});
|
|
20139
|
+
if (opts.connect) {
|
|
20140
|
+
const connect = opts.connect;
|
|
20141
|
+
server.on("connect", (req, socket, head) => {
|
|
20142
|
+
const target = (req.url ?? "").slice(0, 300);
|
|
20143
|
+
const slot = opts.slotMap.resolve(req.socket.localAddress, req.socket.remoteAddress);
|
|
20144
|
+
socket.on("error", () => {
|
|
20145
|
+
});
|
|
20146
|
+
if (!slot) {
|
|
20147
|
+
log(`[${opts.name}] CONNECT ${target} drop job=-`);
|
|
20148
|
+
socket.destroy();
|
|
20149
|
+
return;
|
|
20150
|
+
}
|
|
20151
|
+
const binding = opts.slotMap.binding(slot.index);
|
|
20152
|
+
if (!binding) {
|
|
20153
|
+
log(`[${opts.name}] CONNECT ${target} 403 job=- slot=${slot.index} idle`);
|
|
20154
|
+
socket.end("HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n");
|
|
20155
|
+
return;
|
|
20156
|
+
}
|
|
20157
|
+
let logged = false;
|
|
20158
|
+
const ctx = {
|
|
20159
|
+
slot,
|
|
20160
|
+
binding,
|
|
20161
|
+
path: target,
|
|
20162
|
+
done(status, note) {
|
|
20163
|
+
if (logged) return;
|
|
20164
|
+
logged = true;
|
|
20165
|
+
log(`[${opts.name}] CONNECT ${target} ${status} job=${binding.jobId}${note ? ` ${note}` : ""}`);
|
|
20166
|
+
}
|
|
20167
|
+
};
|
|
20168
|
+
const fail2 = (e) => {
|
|
20169
|
+
ctx.done(502, `handler error: ${e?.name ?? "error"}`);
|
|
20170
|
+
if (socket.writable) socket.end("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n");
|
|
20171
|
+
else socket.destroy();
|
|
20172
|
+
};
|
|
20173
|
+
try {
|
|
20174
|
+
Promise.resolve(connect(req, socket, head, ctx)).catch(fail2);
|
|
20175
|
+
} catch (e) {
|
|
20176
|
+
fail2(e);
|
|
20177
|
+
}
|
|
20178
|
+
});
|
|
20179
|
+
}
|
|
19543
20180
|
server.requestTimeout = 0;
|
|
19544
20181
|
server.headersTimeout = 6e4;
|
|
19545
20182
|
return {
|
|
@@ -19706,12 +20343,121 @@ var init_credentialGate = __esm({
|
|
|
19706
20343
|
}
|
|
19707
20344
|
});
|
|
19708
20345
|
|
|
20346
|
+
// src/daemon/sandbox/githubGate.ts
|
|
20347
|
+
import net from "node:net";
|
|
20348
|
+
function parseConnectTarget(authority) {
|
|
20349
|
+
const i2 = authority.lastIndexOf(":");
|
|
20350
|
+
if (i2 <= 0) return { ok: false };
|
|
20351
|
+
const portStr = authority.slice(i2 + 1);
|
|
20352
|
+
let host = authority.slice(0, i2).toLowerCase();
|
|
20353
|
+
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
20354
|
+
if (host.endsWith(".")) host = host.slice(0, -1);
|
|
20355
|
+
if (portStr !== "443" || net.isIP(host) !== 0 || !GITHUB_ALLOWED_HOSTS.has(host)) return { ok: false };
|
|
20356
|
+
return { ok: true, host, port: 443 };
|
|
20357
|
+
}
|
|
20358
|
+
var GITHUB_ALLOWED_HOSTS, FORBIDDEN, BAD_GATEWAY, GithubGate;
|
|
20359
|
+
var init_githubGate = __esm({
|
|
20360
|
+
"src/daemon/sandbox/githubGate.ts"() {
|
|
20361
|
+
"use strict";
|
|
20362
|
+
init_gateServer();
|
|
20363
|
+
init_types();
|
|
20364
|
+
GITHUB_ALLOWED_HOSTS = /* @__PURE__ */ new Set(["github.com", "codeload.github.com"]);
|
|
20365
|
+
FORBIDDEN = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n";
|
|
20366
|
+
BAD_GATEWAY = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n";
|
|
20367
|
+
GithubGate = class {
|
|
20368
|
+
gate;
|
|
20369
|
+
dial;
|
|
20370
|
+
/** Open tunnels per slot: a clone-step tunnel must not outlive the clone step. */
|
|
20371
|
+
tunnels = /* @__PURE__ */ new Map();
|
|
20372
|
+
constructor(opts) {
|
|
20373
|
+
this.dial = opts.dial ?? ((host, port) => net.connect({ host, port }));
|
|
20374
|
+
this.gate = createGateServer({
|
|
20375
|
+
name: "github-gate",
|
|
20376
|
+
slotMap: opts.slotMap,
|
|
20377
|
+
host: opts.host,
|
|
20378
|
+
port: opts.port ?? GATE_PORTS.github,
|
|
20379
|
+
logger: opts.logger,
|
|
20380
|
+
handler: (_req, res, ctx) => {
|
|
20381
|
+
ctx.done(404);
|
|
20382
|
+
res.writeHead(404).end();
|
|
20383
|
+
},
|
|
20384
|
+
connect: (req, socket, head, ctx) => this.tunnel(req, socket, head, ctx)
|
|
20385
|
+
});
|
|
20386
|
+
}
|
|
20387
|
+
listen() {
|
|
20388
|
+
return this.gate.listen();
|
|
20389
|
+
}
|
|
20390
|
+
async close() {
|
|
20391
|
+
for (const index of [...this.tunnels.keys()]) this.closeTunnels(index);
|
|
20392
|
+
await this.gate.close();
|
|
20393
|
+
}
|
|
20394
|
+
/** The slot's step moved (clone → agent): its tunnels go. */
|
|
20395
|
+
onStepChange(index) {
|
|
20396
|
+
this.closeTunnels(index);
|
|
20397
|
+
}
|
|
20398
|
+
onClear(index) {
|
|
20399
|
+
this.closeTunnels(index);
|
|
20400
|
+
}
|
|
20401
|
+
closeTunnels(index) {
|
|
20402
|
+
const set = this.tunnels.get(index);
|
|
20403
|
+
if (!set) return;
|
|
20404
|
+
this.tunnels.delete(index);
|
|
20405
|
+
for (const destroy of set) destroy();
|
|
20406
|
+
}
|
|
20407
|
+
tunnel(_req, socket, head, ctx) {
|
|
20408
|
+
const { counters } = ctx.binding;
|
|
20409
|
+
const refuse = (note) => {
|
|
20410
|
+
counters.egress_denied++;
|
|
20411
|
+
counters.first_egress_denied ??= ctx.path;
|
|
20412
|
+
ctx.done(403, note);
|
|
20413
|
+
socket.end(FORBIDDEN);
|
|
20414
|
+
};
|
|
20415
|
+
if (ctx.binding.step !== "clone") return refuse(`step=${ctx.binding.step ?? "none"}`);
|
|
20416
|
+
const target = parseConnectTarget(ctx.path);
|
|
20417
|
+
if (!target.ok) return refuse("not allowed");
|
|
20418
|
+
const upstream = this.dial(target.host, target.port);
|
|
20419
|
+
let set = this.tunnels.get(ctx.slot.index);
|
|
20420
|
+
if (!set) this.tunnels.set(ctx.slot.index, set = /* @__PURE__ */ new Set());
|
|
20421
|
+
const destroy = () => {
|
|
20422
|
+
upstream.destroy();
|
|
20423
|
+
socket.destroy();
|
|
20424
|
+
};
|
|
20425
|
+
set.add(destroy);
|
|
20426
|
+
let open4 = false;
|
|
20427
|
+
const cleanup = () => {
|
|
20428
|
+
set.delete(destroy);
|
|
20429
|
+
destroy();
|
|
20430
|
+
};
|
|
20431
|
+
upstream.once("connect", () => {
|
|
20432
|
+
open4 = true;
|
|
20433
|
+
ctx.done(200);
|
|
20434
|
+
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
20435
|
+
if (head.length) upstream.write(head);
|
|
20436
|
+
upstream.pipe(socket);
|
|
20437
|
+
socket.pipe(upstream);
|
|
20438
|
+
});
|
|
20439
|
+
upstream.on("error", () => {
|
|
20440
|
+
if (open4) return;
|
|
20441
|
+
ctx.done(502, "dial failed");
|
|
20442
|
+
set.delete(destroy);
|
|
20443
|
+
socket.end(BAD_GATEWAY);
|
|
20444
|
+
});
|
|
20445
|
+
upstream.on("close", () => {
|
|
20446
|
+
if (open4) cleanup();
|
|
20447
|
+
});
|
|
20448
|
+
socket.on("close", cleanup);
|
|
20449
|
+
socket.on("error", cleanup);
|
|
20450
|
+
}
|
|
20451
|
+
};
|
|
20452
|
+
}
|
|
20453
|
+
});
|
|
20454
|
+
|
|
19709
20455
|
// src/daemon/sandbox/mcpGate.ts
|
|
19710
20456
|
function redact(err4) {
|
|
19711
20457
|
const msg = err4 instanceof Error ? err4.message : "error";
|
|
19712
20458
|
return msg.replace(/\b(amj_|am_|sk-ant-|eyJ)[A-Za-z0-9._-]*/g, "[redacted]").slice(0, 200);
|
|
19713
20459
|
}
|
|
19714
|
-
var MCP_PATH, REMINT_LEAD_MS, MAX_TIMER_MS,
|
|
20460
|
+
var MCP_PATH, REMINT_LEAD_MS, MAX_TIMER_MS, realTimers2, McpGate;
|
|
19715
20461
|
var init_mcpGate = __esm({
|
|
19716
20462
|
"src/daemon/sandbox/mcpGate.ts"() {
|
|
19717
20463
|
"use strict";
|
|
@@ -19720,7 +20466,7 @@ var init_mcpGate = __esm({
|
|
|
19720
20466
|
MCP_PATH = "/functions/v1/mcp";
|
|
19721
20467
|
REMINT_LEAD_MS = 10 * 60 * 1e3;
|
|
19722
20468
|
MAX_TIMER_MS = 2 ** 31 - 1;
|
|
19723
|
-
|
|
20469
|
+
realTimers2 = {
|
|
19724
20470
|
now: () => Date.now(),
|
|
19725
20471
|
setTimeout: (fn, ms) => {
|
|
19726
20472
|
const t = setTimeout(fn, ms);
|
|
@@ -19733,7 +20479,7 @@ var init_mcpGate = __esm({
|
|
|
19733
20479
|
constructor(opts) {
|
|
19734
20480
|
this.opts = opts;
|
|
19735
20481
|
this.upstream = new URL(opts.upstream);
|
|
19736
|
-
this.timers = opts.timers ??
|
|
20482
|
+
this.timers = opts.timers ?? realTimers2;
|
|
19737
20483
|
this.log = opts.logger ?? ((line) => console.log(line));
|
|
19738
20484
|
this.gate = createGateServer({
|
|
19739
20485
|
name: "mcp-gate",
|
|
@@ -19776,176 +20522,79 @@ var init_mcpGate = __esm({
|
|
|
19776
20522
|
const s = this.schedules.get(index);
|
|
19777
20523
|
if (s) {
|
|
19778
20524
|
this.timers.clearTimeout(s.handle);
|
|
19779
|
-
this.schedules.delete(index);
|
|
19780
|
-
}
|
|
19781
|
-
this.generation++;
|
|
19782
|
-
}
|
|
19783
|
-
schedule(index, jobId, expiresAt) {
|
|
19784
|
-
const generation = ++this.generation;
|
|
19785
|
-
const arm = (fireAt) => {
|
|
19786
|
-
const delay = Math.max(0, fireAt - this.timers.now());
|
|
19787
|
-
const handle = this.timers.setTimeout(() => {
|
|
19788
|
-
if (this.schedules.get(index)?.generation !== generation) return;
|
|
19789
|
-
if (fireAt - this.timers.now() > 0) arm(fireAt);
|
|
19790
|
-
else void this.remint(index, jobId, generation);
|
|
19791
|
-
}, Math.min(delay, MAX_TIMER_MS));
|
|
19792
|
-
this.schedules.set(index, { jobId, handle, generation });
|
|
19793
|
-
};
|
|
19794
|
-
arm(expiresAt - REMINT_LEAD_MS);
|
|
19795
|
-
}
|
|
19796
|
-
async remint(index, jobId, generation) {
|
|
19797
|
-
let next;
|
|
19798
|
-
try {
|
|
19799
|
-
next = await this.opts.remint(jobId);
|
|
19800
|
-
} catch (err4) {
|
|
19801
|
-
if (this.schedules.get(index)?.generation === generation) this.schedules.delete(index);
|
|
19802
|
-
this.log(`[mcp-gate] remint failed job=${jobId}: ${redact(err4)}`);
|
|
19803
|
-
return;
|
|
19804
|
-
}
|
|
19805
|
-
const binding = this.opts.slotMap.binding(index);
|
|
19806
|
-
if (this.schedules.get(index)?.generation !== generation || binding?.jobId !== jobId || !binding.mcp) {
|
|
19807
|
-
this.log(`[mcp-gate] remint discarded job=${jobId}: slot moved on`);
|
|
19808
|
-
return;
|
|
19809
|
-
}
|
|
19810
|
-
binding.mcp = { ...binding.mcp, token: next.token, expiresAt: next.expiresAt };
|
|
19811
|
-
binding.counters.remints++;
|
|
19812
|
-
this.schedules.delete(index);
|
|
19813
|
-
this.log(`[mcp-gate] remint ok job=${jobId}`);
|
|
19814
|
-
if (next.expiresAt === null) return;
|
|
19815
|
-
if (next.expiresAt - REMINT_LEAD_MS <= this.timers.now()) {
|
|
19816
|
-
this.log(`[mcp-gate] reminted token expires inside the lead window job=${jobId}; not rescheduling`);
|
|
19817
|
-
return;
|
|
19818
|
-
}
|
|
19819
|
-
this.schedule(index, jobId, next.expiresAt);
|
|
19820
|
-
}
|
|
19821
|
-
handle(req, res, ctx) {
|
|
19822
|
-
if (ctx.path !== MCP_PATH) {
|
|
19823
|
-
ctx.done(404);
|
|
19824
|
-
res.writeHead(404).end();
|
|
19825
|
-
return;
|
|
19826
|
-
}
|
|
19827
|
-
if (req.method === "GET") {
|
|
19828
|
-
ctx.done(405);
|
|
19829
|
-
res.writeHead(405, { allow: "POST" }).end();
|
|
19830
|
-
return;
|
|
19831
|
-
}
|
|
19832
|
-
if (req.method !== "POST") {
|
|
19833
|
-
ctx.done(404);
|
|
19834
|
-
res.writeHead(404).end();
|
|
19835
|
-
return;
|
|
19836
|
-
}
|
|
19837
|
-
const mcp = ctx.binding.mcp;
|
|
19838
|
-
if (!mcp) {
|
|
19839
|
-
ctx.done(403, "no-mcp");
|
|
19840
|
-
res.writeHead(403).end();
|
|
19841
|
-
return;
|
|
19842
|
-
}
|
|
19843
|
-
ctx.binding.counters.mcp_requests++;
|
|
19844
|
-
const headers = filterHeaders(
|
|
19845
|
-
req.headers,
|
|
19846
|
-
(k) => k === "authorization" || k === "apikey" || k.startsWith("x-archme-source-")
|
|
19847
|
-
);
|
|
19848
|
-
headers.authorization = `Bearer ${mcp.token}`;
|
|
19849
|
-
const target = new URL(this.upstream.href);
|
|
19850
|
-
target.search = new URLSearchParams({ project: mcp.projectSlug, branch: mcp.branch }).toString();
|
|
19851
|
-
void forward(req, res, target, headers).then((r) => ctx.done(r.status));
|
|
19852
|
-
}
|
|
19853
|
-
};
|
|
19854
|
-
}
|
|
19855
|
-
});
|
|
19856
|
-
|
|
19857
|
-
// src/daemon/sandbox/slotMap.ts
|
|
19858
|
-
function parseIpv4(addr) {
|
|
19859
|
-
const m = IPV4_RE.exec(addr);
|
|
19860
|
-
if (!m) return null;
|
|
19861
|
-
let v = 0;
|
|
19862
|
-
for (let i2 = 1; i2 <= 4; i2++) {
|
|
19863
|
-
const o2 = Number(m[i2]);
|
|
19864
|
-
if (o2 > 255) return null;
|
|
19865
|
-
v = v * 256 + o2;
|
|
19866
|
-
}
|
|
19867
|
-
return v >>> 0;
|
|
19868
|
-
}
|
|
19869
|
-
function parseCidr(cidr) {
|
|
19870
|
-
const [ip, bits, extra] = cidr.split("/");
|
|
19871
|
-
if (ip === void 0 || bits === void 0 || extra !== void 0 || !/^\d{1,2}$/.test(bits)) return null;
|
|
19872
|
-
const n2 = Number(bits);
|
|
19873
|
-
const base = parseIpv4(ip);
|
|
19874
|
-
if (base === null || n2 > 32) return null;
|
|
19875
|
-
const mask = n2 === 0 ? 0 : ~0 << 32 - n2 >>> 0;
|
|
19876
|
-
return { base: (base & mask) >>> 0, mask };
|
|
19877
|
-
}
|
|
19878
|
-
function stripMapped(addr) {
|
|
19879
|
-
return (addr ?? "").replace(/^::ffff:/i, "");
|
|
19880
|
-
}
|
|
19881
|
-
var IPV4_RE, SlotMap;
|
|
19882
|
-
var init_slotMap = __esm({
|
|
19883
|
-
"src/daemon/sandbox/slotMap.ts"() {
|
|
19884
|
-
"use strict";
|
|
19885
|
-
init_appleContainer();
|
|
19886
|
-
IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
19887
|
-
SlotMap = class {
|
|
19888
|
-
byIndex = /* @__PURE__ */ new Map();
|
|
19889
|
-
byGateway = /* @__PURE__ */ new Map();
|
|
19890
|
-
allowGatewayAsPeer;
|
|
19891
|
-
constructor(slots, opts = {}) {
|
|
19892
|
-
this.allowGatewayAsPeer = opts.allowGatewayAsPeer ?? false;
|
|
19893
|
-
for (const slot of slots) {
|
|
19894
|
-
const gateway = parseIpv4(slot.gateway);
|
|
19895
|
-
const cidr = parseCidr(slot.subnet);
|
|
19896
|
-
if (gateway === null) throw new Error(`slot ${slot.index}: gateway is not an IPv4 address`);
|
|
19897
|
-
if (cidr === null) throw new Error(`slot ${slot.index}: subnet is not an IPv4 CIDR`);
|
|
19898
|
-
if (this.byIndex.has(slot.index)) throw new Error(`slot ${slot.index}: duplicate index`);
|
|
19899
|
-
if (this.byGateway.has(slot.gateway)) throw new Error(`slot ${slot.index}: duplicate gateway`);
|
|
19900
|
-
const entry = { slot, gateway, cidr, binding: null };
|
|
19901
|
-
this.byIndex.set(slot.index, entry);
|
|
19902
|
-
this.byGateway.set(slot.gateway, entry);
|
|
19903
|
-
}
|
|
19904
|
-
}
|
|
19905
|
-
/**
|
|
19906
|
-
* The slot a connection belongs to, or null (drop it). `local` must be a slot
|
|
19907
|
-
* gateway; `remote` must be a plain IPv4 inside THAT slot's subnet — never
|
|
19908
|
-
* IPv6, never another slot's range.
|
|
19909
|
-
*/
|
|
19910
|
-
resolve(localAddress, remoteAddress) {
|
|
19911
|
-
const entry = this.byGateway.get(stripMapped(localAddress));
|
|
19912
|
-
if (!entry) return null;
|
|
19913
|
-
const remoteStr = stripMapped(remoteAddress);
|
|
19914
|
-
const remote = parseIpv4(remoteStr);
|
|
19915
|
-
if (remote === null) return null;
|
|
19916
|
-
if ((remote & entry.cidr.mask) >>> 0 !== entry.cidr.base) return null;
|
|
19917
|
-
if (remote === entry.gateway && !this.allowGatewayAsPeer) return null;
|
|
19918
|
-
return entry.slot;
|
|
19919
|
-
}
|
|
19920
|
-
slots() {
|
|
19921
|
-
return [...this.byIndex.values()].map((e) => e.slot);
|
|
19922
|
-
}
|
|
19923
|
-
slot(index) {
|
|
19924
|
-
return this.byIndex.get(index)?.slot ?? null;
|
|
19925
|
-
}
|
|
19926
|
-
bind(index, binding) {
|
|
19927
|
-
const entry = this.entry(index);
|
|
19928
|
-
if (entry.binding && entry.binding.jobId !== binding.jobId) {
|
|
19929
|
-
throw new Error(`slot ${index} is bound to another job`);
|
|
19930
|
-
}
|
|
19931
|
-
entry.binding = binding;
|
|
19932
|
-
}
|
|
19933
|
-
clear(index) {
|
|
19934
|
-
this.entry(index).binding = null;
|
|
20525
|
+
this.schedules.delete(index);
|
|
20526
|
+
}
|
|
20527
|
+
this.generation++;
|
|
19935
20528
|
}
|
|
19936
|
-
|
|
19937
|
-
|
|
20529
|
+
schedule(index, jobId, expiresAt) {
|
|
20530
|
+
const generation = ++this.generation;
|
|
20531
|
+
const arm = (fireAt) => {
|
|
20532
|
+
const delay = Math.max(0, fireAt - this.timers.now());
|
|
20533
|
+
const handle = this.timers.setTimeout(() => {
|
|
20534
|
+
if (this.schedules.get(index)?.generation !== generation) return;
|
|
20535
|
+
if (fireAt - this.timers.now() > 0) arm(fireAt);
|
|
20536
|
+
else void this.remint(index, jobId, generation);
|
|
20537
|
+
}, Math.min(delay, MAX_TIMER_MS));
|
|
20538
|
+
this.schedules.set(index, { jobId, handle, generation });
|
|
20539
|
+
};
|
|
20540
|
+
arm(expiresAt - REMINT_LEAD_MS);
|
|
19938
20541
|
}
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
|
|
20542
|
+
async remint(index, jobId, generation) {
|
|
20543
|
+
let next;
|
|
20544
|
+
try {
|
|
20545
|
+
next = await this.opts.remint(jobId);
|
|
20546
|
+
} catch (err4) {
|
|
20547
|
+
if (this.schedules.get(index)?.generation === generation) this.schedules.delete(index);
|
|
20548
|
+
this.log(`[mcp-gate] remint failed job=${jobId}: ${redact(err4)}`);
|
|
20549
|
+
return;
|
|
19942
20550
|
}
|
|
19943
|
-
|
|
20551
|
+
const binding = this.opts.slotMap.binding(index);
|
|
20552
|
+
if (this.schedules.get(index)?.generation !== generation || binding?.jobId !== jobId || !binding.mcp) {
|
|
20553
|
+
this.log(`[mcp-gate] remint discarded job=${jobId}: slot moved on`);
|
|
20554
|
+
return;
|
|
20555
|
+
}
|
|
20556
|
+
binding.mcp = { ...binding.mcp, token: next.token, expiresAt: next.expiresAt };
|
|
20557
|
+
binding.counters.remints++;
|
|
20558
|
+
this.schedules.delete(index);
|
|
20559
|
+
this.log(`[mcp-gate] remint ok job=${jobId}`);
|
|
20560
|
+
if (next.expiresAt === null) return;
|
|
20561
|
+
if (next.expiresAt - REMINT_LEAD_MS <= this.timers.now()) {
|
|
20562
|
+
this.log(`[mcp-gate] reminted token expires inside the lead window job=${jobId}; not rescheduling`);
|
|
20563
|
+
return;
|
|
20564
|
+
}
|
|
20565
|
+
this.schedule(index, jobId, next.expiresAt);
|
|
19944
20566
|
}
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
|
|
19948
|
-
|
|
20567
|
+
handle(req, res, ctx) {
|
|
20568
|
+
if (ctx.path !== MCP_PATH) {
|
|
20569
|
+
ctx.done(404);
|
|
20570
|
+
res.writeHead(404).end();
|
|
20571
|
+
return;
|
|
20572
|
+
}
|
|
20573
|
+
if (req.method === "GET") {
|
|
20574
|
+
ctx.done(405);
|
|
20575
|
+
res.writeHead(405, { allow: "POST" }).end();
|
|
20576
|
+
return;
|
|
20577
|
+
}
|
|
20578
|
+
if (req.method !== "POST") {
|
|
20579
|
+
ctx.done(404);
|
|
20580
|
+
res.writeHead(404).end();
|
|
20581
|
+
return;
|
|
20582
|
+
}
|
|
20583
|
+
const mcp = ctx.binding.mcp;
|
|
20584
|
+
if (!mcp) {
|
|
20585
|
+
ctx.done(403, "no-mcp");
|
|
20586
|
+
res.writeHead(403).end();
|
|
20587
|
+
return;
|
|
20588
|
+
}
|
|
20589
|
+
ctx.binding.counters.mcp_requests++;
|
|
20590
|
+
const headers = filterHeaders(
|
|
20591
|
+
req.headers,
|
|
20592
|
+
(k) => k === "authorization" || k === "apikey" || k.startsWith("x-archme-source-")
|
|
20593
|
+
);
|
|
20594
|
+
headers.authorization = `Bearer ${mcp.token}`;
|
|
20595
|
+
const target = new URL(this.upstream.href);
|
|
20596
|
+
target.search = new URLSearchParams({ project: mcp.projectSlug, branch: mcp.branch }).toString();
|
|
20597
|
+
void forward(req, res, target, headers).then((r) => ctx.done(r.status));
|
|
19949
20598
|
}
|
|
19950
20599
|
};
|
|
19951
20600
|
}
|
|
@@ -19953,7 +20602,16 @@ var init_slotMap = __esm({
|
|
|
19953
20602
|
|
|
19954
20603
|
// src/daemon/sandbox/slots.ts
|
|
19955
20604
|
function zeroCounters() {
|
|
19956
|
-
return {
|
|
20605
|
+
return {
|
|
20606
|
+
model_requests: 0,
|
|
20607
|
+
upstream_429: 0,
|
|
20608
|
+
last_upstream_status: null,
|
|
20609
|
+
cap_hits: 0,
|
|
20610
|
+
mcp_requests: 0,
|
|
20611
|
+
remints: 0,
|
|
20612
|
+
egress_denied: 0,
|
|
20613
|
+
first_egress_denied: null
|
|
20614
|
+
};
|
|
19957
20615
|
}
|
|
19958
20616
|
function indexOf(slot) {
|
|
19959
20617
|
return typeof slot === "number" ? slot : slot.index;
|
|
@@ -19963,6 +20621,7 @@ var init_slots = __esm({
|
|
|
19963
20621
|
"src/daemon/sandbox/slots.ts"() {
|
|
19964
20622
|
"use strict";
|
|
19965
20623
|
init_credentialGate();
|
|
20624
|
+
init_githubGate();
|
|
19966
20625
|
init_mcpGate();
|
|
19967
20626
|
init_slotMap();
|
|
19968
20627
|
init_types();
|
|
@@ -19970,6 +20629,7 @@ var init_slots = __esm({
|
|
|
19970
20629
|
slotMap;
|
|
19971
20630
|
credential;
|
|
19972
20631
|
mcp;
|
|
20632
|
+
github;
|
|
19973
20633
|
boundPorts = null;
|
|
19974
20634
|
constructor(opts) {
|
|
19975
20635
|
this.slotMap = new SlotMap(opts.slots, opts.slotMapOptions);
|
|
@@ -19990,21 +20650,32 @@ var init_slots = __esm({
|
|
|
19990
20650
|
logger: opts.logger,
|
|
19991
20651
|
timers: opts.timers
|
|
19992
20652
|
});
|
|
20653
|
+
this.github = new GithubGate({
|
|
20654
|
+
slotMap: this.slotMap,
|
|
20655
|
+
host: opts.host,
|
|
20656
|
+
port: opts.ports?.github ?? GATE_PORTS.github,
|
|
20657
|
+
logger: opts.logger,
|
|
20658
|
+
dial: opts.githubDial
|
|
20659
|
+
});
|
|
19993
20660
|
}
|
|
19994
|
-
/** Binds
|
|
20661
|
+
/** Binds all three gates; resolves with the ports actually bound. */
|
|
19995
20662
|
async start() {
|
|
19996
|
-
const
|
|
20663
|
+
const started = [];
|
|
19997
20664
|
try {
|
|
20665
|
+
const anthropic = await this.credential.listen();
|
|
20666
|
+
started.push(this.credential);
|
|
19998
20667
|
const mcp = await this.mcp.listen();
|
|
19999
|
-
this.
|
|
20668
|
+
started.push(this.mcp);
|
|
20669
|
+
const github = await this.github.listen();
|
|
20670
|
+
this.boundPorts = { anthropic, mcp, github };
|
|
20000
20671
|
return this.boundPorts;
|
|
20001
20672
|
} catch (err4) {
|
|
20002
|
-
await
|
|
20673
|
+
await Promise.all(started.map((g) => g.close()));
|
|
20003
20674
|
throw err4;
|
|
20004
20675
|
}
|
|
20005
20676
|
}
|
|
20006
20677
|
async stop() {
|
|
20007
|
-
await Promise.all([this.credential.close(), this.mcp.close()]);
|
|
20678
|
+
await Promise.all([this.credential.close(), this.mcp.close(), this.github.close()]);
|
|
20008
20679
|
this.boundPorts = null;
|
|
20009
20680
|
}
|
|
20010
20681
|
ports() {
|
|
@@ -20013,177 +20684,70 @@ var init_slots = __esm({
|
|
|
20013
20684
|
/** Throws if the slot is unknown or already serving another job. */
|
|
20014
20685
|
bindJob(slot, input) {
|
|
20015
20686
|
const index = indexOf(slot);
|
|
20016
|
-
this.slotMap.bind(index, {
|
|
20017
|
-
jobId: input.jobId,
|
|
20018
|
-
maxModelRequests: input.maxModelRequests,
|
|
20019
|
-
mcp: input.mcp ? { ...input.mcp } : null,
|
|
20020
|
-
|
|
20021
|
-
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
this.slotMap.
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
|
|
20059
|
-
|
|
20060
|
-
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
}
|
|
20074
|
-
function fits(line, cap) {
|
|
20075
|
-
return Buffer.byteLength(line, "utf8") <= cap;
|
|
20076
|
-
}
|
|
20077
|
-
function withTruncatedError(r) {
|
|
20078
|
-
return { ...r, error: r.error ? `${r.error}; ${RESULT_TRUNCATED}` : RESULT_TRUNCATED };
|
|
20079
|
-
}
|
|
20080
|
-
function minimal(r) {
|
|
20081
|
-
if (r.kind === "clone") {
|
|
20082
|
-
const m2 = { kind: "clone", outcome: r.outcome, memoryPeakBytes: r.memoryPeakBytes, durations_ms: {}, error: RESULT_TRUNCATED };
|
|
20083
|
-
return m2;
|
|
20084
|
-
}
|
|
20085
|
-
const m = {
|
|
20086
|
-
kind: "agent",
|
|
20087
|
-
exitCode: r.exitCode,
|
|
20088
|
-
signal: r.signal,
|
|
20089
|
-
claudeStdout: "",
|
|
20090
|
-
affected: null,
|
|
20091
|
-
promptBytes: r.promptBytes,
|
|
20092
|
-
memoryPeakBytes: r.memoryPeakBytes,
|
|
20093
|
-
durations_ms: {},
|
|
20094
|
-
error: RESULT_TRUNCATED
|
|
20095
|
-
};
|
|
20096
|
-
return m;
|
|
20097
|
-
}
|
|
20098
|
-
function formatResultLine(result, nonce, cap = RESULT_MAX_BYTES) {
|
|
20099
|
-
if (!isValidNonce(nonce)) throw new Error("result: invalid nonce");
|
|
20100
|
-
const encode_ = (r) => encode(r, nonce);
|
|
20101
|
-
let line = encode_(result);
|
|
20102
|
-
if (fits(line, cap)) return line;
|
|
20103
|
-
if (result.kind === "agent") {
|
|
20104
|
-
const noAffected = { ...result, affected: null };
|
|
20105
|
-
line = encode_(withTruncatedError(noAffected));
|
|
20106
|
-
if (fits(line, cap)) return line;
|
|
20107
|
-
line = encode_(withTruncatedError({ ...noAffected, claudeStdout: "" }));
|
|
20108
|
-
if (fits(line, cap)) return line;
|
|
20109
|
-
}
|
|
20110
|
-
return encode_(minimal(result));
|
|
20111
|
-
}
|
|
20112
|
-
function isObj(v) {
|
|
20113
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
20114
|
-
}
|
|
20115
|
-
function isNumOrNull(v) {
|
|
20116
|
-
return v === null || typeof v === "number" && Number.isFinite(v);
|
|
20117
|
-
}
|
|
20118
|
-
function shapeError(o2) {
|
|
20119
|
-
if (!isNumOrNull(o2.memoryPeakBytes)) return "memoryPeakBytes";
|
|
20120
|
-
if (!isObj(o2.durations_ms) || !Object.values(o2.durations_ms).every((v) => typeof v === "number")) return "durations_ms";
|
|
20121
|
-
if (o2.error !== void 0 && typeof o2.error !== "string") return "error";
|
|
20122
|
-
if (o2.kind === "clone") {
|
|
20123
|
-
if (o2.outcome !== "diff" && o2.outcome !== "skip" && o2.outcome !== "rejected") return "outcome";
|
|
20124
|
-
if (o2.diff !== void 0 && !isObj(o2.diff)) return "diff";
|
|
20125
|
-
return null;
|
|
20126
|
-
}
|
|
20127
|
-
if (o2.kind === "agent") {
|
|
20128
|
-
if (!isNumOrNull(o2.exitCode)) return "exitCode";
|
|
20129
|
-
if (o2.signal !== null && typeof o2.signal !== "string") return "signal";
|
|
20130
|
-
if (typeof o2.claudeStdout !== "string") return "claudeStdout";
|
|
20131
|
-
if (o2.affected !== null && !(isObj(o2.affected) && ["stories", "components", "unresolvedFiles"].every((k) => Array.isArray(o2.affected[k])))) {
|
|
20132
|
-
return "affected";
|
|
20133
|
-
}
|
|
20134
|
-
if (typeof o2.promptBytes !== "number") return "promptBytes";
|
|
20135
|
-
return null;
|
|
20136
|
-
}
|
|
20137
|
-
return "kind";
|
|
20138
|
-
}
|
|
20139
|
-
function parseResultLine(stdout, nonce, cap = RESULT_MAX_BYTES) {
|
|
20140
|
-
if (!isValidNonce(nonce)) return { ok: false, reason: "no result line" };
|
|
20141
|
-
const prefix = `${RESULT_PREFIX}${nonce} `;
|
|
20142
|
-
const lines = stdout.split("\n");
|
|
20143
|
-
let line;
|
|
20144
|
-
for (let i2 = lines.length - 1; i2 >= 0; i2--) {
|
|
20145
|
-
const l = lines[i2].replace(/\r$/, "");
|
|
20146
|
-
if (l.startsWith(prefix)) {
|
|
20147
|
-
line = l;
|
|
20148
|
-
break;
|
|
20149
|
-
}
|
|
20150
|
-
}
|
|
20151
|
-
if (line === void 0) return { ok: false, reason: "no result line" };
|
|
20152
|
-
if (Buffer.byteLength(line, "utf8") > cap) return { ok: false, reason: "result line exceeds cap" };
|
|
20153
|
-
const b64 = line.slice(prefix.length).trim();
|
|
20154
|
-
if (b64 === "" || b64.length % 4 !== 0 || !BASE64_RE.test(b64)) return { ok: false, reason: "result is not base64" };
|
|
20155
|
-
let data;
|
|
20156
|
-
try {
|
|
20157
|
-
data = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
|
|
20158
|
-
} catch {
|
|
20159
|
-
return { ok: false, reason: "result is not JSON" };
|
|
20160
|
-
}
|
|
20161
|
-
if (!isObj(data)) return { ok: false, reason: "result is not an object" };
|
|
20162
|
-
const bad = shapeError(data);
|
|
20163
|
-
if (bad) return { ok: false, reason: `result has a malformed ${bad}` };
|
|
20164
|
-
return { ok: true, result: data };
|
|
20165
|
-
}
|
|
20166
|
-
async function readMemoryPeak(path7 = "/sys/fs/cgroup/memory.peak", read = (p) => readFile13(p, "utf8")) {
|
|
20167
|
-
try {
|
|
20168
|
-
const n2 = Number.parseInt((await read(path7)).trim(), 10);
|
|
20169
|
-
return Number.isFinite(n2) && n2 >= 0 ? n2 : null;
|
|
20170
|
-
} catch {
|
|
20171
|
-
return null;
|
|
20172
|
-
}
|
|
20173
|
-
}
|
|
20174
|
-
var RESULT_TRUNCATED, BASE64_RE;
|
|
20175
|
-
var init_result2 = __esm({
|
|
20176
|
-
"src/daemon/sandboxJob/result.ts"() {
|
|
20177
|
-
"use strict";
|
|
20178
|
-
init_types();
|
|
20179
|
-
RESULT_TRUNCATED = "result truncated";
|
|
20180
|
-
BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
20687
|
+
this.slotMap.bind(index, {
|
|
20688
|
+
jobId: input.jobId,
|
|
20689
|
+
maxModelRequests: input.maxModelRequests,
|
|
20690
|
+
mcp: input.mcp ? { ...input.mcp } : null,
|
|
20691
|
+
step: null,
|
|
20692
|
+
counters: zeroCounters()
|
|
20693
|
+
});
|
|
20694
|
+
this.mcp.onBind(index);
|
|
20695
|
+
}
|
|
20696
|
+
/** Which step's box is running on the slot; set before each box starts.
|
|
20697
|
+
* Leaving the clone step closes the slot's GitHub tunnels. */
|
|
20698
|
+
setStep(slot, step) {
|
|
20699
|
+
const index = indexOf(slot);
|
|
20700
|
+
const b = this.slotMap.binding(index);
|
|
20701
|
+
if (!b) throw new Error(`slot ${index} is not bound`);
|
|
20702
|
+
b.step = step;
|
|
20703
|
+
this.github.onStepChange(index);
|
|
20704
|
+
}
|
|
20705
|
+
/** Unbinds the slot (it answers 403 again) and returns the job's final counters. */
|
|
20706
|
+
clearJob(slot) {
|
|
20707
|
+
const index = indexOf(slot);
|
|
20708
|
+
const counters = this.countersFor(index);
|
|
20709
|
+
this.mcp.onClear(index);
|
|
20710
|
+
this.github.onClear(index);
|
|
20711
|
+
this.slotMap.clear(index);
|
|
20712
|
+
return counters;
|
|
20713
|
+
}
|
|
20714
|
+
/** A snapshot of the slot's counters, or null when idle. */
|
|
20715
|
+
countersFor(slot) {
|
|
20716
|
+
const b = this.slotMap.binding(indexOf(slot));
|
|
20717
|
+
return b ? { ...b.counters } : null;
|
|
20718
|
+
}
|
|
20719
|
+
};
|
|
20720
|
+
SlotPool = class {
|
|
20721
|
+
free_ = /* @__PURE__ */ new Set();
|
|
20722
|
+
byIndex = /* @__PURE__ */ new Map();
|
|
20723
|
+
constructor(slots) {
|
|
20724
|
+
for (const s of slots) {
|
|
20725
|
+
this.byIndex.set(s.index, s);
|
|
20726
|
+
this.free_.add(s.index);
|
|
20727
|
+
}
|
|
20728
|
+
}
|
|
20729
|
+
/** The lowest free slot, or null when every slot is busy. */
|
|
20730
|
+
lease() {
|
|
20731
|
+
let best = null;
|
|
20732
|
+
for (const i2 of this.free_) if (best === null || i2 < best) best = i2;
|
|
20733
|
+
if (best === null) return null;
|
|
20734
|
+
this.free_.delete(best);
|
|
20735
|
+
return this.byIndex.get(best) ?? null;
|
|
20736
|
+
}
|
|
20737
|
+
/** Idempotent; unknown indices are ignored. */
|
|
20738
|
+
free(index) {
|
|
20739
|
+
if (this.byIndex.has(index)) this.free_.add(index);
|
|
20740
|
+
}
|
|
20741
|
+
available() {
|
|
20742
|
+
return this.free_.size;
|
|
20743
|
+
}
|
|
20744
|
+
};
|
|
20181
20745
|
}
|
|
20182
20746
|
});
|
|
20183
20747
|
|
|
20184
20748
|
// src/daemon/sandboxJob/clone.ts
|
|
20185
|
-
import { mkdir as
|
|
20186
|
-
import { dirname as
|
|
20749
|
+
import { mkdir as mkdir12, writeFile as writeFile11 } from "node:fs/promises";
|
|
20750
|
+
import { dirname as dirname16 } from "node:path";
|
|
20187
20751
|
function asArchmeJob(job) {
|
|
20188
20752
|
return job;
|
|
20189
20753
|
}
|
|
@@ -20208,7 +20772,7 @@ async function runCloneStep(input, deps = {}) {
|
|
|
20208
20772
|
const { env: gitEnv, cleanup } = await buildGitEnv(input.credentials);
|
|
20209
20773
|
const restoreEnv = applyEnv(gitEnv);
|
|
20210
20774
|
try {
|
|
20211
|
-
await
|
|
20775
|
+
await mkdir12(repoDir, { recursive: true });
|
|
20212
20776
|
const must = async (args, cwd) => {
|
|
20213
20777
|
const r = await git4(args, { cwd, env: gitEnv });
|
|
20214
20778
|
if (r.exitCode !== 0) throw new Error(`git ${args[0]} failed: ${r.stderr.trim()}`);
|
|
@@ -20260,7 +20824,7 @@ async function runCloneStep(input, deps = {}) {
|
|
|
20260
20824
|
after: diff.after,
|
|
20261
20825
|
...diff.provenance ? { provenance: diff.provenance } : {}
|
|
20262
20826
|
};
|
|
20263
|
-
await
|
|
20827
|
+
await mkdir12(dirname16(diffPath), { recursive: true });
|
|
20264
20828
|
await writeFile11(diffPath, JSON.stringify(persisted), { mode: 384 });
|
|
20265
20829
|
const bytes = Buffer.byteLength(diff.patch, "utf8");
|
|
20266
20830
|
log(`diff ${diff.rangeLabel}: ${diff.changedFiles.length} file(s), ${bytes} bytes${diff.materialized ? " (materialized)" : ""}`);
|
|
@@ -20316,7 +20880,7 @@ var init_clone = __esm({
|
|
|
20316
20880
|
});
|
|
20317
20881
|
|
|
20318
20882
|
// src/daemon/sandboxJob/agent.ts
|
|
20319
|
-
import { mkdir as
|
|
20883
|
+
import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile12 } from "node:fs/promises";
|
|
20320
20884
|
import { join as join23 } from "node:path";
|
|
20321
20885
|
function agentBoxEnv(gateway) {
|
|
20322
20886
|
return {
|
|
@@ -20329,10 +20893,16 @@ function agentBoxEnv(gateway) {
|
|
|
20329
20893
|
DISABLE_ERROR_REPORTING: "1"
|
|
20330
20894
|
};
|
|
20331
20895
|
}
|
|
20332
|
-
function cloneBoxEnv() {
|
|
20896
|
+
function cloneBoxEnv(gateway) {
|
|
20897
|
+
const proxy = `http://${gateway}:${GATE_PORTS.github}`;
|
|
20333
20898
|
return {
|
|
20334
20899
|
HOME: BOX_HOME,
|
|
20335
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
20900
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
20901
|
+
https_proxy: proxy,
|
|
20902
|
+
HTTPS_PROXY: proxy,
|
|
20903
|
+
GIT_CONFIG_COUNT: "1",
|
|
20904
|
+
GIT_CONFIG_KEY_0: "http.proxy",
|
|
20905
|
+
GIT_CONFIG_VALUE_0: proxy
|
|
20336
20906
|
};
|
|
20337
20907
|
}
|
|
20338
20908
|
function boxMcpConfig(gateway) {
|
|
@@ -20343,7 +20913,7 @@ function boxMcpConfig(gateway) {
|
|
|
20343
20913
|
};
|
|
20344
20914
|
}
|
|
20345
20915
|
async function writeBoxMcpConfig(gateway, dir = BOX_MCP_DIR) {
|
|
20346
|
-
await
|
|
20916
|
+
await mkdir13(dir, { recursive: true, mode: 448 });
|
|
20347
20917
|
const path7 = join23(dir, "mcp.json");
|
|
20348
20918
|
await writeFile12(path7, JSON.stringify(boxMcpConfig(gateway), null, 2), { mode: 384 });
|
|
20349
20919
|
return path7;
|
|
@@ -20411,7 +20981,7 @@ async function runAgentStep(input, deps = {}) {
|
|
|
20411
20981
|
);
|
|
20412
20982
|
const promptBytes = Buffer.byteLength(prompt, "utf8");
|
|
20413
20983
|
const mcpConfigPath = await writeBoxMcpConfig(input.gateway, deps.mcpDir);
|
|
20414
|
-
for (const d of deps.homeDirs ?? [BOX_HOME, BOX_CLAUDE_CONFIG_DIR]) await
|
|
20984
|
+
for (const d of deps.homeDirs ?? [BOX_HOME, BOX_CLAUDE_CONFIG_DIR]) await mkdir13(d, { recursive: true, mode: 448 });
|
|
20415
20985
|
log(`spawning claude (model ${input.model}, prompt ${promptBytes} bytes)`);
|
|
20416
20986
|
const proc = spawn2({
|
|
20417
20987
|
prompt,
|
|
@@ -20515,6 +21085,7 @@ function createSandboxParts(settings) {
|
|
|
20515
21085
|
await gates.start();
|
|
20516
21086
|
return {
|
|
20517
21087
|
bindJob: (slot, b) => gates.bindJob(slot, b),
|
|
21088
|
+
setStep: (slot, step) => gates.setStep(slot, step),
|
|
20518
21089
|
clearJob: (slot) => {
|
|
20519
21090
|
gates.clearJob(slot);
|
|
20520
21091
|
},
|
|
@@ -20565,7 +21136,7 @@ __export(run_exports, {
|
|
|
20565
21136
|
});
|
|
20566
21137
|
import { createClient } from "@supabase/supabase-js";
|
|
20567
21138
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
20568
|
-
import { mkdtemp as mkdtemp4, mkdir as
|
|
21139
|
+
import { mkdtemp as mkdtemp4, mkdir as mkdir14, writeFile as writeFile13, rm as rm8, stat as stat8, chmod as chmod6 } from "node:fs/promises";
|
|
20569
21140
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
20570
21141
|
import { join as join24 } from "node:path";
|
|
20571
21142
|
import { homedir as homedir8 } from "node:os";
|
|
@@ -20732,7 +21303,7 @@ async function buildGitEnv(credentials) {
|
|
|
20732
21303
|
].join("\n"),
|
|
20733
21304
|
{ mode: 448 }
|
|
20734
21305
|
);
|
|
20735
|
-
await
|
|
21306
|
+
await chmod6(askpassPath, 448).catch(() => {
|
|
20736
21307
|
});
|
|
20737
21308
|
const gitConfigEntries = [
|
|
20738
21309
|
["credential.helper", ""],
|
|
@@ -20793,12 +21364,12 @@ async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
|
|
|
20793
21364
|
const deviceId = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId();
|
|
20794
21365
|
if (!deviceId) throw new Error("daemon is not signed in with a registered device");
|
|
20795
21366
|
const credentials = await getRepoCredentials(ctx, job.id, deviceId);
|
|
20796
|
-
await
|
|
21367
|
+
await mkdir14(ctx.cloneBaseDir, { recursive: true });
|
|
20797
21368
|
const dir = cloneDirFor(ctx.cloneBaseDir, job.repo_owner, job.repo_name);
|
|
20798
21369
|
const { env, cleanup: cleanupGitEnv } = await buildGitEnv(credentials);
|
|
20799
21370
|
await cloneMutex.runExclusive(dir, async () => {
|
|
20800
21371
|
if (!await exists(join24(dir, ".git"))) {
|
|
20801
|
-
await
|
|
21372
|
+
await mkdir14(join24(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
|
|
20802
21373
|
console.log(` cloning ${job.repo_owner}/${job.repo_name} \u2192 ${dir}`);
|
|
20803
21374
|
await execa("git", ["clone", "--no-tags", credentials.https_url, dir], { env, reject: true });
|
|
20804
21375
|
} else {
|
|
@@ -21441,7 +22012,7 @@ function sandboxDepsFor(ctx, job, deviceId, host) {
|
|
|
21441
22012
|
settings: host.settings,
|
|
21442
22013
|
parseResultLine: (stdout, nonce) => host.io.parseResultLine(stdout, nonce),
|
|
21443
22014
|
agentBoxEnv: (gateway) => host.io.agentBoxEnv(gateway),
|
|
21444
|
-
cloneBoxEnv: () => host.io.cloneBoxEnv(),
|
|
22015
|
+
cloneBoxEnv: (gateway) => host.io.cloneBoxEnv(gateway),
|
|
21445
22016
|
prefetch: (j) => (prefetchOverride ?? prefetchJobContext)(ctx, j, deviceId),
|
|
21446
22017
|
writeInputDir: (step, input) => writeInputDir(step, input),
|
|
21447
22018
|
log: (msg) => console.log(`[${job.id}] ${msg}`),
|
|
@@ -21585,8 +22156,13 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
21585
22156
|
const isLocalRepo = findLocalRepoForJob(await loadLocalRepos(), job) != null;
|
|
21586
22157
|
const decision = shouldSandbox(sandboxHost.settings, job, { isLocalRepo });
|
|
21587
22158
|
if (decision.reason === "local_repo") log("sandbox_skipped_local_repo: local-poll repo, running unsandboxed");
|
|
22159
|
+
const required = sandboxHost.settings.mode === "required";
|
|
22160
|
+
if (decision.reason === "local_repo" && required) log("sandbox: required mode keeps local-poll repos unboxed (\xA713)");
|
|
21588
22161
|
const slot = decision.boxed ? sandboxHost.pool.lease() : null;
|
|
21589
|
-
if (decision.boxed && !slot)
|
|
22162
|
+
if (decision.boxed && !slot) {
|
|
22163
|
+
if (required) throw new Error("sandbox_no_slot: no free sandbox slot (ARCHME_SANDBOX=required); retrying later");
|
|
22164
|
+
log("sandbox_no_slot: no free slot, running unsandboxed");
|
|
22165
|
+
}
|
|
21590
22166
|
if (slot) sandboxRun: {
|
|
21591
22167
|
boxedJobs.add(job.id);
|
|
21592
22168
|
log(`sandbox: running boxed on slot ${slot.index}`);
|
|
@@ -21597,7 +22173,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
21597
22173
|
timeouts: { claudeMs: CLAUDE_TIMEOUT_MS, largeDiffClaudeMs: CLAUDE_LARGE_DIFF_TIMEOUT_MS }
|
|
21598
22174
|
});
|
|
21599
22175
|
} catch (err4) {
|
|
21600
|
-
if (err4 instanceof SandboxInfraError && !err4.agentStarted && !shuttingDown && !cancelled.has(job.id)) {
|
|
22176
|
+
if (err4 instanceof SandboxInfraError && !err4.agentStarted && !shuttingDown && !cancelled.has(job.id) && sandboxHost.settings.mode === "shadow") {
|
|
21601
22177
|
log(`sandbox_infra_fallback: ${err4.message}, running unboxed`);
|
|
21602
22178
|
boxedJobs.delete(job.id);
|
|
21603
22179
|
break sandboxRun;
|
|
@@ -22057,6 +22633,7 @@ async function dispatch(sched) {
|
|
|
22057
22633
|
}
|
|
22058
22634
|
pauseUntilMs = null;
|
|
22059
22635
|
}
|
|
22636
|
+
if (confinementGate && !confinementGate.allowsClaims()) return;
|
|
22060
22637
|
sched.dispatching = true;
|
|
22061
22638
|
try {
|
|
22062
22639
|
let atCap = false;
|
|
@@ -22250,7 +22827,10 @@ async function runDaemon() {
|
|
|
22250
22827
|
if (process.env.ARCHME_SANDBOX_SLOTS !== void 0) {
|
|
22251
22828
|
console.log(" ARCHME_SANDBOX_SLOTS is no longer read; sandbox slots follow MAX_CONCURRENT_JOBS");
|
|
22252
22829
|
}
|
|
22253
|
-
console.log(` sandbox: ${sandboxSettings.mode}${sandboxSettings.mode === "shadow" ? ` (projects: ${sandboxSettings.projects.size ? [...sandboxSettings.projects].join(", ") : "all"}; ${sandboxSettings.slots} slot(s))` : ""}`);
|
|
22830
|
+
console.log(` sandbox: ${sandboxSettings.mode}${sandboxSettings.mode === "shadow" ? ` (projects: ${sandboxSettings.projects.size ? [...sandboxSettings.projects].join(", ") : "all"}; ${sandboxSettings.slots} slot(s))` : sandboxSettings.mode === "required" ? ` (every diff/PR job boxed; ${sandboxSettings.slots} slot(s))` : ""}`);
|
|
22831
|
+
if (sandboxSettings.mode === "required" && process.env.ARCHME_SANDBOX_PROJECTS?.trim()) {
|
|
22832
|
+
console.log(" ARCHME_SANDBOX_PROJECTS is ignored in required mode: every project's jobs run boxed");
|
|
22833
|
+
}
|
|
22254
22834
|
const bearerCfg = {
|
|
22255
22835
|
supabaseUrl: SUPABASE_URL,
|
|
22256
22836
|
anonKey: SUPABASE_ANON_KEY,
|
|
@@ -22323,7 +22903,7 @@ async function runDaemon() {
|
|
|
22323
22903
|
if (sandboxSettings.mode !== "off") {
|
|
22324
22904
|
try {
|
|
22325
22905
|
const { createSandboxParts: createSandboxParts2 } = await Promise.resolve().then(() => (init_parts(), parts_exports));
|
|
22326
|
-
const { host } = await bootSandbox(sandboxSettings, createSandboxParts2(sandboxSettings), {
|
|
22906
|
+
const { host, monitor } = await bootSandbox(sandboxSettings, createSandboxParts2(sandboxSettings), {
|
|
22327
22907
|
mcpBaseUrl: ARCHME_MCP_BASE_URL,
|
|
22328
22908
|
// The MCP gatekeeper re-mints a job's token before it expires (§7.2),
|
|
22329
22909
|
// with the device that claimed that job, as every job-scoped call must.
|
|
@@ -22337,6 +22917,8 @@ async function runDaemon() {
|
|
|
22337
22917
|
log: (m) => console.log(m)
|
|
22338
22918
|
});
|
|
22339
22919
|
sandboxHost = host;
|
|
22920
|
+
monitor.start();
|
|
22921
|
+
confinementGate = monitor;
|
|
22340
22922
|
} catch (err4) {
|
|
22341
22923
|
const msg = err4 instanceof Error ? err4.message : String(err4);
|
|
22342
22924
|
if (sandboxBootFailurePolicy(sandboxSettings.mode) === "run_unboxed") {
|
|
@@ -22465,6 +23047,7 @@ ${signal} received, shutting down`);
|
|
|
22465
23047
|
for (const ctx of sessions) ctx.auth.clearRefreshTimer();
|
|
22466
23048
|
clearInterval(sweepTimer);
|
|
22467
23049
|
clearInterval(localPollTimer);
|
|
23050
|
+
confinementGate?.stop?.();
|
|
22468
23051
|
for (const u2 of unsubscribers) u2();
|
|
22469
23052
|
const SHUTDOWN_DEADLINE_MS = 3e4;
|
|
22470
23053
|
const deadline = setTimeout(() => {
|
|
@@ -22507,7 +23090,7 @@ ${signal} received, shutting down`);
|
|
|
22507
23090
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
22508
23091
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
22509
23092
|
}
|
|
22510
|
-
var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, AUTH_PAUSE_MS, ClaudeRunError, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, sandboxHost, boxedJobs, leaseLost, prefetchOverride, DEFAULT_CLONE_BASE_DIR, JOB_TOKEN_PREFIX, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
|
|
23093
|
+
var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, AUTH_PAUSE_MS, ClaudeRunError, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, sandboxHost, confinementGate, boxedJobs, leaseLost, prefetchOverride, DEFAULT_CLONE_BASE_DIR, JOB_TOKEN_PREFIX, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
|
|
22511
23094
|
var init_run = __esm({
|
|
22512
23095
|
"src/daemon/run.ts"() {
|
|
22513
23096
|
"use strict";
|
|
@@ -22587,6 +23170,7 @@ var init_run = __esm({
|
|
|
22587
23170
|
ctxByJob = /* @__PURE__ */ new Map();
|
|
22588
23171
|
deviceByJob = /* @__PURE__ */ new Map();
|
|
22589
23172
|
sandboxHost = null;
|
|
23173
|
+
confinementGate = null;
|
|
22590
23174
|
boxedJobs = /* @__PURE__ */ new Set();
|
|
22591
23175
|
leaseLost = /* @__PURE__ */ new Set();
|
|
22592
23176
|
prefetchOverride = null;
|
|
@@ -22767,6 +23351,9 @@ ${ANCHOR_VERIFICATION}`;
|
|
|
22767
23351
|
sandboxHost = host;
|
|
22768
23352
|
prefetchOverride = prefetch;
|
|
22769
23353
|
},
|
|
23354
|
+
setConfinementGate(gate) {
|
|
23355
|
+
confinementGate = gate;
|
|
23356
|
+
},
|
|
22770
23357
|
// Daemon-version hooks for tests / introspection. The real value is
|
|
22771
23358
|
// populated at runDaemon boot from package.json; tests can stub.
|
|
22772
23359
|
setDaemonVersion(v) {
|
|
@@ -22779,13 +23366,450 @@ ${ANCHOR_VERIFICATION}`;
|
|
|
22779
23366
|
}
|
|
22780
23367
|
});
|
|
22781
23368
|
|
|
23369
|
+
// src/daemon/sandbox/pfAnchor.ts
|
|
23370
|
+
function renderAnchor() {
|
|
23371
|
+
const slotLines = Array.from({ length: MAX_SLOTS }, (_, i2) => {
|
|
23372
|
+
const s = slotFor(i2);
|
|
23373
|
+
return `pass in quick inet proto tcp from ${s.subnet} to ${s.gateway} port ${PORTS} keep state`;
|
|
23374
|
+
});
|
|
23375
|
+
return [
|
|
23376
|
+
`# ${ANCHOR_NAME} \u2014 installed by \`archme daemon sandbox setup\` (L2 \xA78.2).`,
|
|
23377
|
+
"# DHCP inside each slot",
|
|
23378
|
+
"pass in quick inet proto udp from any port 68 to any port 67",
|
|
23379
|
+
"# each slot may reach only its own gateway, only on the gatekeeper ports",
|
|
23380
|
+
...slotLines,
|
|
23381
|
+
"# everything else from a slot: host services on any host address, forwarding",
|
|
23382
|
+
`block drop in quick inet from ${SLOT_RANGE} to any`,
|
|
23383
|
+
"# the gatekeepers listen on the wildcard: nobody but a slot reaches them",
|
|
23384
|
+
`block drop in quick inet proto tcp from any to any port ${PORTS}`,
|
|
23385
|
+
""
|
|
23386
|
+
].join("\n");
|
|
23387
|
+
}
|
|
23388
|
+
function addAnchorToPfConf(text) {
|
|
23389
|
+
const lines = text.replace(/\n$/, "").split("\n");
|
|
23390
|
+
if (ANCHOR_LINES.every((l) => lines.some((x) => x.trim() === l))) return { text, changed: false };
|
|
23391
|
+
const kept = lines.filter((x) => !ANCHOR_LINES.includes(x.trim()));
|
|
23392
|
+
let at = -1;
|
|
23393
|
+
kept.forEach((l, i2) => {
|
|
23394
|
+
if (!l.trim().startsWith("#") && l.includes("com.apple")) at = i2;
|
|
23395
|
+
});
|
|
23396
|
+
const insertAt = at >= 0 ? at + 1 : kept.length;
|
|
23397
|
+
kept.splice(insertAt, 0, ...ANCHOR_LINES);
|
|
23398
|
+
return { text: `${kept.join("\n")}
|
|
23399
|
+
`, changed: true };
|
|
23400
|
+
}
|
|
23401
|
+
function renderPfLaunchDaemon() {
|
|
23402
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
23403
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
23404
|
+
<plist version="1.0">
|
|
23405
|
+
<dict>
|
|
23406
|
+
<key>Label</key>
|
|
23407
|
+
<string>${PF_DAEMON_LABEL}</string>
|
|
23408
|
+
<key>ProgramArguments</key>
|
|
23409
|
+
<array>
|
|
23410
|
+
<string>/sbin/pfctl</string>
|
|
23411
|
+
<string>-E</string>
|
|
23412
|
+
<string>-f</string>
|
|
23413
|
+
<string>${PF_CONF_PATH}</string>
|
|
23414
|
+
</array>
|
|
23415
|
+
<key>RunAtLoad</key>
|
|
23416
|
+
<true/>
|
|
23417
|
+
</dict>
|
|
23418
|
+
</plist>
|
|
23419
|
+
`;
|
|
23420
|
+
}
|
|
23421
|
+
function parseDestination(dest) {
|
|
23422
|
+
const [addr, bitsStr] = dest.split("/");
|
|
23423
|
+
if (!addr || !/^\d+(\.\d+){0,3}$/.test(addr)) return null;
|
|
23424
|
+
const octets = addr.split(".");
|
|
23425
|
+
const bits = bitsStr !== void 0 ? Number(bitsStr) : octets.length * 8;
|
|
23426
|
+
while (octets.length < 4) octets.push("0");
|
|
23427
|
+
const base = parseIpv4(octets.join("."));
|
|
23428
|
+
if (base === null || !Number.isInteger(bits) || bits <= 0 || bits > 32) return null;
|
|
23429
|
+
return { base, bits };
|
|
23430
|
+
}
|
|
23431
|
+
function mask(bits) {
|
|
23432
|
+
return bits === 0 ? 0 : ~0 << 32 - bits >>> 0;
|
|
23433
|
+
}
|
|
23434
|
+
function overlaps(a2, b) {
|
|
23435
|
+
const m = mask(Math.min(a2.bits, b.bits));
|
|
23436
|
+
return (a2.base & m) >>> 0 === (b.base & m) >>> 0;
|
|
23437
|
+
}
|
|
23438
|
+
function within(inner, outer) {
|
|
23439
|
+
return inner.bits >= outer.bits && overlaps(inner, outer);
|
|
23440
|
+
}
|
|
23441
|
+
function routeCollisions(netstat, ourSlots2) {
|
|
23442
|
+
const range2 = parseDestination(SLOT_RANGE.replace(".0/21", "/21"));
|
|
23443
|
+
const ours = [...ourSlots2].map((i2) => parseDestination(slotFor(i2).subnet.replace(".0/24", "/24")));
|
|
23444
|
+
const out = [];
|
|
23445
|
+
for (const line of netstat.split("\n")) {
|
|
23446
|
+
const cols = line.trim().split(/\s+/);
|
|
23447
|
+
const net3 = parseDestination(cols[0] ?? "");
|
|
23448
|
+
if (!net3 || !overlaps(net3, range2)) continue;
|
|
23449
|
+
if (ours.some((o2) => within(net3, o2))) continue;
|
|
23450
|
+
out.push(`${cols[0]} via ${cols[3] ?? "?"}`);
|
|
23451
|
+
}
|
|
23452
|
+
return out;
|
|
23453
|
+
}
|
|
23454
|
+
function bakedUnitEnv(plist) {
|
|
23455
|
+
const m = /<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/.exec(plist);
|
|
23456
|
+
if (!m) return {};
|
|
23457
|
+
const out = {};
|
|
23458
|
+
const unescape2 = (s) => s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
23459
|
+
for (const kv of m[1].matchAll(/<key>([^<]*)<\/key>\s*<string>([^<]*)<\/string>/g)) {
|
|
23460
|
+
out[unescape2(kv[1])] = unescape2(kv[2]);
|
|
23461
|
+
}
|
|
23462
|
+
return out;
|
|
23463
|
+
}
|
|
23464
|
+
var ANCHOR_NAME, ANCHOR_PATH, PF_CONF_PATH, PF_CONF_BACKUP_PATH, PF_DAEMON_LABEL, PF_DAEMON_PATH, SLOT_RANGE, PORTS, ANCHOR_LINES;
|
|
23465
|
+
var init_pfAnchor = __esm({
|
|
23466
|
+
"src/daemon/sandbox/pfAnchor.ts"() {
|
|
23467
|
+
"use strict";
|
|
23468
|
+
init_appleContainer();
|
|
23469
|
+
init_slotMap();
|
|
23470
|
+
init_types();
|
|
23471
|
+
ANCHOR_NAME = "com.archme.sandbox";
|
|
23472
|
+
ANCHOR_PATH = `/etc/pf.anchors/${ANCHOR_NAME}`;
|
|
23473
|
+
PF_CONF_PATH = "/etc/pf.conf";
|
|
23474
|
+
PF_CONF_BACKUP_PATH = "/etc/pf.conf.archme-backup";
|
|
23475
|
+
PF_DAEMON_LABEL = "com.archme.pf";
|
|
23476
|
+
PF_DAEMON_PATH = `/Library/LaunchDaemons/${PF_DAEMON_LABEL}.plist`;
|
|
23477
|
+
SLOT_RANGE = `192.168.${SLOT_RANGE_BASE_THIRD_OCTET}.0/21`;
|
|
23478
|
+
PORTS = `{ ${GATE_PORTS.anthropic} ${GATE_PORTS.mcp} ${GATE_PORTS.github} }`;
|
|
23479
|
+
ANCHOR_LINES = [
|
|
23480
|
+
`anchor "${ANCHOR_NAME}"`,
|
|
23481
|
+
`load anchor "${ANCHOR_NAME}" from "${ANCHOR_PATH}"`
|
|
23482
|
+
];
|
|
23483
|
+
}
|
|
23484
|
+
});
|
|
23485
|
+
|
|
23486
|
+
// src/daemon/sandbox/setupCommand.ts
|
|
23487
|
+
var setupCommand_exports = {};
|
|
23488
|
+
__export(setupCommand_exports, {
|
|
23489
|
+
HARDENING_TEXT: () => HARDENING_TEXT,
|
|
23490
|
+
runSandboxSetup: () => runSandboxSetup,
|
|
23491
|
+
runSandboxStatus: () => runSandboxStatus
|
|
23492
|
+
});
|
|
23493
|
+
async function checkHost(io) {
|
|
23494
|
+
const ver = (await io.run("sw_vers", ["-productVersion"])).stdout.trim();
|
|
23495
|
+
const major = Number(ver.split(".")[0]);
|
|
23496
|
+
if (!(major >= 26)) return `macOS 26 or later is required (this is ${ver || "not macOS"})`;
|
|
23497
|
+
const arch = (await io.run("uname", ["-m"])).stdout.trim();
|
|
23498
|
+
if (arch !== "arm64") return `Apple Silicon is required (this is ${arch || "unknown"})`;
|
|
23499
|
+
if (!await runtimeUp(io)) return "Apple `container` is not running; start it with `container system start`";
|
|
23500
|
+
return null;
|
|
23501
|
+
}
|
|
23502
|
+
async function runtimeUp(io) {
|
|
23503
|
+
const s = await io.run("container", ["system", "status"]);
|
|
23504
|
+
return s.exitCode === 0 && /^status\s+running\b/m.test(s.stdout);
|
|
23505
|
+
}
|
|
23506
|
+
async function ourSlots(io) {
|
|
23507
|
+
const nets = await io.listNetworks();
|
|
23508
|
+
const ours = /* @__PURE__ */ new Set();
|
|
23509
|
+
for (let i2 = 0; i2 < MAX_SLOTS; i2++) {
|
|
23510
|
+
const s = slotFor(i2);
|
|
23511
|
+
if (nets.some((n2) => n2.name === s.network && n2.subnet === s.subnet)) ours.add(i2);
|
|
23512
|
+
}
|
|
23513
|
+
return ours;
|
|
23514
|
+
}
|
|
23515
|
+
async function currentMode(io) {
|
|
23516
|
+
const unit = await io.unitEnv();
|
|
23517
|
+
if (unit) return { mode: unit.ARCHME_SANDBOX?.trim() || "off", from: "installed service" };
|
|
23518
|
+
return { mode: io.env.ARCHME_SANDBOX?.trim() || "off", from: "this shell" };
|
|
23519
|
+
}
|
|
23520
|
+
async function runSandboxSetup(io, opts) {
|
|
23521
|
+
const fail2 = (msg) => {
|
|
23522
|
+
io.print(`\u2717 ${msg}`);
|
|
23523
|
+
return 1;
|
|
23524
|
+
};
|
|
23525
|
+
const host = await checkHost(io);
|
|
23526
|
+
if (host) return fail2(host);
|
|
23527
|
+
io.print("\u2713 macOS 26+, Apple Silicon, container running");
|
|
23528
|
+
if (!io.image) return fail2("no sandbox image: this install ships no sandbox-image.json, so set ARCHME_SANDBOX_IMAGE");
|
|
23529
|
+
if ((await io.run("container", ["image", "inspect", io.image])).exitCode !== 0) {
|
|
23530
|
+
io.print(`Pulling ${io.image}\u2026`);
|
|
23531
|
+
const pull = await io.run("container", ["image", "pull", io.image]);
|
|
23532
|
+
if (pull.exitCode !== 0) return fail2(`image pull failed: ${pull.stderr.trim()}`);
|
|
23533
|
+
}
|
|
23534
|
+
io.print(`\u2713 image ${io.image}`);
|
|
23535
|
+
const existing = await io.readToken().catch(() => null);
|
|
23536
|
+
const keep = existing && (opts.yes || await io.confirm(`A Claude token is stored (${existing.source}). Keep it?`));
|
|
23537
|
+
if (keep) {
|
|
23538
|
+
io.print(`\u2713 Claude token (${existing.source})`);
|
|
23539
|
+
} else {
|
|
23540
|
+
io.print("Run `claude setup-token` and paste the token it prints (input is hidden).");
|
|
23541
|
+
const stored = await io.storeToken(await io.promptSecret("Claude setup token: "));
|
|
23542
|
+
io.print(`\u2713 Claude token stored in ${stored.source === "keychain" ? "the macOS keychain" : "a 0600 file"}${stored.note ? ` (${stored.note})` : ""}`);
|
|
23543
|
+
}
|
|
23544
|
+
const netstat = await io.run("netstat", ["-rn", "-f", "inet"]);
|
|
23545
|
+
if (netstat.exitCode !== 0) return fail2(`netstat failed: ${netstat.stderr.trim()}`);
|
|
23546
|
+
const collisions = routeCollisions(netstat.stdout, await ourSlots(io));
|
|
23547
|
+
if (collisions.length) {
|
|
23548
|
+
return fail2(`the slot range 192.168.240.0/21 is already routed: ${collisions.join(", ")}. Free it (a VPN or another VM tool) and re-run.`);
|
|
23549
|
+
}
|
|
23550
|
+
await io.ensureSlots(MAX_SLOTS);
|
|
23551
|
+
io.print(`\u2713 ${MAX_SLOTS} slot networks`);
|
|
23552
|
+
const anchor = renderAnchor();
|
|
23553
|
+
const plist = renderPfLaunchDaemon();
|
|
23554
|
+
const pfConf = await io.readFile(PF_CONF_PATH);
|
|
23555
|
+
if (pfConf === null) return fail2(`cannot read ${PF_CONF_PATH}`);
|
|
23556
|
+
const conf = addAnchorToPfConf(pfConf);
|
|
23557
|
+
const anchorChanged = await io.readFile(ANCHOR_PATH) !== anchor;
|
|
23558
|
+
const plistChanged = await io.readFile(PF_DAEMON_PATH) !== plist;
|
|
23559
|
+
const pfUnchanged = !anchorChanged && !conf.changed && !plistChanged;
|
|
23560
|
+
const reload = [
|
|
23561
|
+
// Not loaded yet on a first run.
|
|
23562
|
+
{ args: ["launchctl", "bootout", `system/${PF_DAEMON_LABEL}`], mayFail: true },
|
|
23563
|
+
// RunAtLoad: this applies the rules now.
|
|
23564
|
+
{ args: ["launchctl", "bootstrap", "system", PF_DAEMON_PATH] }
|
|
23565
|
+
];
|
|
23566
|
+
const runSudo = async (steps) => {
|
|
23567
|
+
for (const s of steps) {
|
|
23568
|
+
io.print(`$ sudo ${s.args.join(" ")}`);
|
|
23569
|
+
const r = await io.sudo(s.args);
|
|
23570
|
+
if (r.exitCode !== 0 && !s.mayFail) return `\`sudo ${s.args.join(" ")}\` failed (exit ${r.exitCode}); nothing after it ran`;
|
|
23571
|
+
}
|
|
23572
|
+
await io.sleep(2e3);
|
|
23573
|
+
return null;
|
|
23574
|
+
};
|
|
23575
|
+
if (pfUnchanged) {
|
|
23576
|
+
io.print("\u2713 pf: already installed");
|
|
23577
|
+
} else {
|
|
23578
|
+
io.print(`
|
|
23579
|
+
These rules go in ${ANCHOR_PATH}:
|
|
23580
|
+
`);
|
|
23581
|
+
for (const l of anchor.trimEnd().split("\n")) io.print(` ${l}`);
|
|
23582
|
+
if (conf.changed) {
|
|
23583
|
+
io.print(`
|
|
23584
|
+
${PF_CONF_PATH} gains (a backup goes to ${PF_CONF_BACKUP_PATH}):
|
|
23585
|
+
`);
|
|
23586
|
+
const before = new Set(pfConf.split("\n"));
|
|
23587
|
+
for (const l of conf.text.split("\n")) if (l && !before.has(l)) io.print(` + ${l}`);
|
|
23588
|
+
}
|
|
23589
|
+
io.print(`
|
|
23590
|
+
${PF_DAEMON_PATH} enables pf and loads ${PF_CONF_PATH} at every boot (${PF_DAEMON_LABEL}).
|
|
23591
|
+
`);
|
|
23592
|
+
if (!opts.yes && !await io.confirm("Install these with sudo?")) return fail2("pf not installed");
|
|
23593
|
+
const anchorTmp = await io.writeTemp("com.archme.sandbox", anchor);
|
|
23594
|
+
const confTmp = await io.writeTemp("pf.conf", conf.text);
|
|
23595
|
+
const plistTmp = await io.writeTemp(`${PF_DAEMON_LABEL}.plist`, plist);
|
|
23596
|
+
const steps = [
|
|
23597
|
+
{ args: sudoInstall(anchorTmp, ANCHOR_PATH) },
|
|
23598
|
+
// Validate the new pf.conf (it loads the anchor just installed) before it replaces the old.
|
|
23599
|
+
{ args: ["pfctl", "-nf", confTmp] },
|
|
23600
|
+
...conf.changed ? [{ args: ["cp", PF_CONF_PATH, PF_CONF_BACKUP_PATH] }, { args: sudoInstall(confTmp, PF_CONF_PATH) }] : [],
|
|
23601
|
+
{ args: sudoInstall(plistTmp, PF_DAEMON_PATH) },
|
|
23602
|
+
...reload
|
|
23603
|
+
];
|
|
23604
|
+
const err4 = await runSudo(steps);
|
|
23605
|
+
if (err4) return fail2(err4);
|
|
23606
|
+
io.print("\u2713 pf installed");
|
|
23607
|
+
}
|
|
23608
|
+
io.print("");
|
|
23609
|
+
let code = await runSandboxStatus(io);
|
|
23610
|
+
if (code !== 0 && pfUnchanged && (opts.yes || await io.confirm("Reload pf from the installed files with sudo?"))) {
|
|
23611
|
+
const err4 = await runSudo(reload);
|
|
23612
|
+
if (err4) return fail2(err4);
|
|
23613
|
+
io.print("");
|
|
23614
|
+
code = await runSandboxStatus(io);
|
|
23615
|
+
}
|
|
23616
|
+
const { mode, from } = await currentMode(io);
|
|
23617
|
+
io.print("");
|
|
23618
|
+
io.print(`ARCHME_SANDBOX is ${mode} (${from}); setup does not change it.`);
|
|
23619
|
+
io.print("To require confinement for every job: set ARCHME_SANDBOX=required in the daemon's environment, then `archme daemon restart`.");
|
|
23620
|
+
for (const l of HARDENING_TEXT) io.print(l);
|
|
23621
|
+
return code;
|
|
23622
|
+
}
|
|
23623
|
+
async function runSandboxStatus(io) {
|
|
23624
|
+
let ok = true;
|
|
23625
|
+
const line = (good, name, detail) => {
|
|
23626
|
+
if (!good) ok = false;
|
|
23627
|
+
io.print(`${good ? "\u2713" : "\u2717"} ${name.padEnd(9)} ${detail}`);
|
|
23628
|
+
};
|
|
23629
|
+
const up = await runtimeUp(io);
|
|
23630
|
+
line(up, "runtime", up ? "container running" : "container is not running");
|
|
23631
|
+
if (io.image) {
|
|
23632
|
+
const present = up && (await io.run("container", ["image", "inspect", io.image])).exitCode === 0;
|
|
23633
|
+
line(present, "image", `${io.image}${present ? "" : " (not present: run `archme daemon sandbox setup`)"}`);
|
|
23634
|
+
} else {
|
|
23635
|
+
line(false, "image", "none configured (no sandbox-image.json and no ARCHME_SANDBOX_IMAGE)");
|
|
23636
|
+
}
|
|
23637
|
+
try {
|
|
23638
|
+
const t = await io.readToken();
|
|
23639
|
+
line(true, "token", t.source === "keychain" ? "macOS keychain" : t.source === "file-override" ? "ARCHME_SANDBOX_CLAUDE_TOKEN_FILE" : "file");
|
|
23640
|
+
} catch (e) {
|
|
23641
|
+
line(false, "token", e.message);
|
|
23642
|
+
}
|
|
23643
|
+
const ours = up ? await ourSlots(io) : /* @__PURE__ */ new Set();
|
|
23644
|
+
const missing = Array.from({ length: MAX_SLOTS }, (_, i2) => i2).filter((i2) => !ours.has(i2)).map((i2) => slotFor(i2).network);
|
|
23645
|
+
line(missing.length === 0, "slots", missing.length ? `missing: ${missing.join(", ")}` : `${MAX_SLOTS} slot networks`);
|
|
23646
|
+
let probeSlot = null;
|
|
23647
|
+
if (up) {
|
|
23648
|
+
const busy = new Set((await io.listBoxes()).map((b) => b.labels[SLOT_LABEL]).filter((x) => !!x));
|
|
23649
|
+
for (let i2 = MAX_SLOTS - 1; i2 >= 0; i2--) {
|
|
23650
|
+
if (ours.has(i2) && !busy.has(String(i2))) {
|
|
23651
|
+
probeSlot = slotFor(i2);
|
|
23652
|
+
break;
|
|
23653
|
+
}
|
|
23654
|
+
}
|
|
23655
|
+
}
|
|
23656
|
+
if (!probeSlot) {
|
|
23657
|
+
line(false, "probe", "not run: no free slot network");
|
|
23658
|
+
} else {
|
|
23659
|
+
const r = await io.probe(probeSlot);
|
|
23660
|
+
line(r.ok, "probe", formatConfinementReport(r));
|
|
23661
|
+
for (const [id, o2] of Object.entries(r.outcomes)) io.print(` ${id.padEnd(15)} ${o2}`);
|
|
23662
|
+
}
|
|
23663
|
+
const { mode, from } = await currentMode(io);
|
|
23664
|
+
io.print(` mode ${mode} (${from})`);
|
|
23665
|
+
return ok ? 0 : 1;
|
|
23666
|
+
}
|
|
23667
|
+
var HARDENING_TEXT, sudoInstall;
|
|
23668
|
+
var init_setupCommand = __esm({
|
|
23669
|
+
"src/daemon/sandbox/setupCommand.ts"() {
|
|
23670
|
+
"use strict";
|
|
23671
|
+
init_appleContainer();
|
|
23672
|
+
init_confinement();
|
|
23673
|
+
init_pfAnchor();
|
|
23674
|
+
init_types();
|
|
23675
|
+
HARDENING_TEXT = [
|
|
23676
|
+
"Host hardening (L2 \xA78.5), by hand:",
|
|
23677
|
+
" - turn off SSH password authentication (PasswordAuthentication no in /etc/ssh/sshd_config)",
|
|
23678
|
+
" - keep AirPlay Receiver off (System Settings \u2192 General \u2192 AirDrop & Handoff)"
|
|
23679
|
+
];
|
|
23680
|
+
sudoInstall = (from, to) => ["install", "-m", "644", "-o", "root", "-g", "wheel", from, to];
|
|
23681
|
+
}
|
|
23682
|
+
});
|
|
23683
|
+
|
|
23684
|
+
// src/daemon/sandbox/setupIo.ts
|
|
23685
|
+
var setupIo_exports = {};
|
|
23686
|
+
__export(setupIo_exports, {
|
|
23687
|
+
createSetupIo: () => createSetupIo
|
|
23688
|
+
});
|
|
23689
|
+
import { mkdtemp as mkdtemp5, readFile as readFile15, writeFile as writeFile14 } from "node:fs/promises";
|
|
23690
|
+
import { networkInterfaces as networkInterfaces2, tmpdir as tmpdir5 } from "node:os";
|
|
23691
|
+
import { join as join25 } from "node:path";
|
|
23692
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
23693
|
+
import { Writable as Writable4 } from "node:stream";
|
|
23694
|
+
function ask(question, hidden2 = false) {
|
|
23695
|
+
let muted = false;
|
|
23696
|
+
const output = new Writable4({
|
|
23697
|
+
write(chunk, _enc, cb) {
|
|
23698
|
+
if (!muted) process.stdout.write(chunk);
|
|
23699
|
+
cb();
|
|
23700
|
+
}
|
|
23701
|
+
});
|
|
23702
|
+
const rl = createInterface2({ input: process.stdin, output, terminal: true });
|
|
23703
|
+
return new Promise((resolve7) => {
|
|
23704
|
+
rl.question(question, (answer) => {
|
|
23705
|
+
rl.close();
|
|
23706
|
+
if (hidden2) process.stdout.write("\n");
|
|
23707
|
+
resolve7(answer);
|
|
23708
|
+
});
|
|
23709
|
+
muted = hidden2;
|
|
23710
|
+
});
|
|
23711
|
+
}
|
|
23712
|
+
function createSetupIo(env = process.env) {
|
|
23713
|
+
let image = null;
|
|
23714
|
+
try {
|
|
23715
|
+
image = resolveSandboxImage(env, () => readShippedImage());
|
|
23716
|
+
} catch {
|
|
23717
|
+
image = null;
|
|
23718
|
+
}
|
|
23719
|
+
const runtime = new AppleContainerRuntime({ image: image ?? void 0 });
|
|
23720
|
+
const token = sandboxTokenLocation(env);
|
|
23721
|
+
let tempDir = null;
|
|
23722
|
+
return {
|
|
23723
|
+
env,
|
|
23724
|
+
image,
|
|
23725
|
+
run: async (cmd, args) => {
|
|
23726
|
+
const r = await execa(cmd, args, { reject: false, stdin: "ignore" });
|
|
23727
|
+
return {
|
|
23728
|
+
exitCode: r.exitCode ?? -1,
|
|
23729
|
+
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
23730
|
+
stderr: typeof r.stderr === "string" ? r.stderr : r.message ?? ""
|
|
23731
|
+
};
|
|
23732
|
+
},
|
|
23733
|
+
// On the terminal: sudo may ask for a password.
|
|
23734
|
+
sudo: async (args) => {
|
|
23735
|
+
const r = await execa("sudo", args, { reject: false, stdio: "inherit" });
|
|
23736
|
+
return { exitCode: r.exitCode ?? -1, stdout: "", stderr: "" };
|
|
23737
|
+
},
|
|
23738
|
+
print: (line) => console.log(line),
|
|
23739
|
+
confirm: async (q) => !/^n/i.test((await ask(`${q} [Y/n] `)).trim()),
|
|
23740
|
+
promptSecret: (q) => ask(q, true),
|
|
23741
|
+
readFile: (p) => readFile15(p, "utf8").catch(() => null),
|
|
23742
|
+
writeTemp: async (name, content) => {
|
|
23743
|
+
tempDir ??= await mkdtemp5(join25(tmpdir5(), "archme-sandbox-setup-"));
|
|
23744
|
+
const p = join25(tempDir, name);
|
|
23745
|
+
await writeFile14(p, content, { mode: 420 });
|
|
23746
|
+
return p;
|
|
23747
|
+
},
|
|
23748
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
23749
|
+
listNetworks: () => runtime.listNetworks(),
|
|
23750
|
+
ensureSlots: async (n2) => {
|
|
23751
|
+
await runtime.ensureSlots(n2);
|
|
23752
|
+
},
|
|
23753
|
+
listBoxes: () => runtime.list(),
|
|
23754
|
+
readToken: () => readSandboxClaudeToken({ claudeTokenPath: token.path, claudeTokenOverride: token.override }),
|
|
23755
|
+
storeToken: (t) => storeSandboxClaudeToken(t, { path: token.path, override: token.override }),
|
|
23756
|
+
probe: async (slot) => {
|
|
23757
|
+
if (!image) return { ok: false, slot: slot.index, failures: ["no sandbox image"], outcomes: {}, ms: 0 };
|
|
23758
|
+
const gates = new GatekeeperSet({
|
|
23759
|
+
slots: Array.from({ length: MAX_SLOTS }, (_, i2) => slotFor(i2)),
|
|
23760
|
+
getClaudeToken: () => "status-probe-has-no-token",
|
|
23761
|
+
mcpUpstream: "http://127.0.0.1:9/functions/v1/mcp",
|
|
23762
|
+
remint: async () => {
|
|
23763
|
+
throw new Error("no job");
|
|
23764
|
+
},
|
|
23765
|
+
logger: () => {
|
|
23766
|
+
}
|
|
23767
|
+
});
|
|
23768
|
+
let own = false;
|
|
23769
|
+
try {
|
|
23770
|
+
await gates.start();
|
|
23771
|
+
own = true;
|
|
23772
|
+
} catch {
|
|
23773
|
+
console.log(" (the daemon's gatekeepers are answering)");
|
|
23774
|
+
}
|
|
23775
|
+
try {
|
|
23776
|
+
return await runConfinementProbe({ runtime, image }, slot, primaryLanIpv4(networkInterfaces2()));
|
|
23777
|
+
} finally {
|
|
23778
|
+
if (own) await gates.stop();
|
|
23779
|
+
}
|
|
23780
|
+
},
|
|
23781
|
+
unitEnv: async () => {
|
|
23782
|
+
if (process.platform !== "darwin") return null;
|
|
23783
|
+
const plist = await readFile15(resolveUnitPath("darwin", env), "utf8").catch(() => null);
|
|
23784
|
+
return plist === null ? null : bakedUnitEnv(plist);
|
|
23785
|
+
}
|
|
23786
|
+
};
|
|
23787
|
+
}
|
|
23788
|
+
var init_setupIo = __esm({
|
|
23789
|
+
"src/daemon/sandbox/setupIo.ts"() {
|
|
23790
|
+
"use strict";
|
|
23791
|
+
init_execa();
|
|
23792
|
+
init_paths();
|
|
23793
|
+
init_appleContainer();
|
|
23794
|
+
init_claudeToken();
|
|
23795
|
+
init_confinement();
|
|
23796
|
+
init_imageManifest();
|
|
23797
|
+
init_pfAnchor();
|
|
23798
|
+
init_setting();
|
|
23799
|
+
init_slots();
|
|
23800
|
+
init_types();
|
|
23801
|
+
}
|
|
23802
|
+
});
|
|
23803
|
+
|
|
22782
23804
|
// src/daemon/service/dispatch.ts
|
|
22783
23805
|
var dispatch_exports2 = {};
|
|
22784
23806
|
__export(dispatch_exports2, {
|
|
22785
23807
|
dispatchDaemon: () => dispatchDaemon
|
|
22786
23808
|
});
|
|
22787
23809
|
import { rm as rm9 } from "node:fs/promises";
|
|
22788
|
-
async function dispatchDaemon(route2) {
|
|
23810
|
+
async function dispatchDaemon(route2, deps = {}) {
|
|
23811
|
+
const { platform: platform3, env } = deps;
|
|
23812
|
+
const controller = () => deps.controller ?? pickController(platform3, env);
|
|
22789
23813
|
switch (route2.action) {
|
|
22790
23814
|
case "help":
|
|
22791
23815
|
printDaemonHelp();
|
|
@@ -22803,7 +23827,7 @@ async function dispatchDaemon(route2) {
|
|
|
22803
23827
|
}
|
|
22804
23828
|
case "start": {
|
|
22805
23829
|
try {
|
|
22806
|
-
const out = await runStartFlow({ noLogin: route2.noLogin });
|
|
23830
|
+
const out = await runStartFlow({ noLogin: route2.noLogin, controller: controller(), platform: platform3, env });
|
|
22807
23831
|
console.log(
|
|
22808
23832
|
`${out.result.actions.includes("installed") ? "Installing " : out.result.actions.includes("reinstalled") ? "Reinstalling " : ""}service\u2026`
|
|
22809
23833
|
);
|
|
@@ -22816,58 +23840,82 @@ async function dispatchDaemon(route2) {
|
|
|
22816
23840
|
console.error(String(err4.message));
|
|
22817
23841
|
return 75;
|
|
22818
23842
|
}
|
|
23843
|
+
if (err4 instanceof CredentialStoreUnreadableError) {
|
|
23844
|
+
console.error(err4.message);
|
|
23845
|
+
console.error(
|
|
23846
|
+
"The daemon service is NOT running (no service is installed, so nothing vouches for an earlier sign-in). Unlock the keychain and run `archme daemon start` again, or run it from the desktop session."
|
|
23847
|
+
);
|
|
23848
|
+
return 1;
|
|
23849
|
+
}
|
|
22819
23850
|
console.error(String(err4));
|
|
22820
23851
|
return 1;
|
|
22821
23852
|
}
|
|
22822
23853
|
}
|
|
22823
23854
|
case "stop": {
|
|
22824
|
-
const c3 =
|
|
23855
|
+
const c3 = controller();
|
|
22825
23856
|
await orchestrateStop(c3);
|
|
22826
23857
|
console.log("Stopped.");
|
|
22827
23858
|
return 0;
|
|
22828
23859
|
}
|
|
22829
23860
|
case "restart": {
|
|
22830
23861
|
try {
|
|
22831
|
-
await ensureStartPrerequisites({ noLogin: route2.noLogin });
|
|
23862
|
+
await ensureStartPrerequisites({ noLogin: route2.noLogin, controller: controller(), platform: platform3, env });
|
|
22832
23863
|
} catch (err4) {
|
|
22833
23864
|
if (err4 instanceof NotSignedInError) {
|
|
22834
23865
|
console.error(String(err4.message));
|
|
22835
23866
|
console.error("(daemon left running unchanged)");
|
|
22836
23867
|
return 75;
|
|
22837
23868
|
}
|
|
22838
|
-
console.error(String(err4));
|
|
23869
|
+
console.error(err4 instanceof CredentialStoreUnreadableError ? err4.message : String(err4));
|
|
22839
23870
|
console.error("(daemon left running unchanged)");
|
|
22840
23871
|
return 1;
|
|
22841
23872
|
}
|
|
22842
|
-
const c3 =
|
|
23873
|
+
const c3 = controller();
|
|
22843
23874
|
await orchestrateStop(c3);
|
|
22844
|
-
const out = await runStartFlow({
|
|
23875
|
+
const out = await runStartFlow({
|
|
23876
|
+
noLogin: route2.noLogin,
|
|
23877
|
+
controller: c3,
|
|
23878
|
+
platform: platform3,
|
|
23879
|
+
env,
|
|
23880
|
+
isSignedIn: async () => true
|
|
23881
|
+
});
|
|
22845
23882
|
console.log(`Restarted (${out.result.actions.join(" \u2192 ")}).`);
|
|
22846
23883
|
return 0;
|
|
22847
23884
|
}
|
|
22848
23885
|
case "status": {
|
|
22849
|
-
const c3 = pickController();
|
|
22850
23886
|
const status = await gatherStatus({
|
|
22851
|
-
controller:
|
|
23887
|
+
controller: controller(),
|
|
22852
23888
|
loadCredentials: defaultLoadCredentials,
|
|
22853
23889
|
loadSupabaseState: defaultLoadSupabaseState,
|
|
22854
|
-
loadUptime: defaultLoadUptime
|
|
23890
|
+
loadUptime: defaultLoadUptime,
|
|
23891
|
+
platform: platform3,
|
|
23892
|
+
env
|
|
22855
23893
|
});
|
|
22856
23894
|
const out = renderStatus(status);
|
|
22857
|
-
process.stdout.write(out.rendered);
|
|
23895
|
+
(deps.stdout ?? process.stdout).write(out.rendered);
|
|
22858
23896
|
return out.exitCode;
|
|
22859
23897
|
}
|
|
23898
|
+
case "sandbox-setup": {
|
|
23899
|
+
const { runSandboxSetup: runSandboxSetup2 } = await Promise.resolve().then(() => (init_setupCommand(), setupCommand_exports));
|
|
23900
|
+
const { createSetupIo: createSetupIo2 } = await Promise.resolve().then(() => (init_setupIo(), setupIo_exports));
|
|
23901
|
+
return await runSandboxSetup2(createSetupIo2(), { yes: route2.yes });
|
|
23902
|
+
}
|
|
23903
|
+
case "sandbox-status": {
|
|
23904
|
+
const { runSandboxStatus: runSandboxStatus2 } = await Promise.resolve().then(() => (init_setupCommand(), setupCommand_exports));
|
|
23905
|
+
const { createSetupIo: createSetupIo2 } = await Promise.resolve().then(() => (init_setupIo(), setupIo_exports));
|
|
23906
|
+
return await runSandboxStatus2(createSetupIo2());
|
|
23907
|
+
}
|
|
22860
23908
|
case "logs":
|
|
22861
23909
|
await tailLog({ lines: route2.lines, follow: route2.follow });
|
|
22862
23910
|
return 0;
|
|
22863
23911
|
case "enable": {
|
|
22864
|
-
const c3 =
|
|
23912
|
+
const c3 = controller();
|
|
22865
23913
|
await c3.enable();
|
|
22866
23914
|
console.log("Enabled at login.");
|
|
22867
23915
|
return 0;
|
|
22868
23916
|
}
|
|
22869
23917
|
case "disable": {
|
|
22870
|
-
const c3 =
|
|
23918
|
+
const c3 = controller();
|
|
22871
23919
|
await c3.disable();
|
|
22872
23920
|
console.log("Disabled at login. Existing instance keeps running until `archme daemon stop`.");
|
|
22873
23921
|
return 0;
|
|
@@ -22878,7 +23926,7 @@ async function dispatchDaemon(route2) {
|
|
|
22878
23926
|
return 0;
|
|
22879
23927
|
}
|
|
22880
23928
|
case "uninstall": {
|
|
22881
|
-
const c3 =
|
|
23929
|
+
const c3 = controller();
|
|
22882
23930
|
await orchestrateStop(c3);
|
|
22883
23931
|
await c3.uninstall();
|
|
22884
23932
|
if (route2.purge) {
|
|
@@ -22900,7 +23948,13 @@ async function defaultLoadCredentials() {
|
|
|
22900
23948
|
return null;
|
|
22901
23949
|
}
|
|
22902
23950
|
const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
22903
|
-
|
|
23951
|
+
let credentials;
|
|
23952
|
+
try {
|
|
23953
|
+
({ credentials } = await loadCredentials2(supabaseUrl));
|
|
23954
|
+
} catch (err4) {
|
|
23955
|
+
if (err4 instanceof CredentialStoreUnreadableError) return { unreadable: err4.message };
|
|
23956
|
+
throw err4;
|
|
23957
|
+
}
|
|
22904
23958
|
if (!credentials) return null;
|
|
22905
23959
|
const email = decodeJwtEmail(credentials.access_token) ?? credentials.user_id ?? null;
|
|
22906
23960
|
return { email, deviceId: credentials.device_id ?? null };
|
|
@@ -22923,8 +23977,8 @@ async function defaultLoadSupabaseState(_deviceId) {
|
|
|
22923
23977
|
async function defaultLoadUptime(pid) {
|
|
22924
23978
|
if (process.platform === "linux") {
|
|
22925
23979
|
try {
|
|
22926
|
-
const { readFile:
|
|
22927
|
-
const stat12 = await
|
|
23980
|
+
const { readFile: readFile20 } = await import("node:fs/promises");
|
|
23981
|
+
const stat12 = await readFile20(`/proc/${pid}/stat`, "utf8");
|
|
22928
23982
|
const parts = stat12.split(") ").pop()?.split(" ") ?? [];
|
|
22929
23983
|
const ticks = Number(parts[19]);
|
|
22930
23984
|
if (!Number.isFinite(ticks)) return null;
|
|
@@ -22967,6 +24021,8 @@ Usage:
|
|
|
22967
24021
|
archme daemon install [--no-login] Write unit file, do not load
|
|
22968
24022
|
archme daemon uninstall [--purge] Unload + remove unit (logs kept; --purge removes them too)
|
|
22969
24023
|
archme daemon run Foreground worker (what the service unit invokes)
|
|
24024
|
+
archme daemon sandbox setup [--yes] Once per machine: image, Claude token, slot networks, pf (sudo)
|
|
24025
|
+
archme daemon sandbox status Check the sandbox confinement holds (runs a probe box)
|
|
22970
24026
|
`);
|
|
22971
24027
|
}
|
|
22972
24028
|
var init_dispatch2 = __esm({
|
|
@@ -22977,6 +24033,7 @@ var init_dispatch2 = __esm({
|
|
|
22977
24033
|
init_logs();
|
|
22978
24034
|
init_status();
|
|
22979
24035
|
init_paths();
|
|
24036
|
+
init_credentials();
|
|
22980
24037
|
}
|
|
22981
24038
|
});
|
|
22982
24039
|
|
|
@@ -23035,6 +24092,7 @@ async function createAuthedClient(opts) {
|
|
|
23035
24092
|
try {
|
|
23036
24093
|
loaded = await loadStoredAndRefresh();
|
|
23037
24094
|
} catch (err4) {
|
|
24095
|
+
if (err4 instanceof CredentialStoreError) throw err4;
|
|
23038
24096
|
const msg = err4 instanceof Error ? err4.message : String(err4);
|
|
23039
24097
|
throw new Error(`${msg}
|
|
23040
24098
|
\u2192 run \`archme login\` first`);
|
|
@@ -23063,6 +24121,7 @@ var init_authedClient = __esm({
|
|
|
23063
24121
|
"use strict";
|
|
23064
24122
|
init_bearer();
|
|
23065
24123
|
init_identity();
|
|
24124
|
+
init_credentials();
|
|
23066
24125
|
SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
|
|
23067
24126
|
SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
|
|
23068
24127
|
ORCHESTRATED_SCAN_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -23114,8 +24173,8 @@ __export(open_exports, {
|
|
|
23114
24173
|
runOpen: () => runOpen,
|
|
23115
24174
|
safeNextPath: () => safeNextPath
|
|
23116
24175
|
});
|
|
23117
|
-
import { readFile as
|
|
23118
|
-
import { join as
|
|
24176
|
+
import { readFile as readFile16 } from "node:fs/promises";
|
|
24177
|
+
import { join as join26 } from "node:path";
|
|
23119
24178
|
function safeNextPath(path7) {
|
|
23120
24179
|
if (!path7 || !path7.startsWith("/") || path7.startsWith("//") || path7.startsWith("/\\")) return "/";
|
|
23121
24180
|
return path7;
|
|
@@ -23202,7 +24261,7 @@ async function defaultOpenBrowser(url) {
|
|
|
23202
24261
|
}
|
|
23203
24262
|
async function defaultReadMcpJson(cwd) {
|
|
23204
24263
|
try {
|
|
23205
|
-
return await
|
|
24264
|
+
return await readFile16(join26(cwd, ".mcp.json"), "utf8");
|
|
23206
24265
|
} catch {
|
|
23207
24266
|
return null;
|
|
23208
24267
|
}
|
|
@@ -23299,7 +24358,14 @@ function pickKey(keys, handle, active) {
|
|
|
23299
24358
|
}
|
|
23300
24359
|
async function runKey(cmd, deps) {
|
|
23301
24360
|
const keys = await deps.listKeys();
|
|
23302
|
-
|
|
24361
|
+
let active;
|
|
24362
|
+
try {
|
|
24363
|
+
active = await deps.loadActive();
|
|
24364
|
+
} catch (err4) {
|
|
24365
|
+
if (!(err4 instanceof CredentialStoreError)) throw err4;
|
|
24366
|
+
deps.error(`${err4.message} Which key is signed in can't be shown.`);
|
|
24367
|
+
active = null;
|
|
24368
|
+
}
|
|
23303
24369
|
if (cmd.action === "list") {
|
|
23304
24370
|
if (keys.length === 0) {
|
|
23305
24371
|
deps.log("No saved keys on this machine.");
|
|
@@ -23369,6 +24435,7 @@ Usage: archme key [list|show|forget] [--handle <handle>] [--yes]`);
|
|
|
23369
24435
|
var init_key = __esm({
|
|
23370
24436
|
"src/account/key.ts"() {
|
|
23371
24437
|
"use strict";
|
|
24438
|
+
init_credentials();
|
|
23372
24439
|
}
|
|
23373
24440
|
});
|
|
23374
24441
|
|
|
@@ -23562,8 +24629,8 @@ var init_transcript_signals = __esm({
|
|
|
23562
24629
|
});
|
|
23563
24630
|
|
|
23564
24631
|
// src/scan/report.ts
|
|
23565
|
-
import { mkdir as
|
|
23566
|
-
import { dirname as
|
|
24632
|
+
import { mkdir as mkdir15, writeFile as writeFile15 } from "node:fs/promises";
|
|
24633
|
+
import { dirname as dirname17 } from "node:path";
|
|
23567
24634
|
async function buildReport(input) {
|
|
23568
24635
|
return {
|
|
23569
24636
|
schema_version: REPORT_SCHEMA_VERSION,
|
|
@@ -23597,10 +24664,10 @@ async function buildReport(input) {
|
|
|
23597
24664
|
};
|
|
23598
24665
|
}
|
|
23599
24666
|
async function writeReport(reportPath, orphansTxtPath, report, orphansFull) {
|
|
23600
|
-
await
|
|
23601
|
-
await
|
|
23602
|
-
await
|
|
23603
|
-
await
|
|
24667
|
+
await mkdir15(dirname17(reportPath), { recursive: true });
|
|
24668
|
+
await writeFile15(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
24669
|
+
await mkdir15(dirname17(orphansTxtPath), { recursive: true });
|
|
24670
|
+
await writeFile15(
|
|
23604
24671
|
orphansTxtPath,
|
|
23605
24672
|
orphansFull.join("\n") + (orphansFull.length ? "\n" : ""),
|
|
23606
24673
|
"utf8"
|
|
@@ -23644,8 +24711,8 @@ var init_claimDefaultProject = __esm({
|
|
|
23644
24711
|
// src/scan/run-scan.ts
|
|
23645
24712
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
23646
24713
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
23647
|
-
import { mkdir as
|
|
23648
|
-
import { basename as basename2, dirname as
|
|
24714
|
+
import { mkdir as mkdir16, readFile as readFile17, writeFile as writeFile16 } from "node:fs/promises";
|
|
24715
|
+
import { basename as basename2, dirname as dirname18, join as join27 } from "node:path";
|
|
23649
24716
|
function modeInstruction(opts) {
|
|
23650
24717
|
const line = modeLine(opts);
|
|
23651
24718
|
if (opts.mode === "orchestrator" && opts.plan) {
|
|
@@ -23701,13 +24768,13 @@ Use scan-prompt variant \`${promptVariant}\` \u2014 pass \`variant: "${promptVar
|
|
|
23701
24768
|
return base;
|
|
23702
24769
|
}
|
|
23703
24770
|
async function writeScanSession(repoPath, session) {
|
|
23704
|
-
const path7 =
|
|
23705
|
-
await
|
|
23706
|
-
await
|
|
24771
|
+
const path7 = join27(repoPath, SCAN_SESSION_FILE);
|
|
24772
|
+
await mkdir16(dirname18(path7), { recursive: true });
|
|
24773
|
+
await writeFile16(path7, JSON.stringify(session, null, 2) + "\n");
|
|
23707
24774
|
}
|
|
23708
24775
|
async function readScanSession(repoPath) {
|
|
23709
|
-
const path7 =
|
|
23710
|
-
const raw = await
|
|
24776
|
+
const path7 = join27(repoPath, SCAN_SESSION_FILE);
|
|
24777
|
+
const raw = await readFile17(path7, "utf8").catch(() => null);
|
|
23711
24778
|
if (raw === null) throw new Error(`no scan session to resume at ${path7} \u2014 launch it instead`);
|
|
23712
24779
|
let parsed;
|
|
23713
24780
|
try {
|
|
@@ -24098,9 +25165,9 @@ async function runScan(opts, deps = {}) {
|
|
|
24098
25165
|
// Incompatible with an interactive TTY session.
|
|
24099
25166
|
streamJson: !interactive
|
|
24100
25167
|
});
|
|
24101
|
-
const transcriptDir =
|
|
24102
|
-
const transcriptPath =
|
|
24103
|
-
await
|
|
25168
|
+
const transcriptDir = join27(opts.repoPath, ".archme", "scan");
|
|
25169
|
+
const transcriptPath = join27(transcriptDir, "transcript.log");
|
|
25170
|
+
await mkdir16(transcriptDir, { recursive: true });
|
|
24104
25171
|
const transcriptStream = createWriteStream2(transcriptPath, { flags: opts.resume ? "a" : "w" });
|
|
24105
25172
|
let finalResultText = "";
|
|
24106
25173
|
const echo = opts.echo ?? ((line) => process.stdout.write(line + "\n"));
|
|
@@ -24143,9 +25210,9 @@ async function runScan(opts, deps = {}) {
|
|
|
24143
25210
|
await new Promise((resolve7, reject) => {
|
|
24144
25211
|
transcriptStream.end((err4) => err4 ? reject(err4) : resolve7());
|
|
24145
25212
|
});
|
|
24146
|
-
const reportPath =
|
|
24147
|
-
const orphansTxtPath =
|
|
24148
|
-
const transcript = await
|
|
25213
|
+
const reportPath = join27(transcriptDir, "report.json");
|
|
25214
|
+
const orphansTxtPath = join27(transcriptDir, "orphans.txt");
|
|
25215
|
+
const transcript = await readFile17(transcriptPath, "utf8").catch(() => "");
|
|
24149
25216
|
signals2 = extractTranscriptSignals(transcript);
|
|
24150
25217
|
const architecture = await fetchArchitectureDetail(supabase, resolved2.projectId, branchName2);
|
|
24151
25218
|
const orphans = await classifyOrphans({
|
|
@@ -24299,7 +25366,7 @@ var init_scanOutcome = __esm({
|
|
|
24299
25366
|
|
|
24300
25367
|
// src/init/repoSize.ts
|
|
24301
25368
|
import { stat as stat9 } from "node:fs/promises";
|
|
24302
|
-
import { extname, join as
|
|
25369
|
+
import { extname, join as join28 } from "node:path";
|
|
24303
25370
|
async function measureRepo(repoPath) {
|
|
24304
25371
|
const files = await gitListFiles(repoPath);
|
|
24305
25372
|
if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
|
|
@@ -24308,7 +25375,7 @@ async function measureRepo(repoPath) {
|
|
|
24308
25375
|
for (const rel of files) {
|
|
24309
25376
|
if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
|
|
24310
25377
|
try {
|
|
24311
|
-
const s = await stat9(
|
|
25378
|
+
const s = await stat9(join28(repoPath, rel));
|
|
24312
25379
|
if (!s.isFile()) continue;
|
|
24313
25380
|
sourceFiles++;
|
|
24314
25381
|
sourceBytes += s.size;
|
|
@@ -24944,10 +26011,10 @@ __export(localWatch_exports, {
|
|
|
24944
26011
|
watchLocalRepoRow: () => watchLocalRepoRow
|
|
24945
26012
|
});
|
|
24946
26013
|
import { homedir as homedir9 } from "node:os";
|
|
24947
|
-
import { join as
|
|
26014
|
+
import { join as join29, resolve as resolve2 } from "node:path";
|
|
24948
26015
|
import { stat as stat10 } from "node:fs/promises";
|
|
24949
26016
|
function mirrorPathFor(cloneBaseDir, owner, name) {
|
|
24950
|
-
return
|
|
26017
|
+
return join29(cloneBaseDir, owner, name);
|
|
24951
26018
|
}
|
|
24952
26019
|
function resolveInitTarget(input) {
|
|
24953
26020
|
if (!input.originUrl) {
|
|
@@ -25119,7 +26186,7 @@ async function registerLocalWatch(args) {
|
|
|
25119
26186
|
if (!watched.ok) return { ok: false, reason: watched.reason, exitCode: 3 };
|
|
25120
26187
|
const { watchedRepoId } = watched;
|
|
25121
26188
|
const mirrorPath = mirrorPathFor(cloneBaseDir, target.repoOwner, target.repoName);
|
|
25122
|
-
const mirrorExists = await stat10(
|
|
26189
|
+
const mirrorExists = await stat10(join29(mirrorPath, ".git")).then(() => true).catch(() => false);
|
|
25123
26190
|
if (mirrorExists) {
|
|
25124
26191
|
log(` mirror: ${mirrorPath} (reusing)`);
|
|
25125
26192
|
await setOriginUrl(mirrorPath, originUrl);
|
|
@@ -25266,7 +26333,7 @@ var init_localWatch = __esm({
|
|
|
25266
26333
|
init_localWatchResolve();
|
|
25267
26334
|
init_prompt();
|
|
25268
26335
|
init_localPrSetup();
|
|
25269
|
-
DEFAULT_CLONE_BASE_DIR2 =
|
|
26336
|
+
DEFAULT_CLONE_BASE_DIR2 = join29(homedir9(), ".archme-job-runner", "clones");
|
|
25270
26337
|
}
|
|
25271
26338
|
});
|
|
25272
26339
|
|
|
@@ -25299,9 +26366,9 @@ var init_plan = __esm({
|
|
|
25299
26366
|
});
|
|
25300
26367
|
|
|
25301
26368
|
// src/init/run-init.ts
|
|
25302
|
-
import { mkdtemp as
|
|
25303
|
-
import { tmpdir as
|
|
25304
|
-
import { join as
|
|
26369
|
+
import { mkdtemp as mkdtemp6, rm as rm10, writeFile as writeFile17 } from "node:fs/promises";
|
|
26370
|
+
import { tmpdir as tmpdir6 } from "node:os";
|
|
26371
|
+
import { join as join30 } from "node:path";
|
|
25305
26372
|
async function defaultStartDaemon() {
|
|
25306
26373
|
const { runStartFlow: runStartFlow2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
25307
26374
|
const out = await runStartFlow2({ noLogin: true });
|
|
@@ -25346,10 +26413,10 @@ async function defaultFetchInstallScript(url) {
|
|
|
25346
26413
|
return await resp.text();
|
|
25347
26414
|
}
|
|
25348
26415
|
async function defaultRunInstallScript(script, cwd) {
|
|
25349
|
-
const dir = await
|
|
25350
|
-
const scriptPath =
|
|
26416
|
+
const dir = await mkdtemp6(join30(tmpdir6(), "archme-init-"));
|
|
26417
|
+
const scriptPath = join30(dir, "install-agent.sh");
|
|
25351
26418
|
try {
|
|
25352
|
-
await
|
|
26419
|
+
await writeFile17(scriptPath, script, { mode: 448 });
|
|
25353
26420
|
await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
|
|
25354
26421
|
} finally {
|
|
25355
26422
|
await rm10(dir, { recursive: true, force: true });
|
|
@@ -26895,11 +27962,11 @@ var init_dispatch3 = __esm({
|
|
|
26895
27962
|
// src/init/prReviews.ts
|
|
26896
27963
|
async function offerPrReviewsForInit(ctx, flag, deps = {}) {
|
|
26897
27964
|
const readMode = deps.readMode ?? readPrReviewMode;
|
|
26898
|
-
const
|
|
27965
|
+
const ask2 = deps.ask ?? askForPrReviews;
|
|
26899
27966
|
const setMode = deps.setMode ?? setPrReviewMode;
|
|
26900
27967
|
const ghLogin = deps.ghLogin ?? ghLoginPrompt;
|
|
26901
27968
|
const current = await readMode(ctx.supabase, ctx.watchedRepoId);
|
|
26902
|
-
const want = await
|
|
27969
|
+
const want = await ask2({ flag, interactive: ctx.interactive, current });
|
|
26903
27970
|
if (want === null) {
|
|
26904
27971
|
if (current === "auto") ctx.log(`\u2713 ${PR_REVIEWS_ALREADY_ON}`);
|
|
26905
27972
|
return;
|
|
@@ -27325,7 +28392,7 @@ __export(run_uninstall_exports, {
|
|
|
27325
28392
|
runUninstall: () => runUninstall
|
|
27326
28393
|
});
|
|
27327
28394
|
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
27328
|
-
import { join as
|
|
28395
|
+
import { join as join31 } from "node:path";
|
|
27329
28396
|
function readIfExists(path7) {
|
|
27330
28397
|
try {
|
|
27331
28398
|
return existsSync3(path7) ? readFileSync6(path7, "utf8") : null;
|
|
@@ -27351,7 +28418,7 @@ async function runUninstall(opts) {
|
|
|
27351
28418
|
const backend = opts.backend ?? await defaultBackend();
|
|
27352
28419
|
const repoPath = await backend.gitRoot(opts.repoPath).catch(() => null) ?? opts.repoPath;
|
|
27353
28420
|
if (repoPath !== opts.repoPath) log(`Using the repo root: ${repoPath}`);
|
|
27354
|
-
const abs = (rel) =>
|
|
28421
|
+
const abs = (rel) => join31(repoPath, rel);
|
|
27355
28422
|
const actions = [];
|
|
27356
28423
|
const skipped = [];
|
|
27357
28424
|
const mcpRaw = readIfExists(abs(".mcp.json"));
|
|
@@ -27931,7 +28998,7 @@ __export(run_exports3, {
|
|
|
27931
28998
|
watchedElsewhere: () => watchedElsewhere
|
|
27932
28999
|
});
|
|
27933
29000
|
import { readdir as readdir3, stat as stat11 } from "node:fs/promises";
|
|
27934
|
-
import { dirname as
|
|
29001
|
+
import { dirname as dirname19, join as join32, resolve as resolve6, sep as sep2 } from "node:path";
|
|
27935
29002
|
function isThisRunsWrite(metadata) {
|
|
27936
29003
|
return metadata?.kind === "scan" || !!metadata?.repo_fold;
|
|
27937
29004
|
}
|
|
@@ -27961,12 +29028,12 @@ async function listClones(dir) {
|
|
|
27961
29028
|
const out = [];
|
|
27962
29029
|
for (const e of entries) {
|
|
27963
29030
|
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
27964
|
-
if (await stat11(
|
|
29031
|
+
if (await stat11(join32(dir, e.name, ".git")).then(() => true, () => false)) out.push(join32(dir, e.name));
|
|
27965
29032
|
}
|
|
27966
29033
|
return out.sort();
|
|
27967
29034
|
}
|
|
27968
29035
|
function commonDir(paths) {
|
|
27969
|
-
const split = paths.map((p) =>
|
|
29036
|
+
const split = paths.map((p) => dirname19(p).split(sep2));
|
|
27970
29037
|
const common2 = [];
|
|
27971
29038
|
for (let i2 = 0; split.every((s) => s[i2] !== void 0 && s[i2] === split[0][i2]); i2++) common2.push(split[0][i2]);
|
|
27972
29039
|
return common2.join(sep2) || sep2;
|
|
@@ -28001,7 +29068,7 @@ function buildRunSheet(opts) {
|
|
|
28001
29068
|
`- Clone: \`${r.root}\``,
|
|
28002
29069
|
`- Launch: \`${launch(r)}\``,
|
|
28003
29070
|
`- Resume: \`${launch(r)} --resume --message "<answer or correction>"\``,
|
|
28004
|
-
`- Session file: \`${
|
|
29071
|
+
`- Session file: \`${join32(r.root, SCAN_SESSION_FILE)}\``
|
|
28005
29072
|
])
|
|
28006
29073
|
];
|
|
28007
29074
|
if (opts.context.length > 0) {
|
|
@@ -28050,7 +29117,7 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
28050
29117
|
roots.push(found.root);
|
|
28051
29118
|
continue;
|
|
28052
29119
|
}
|
|
28053
|
-
const dest =
|
|
29120
|
+
const dest = join32(cwd, arg.split("/")[1]);
|
|
28054
29121
|
if (await (deps.isDirectory ?? isDirectory)(dest)) return fail2(78, `${dest} exists and is not a clone of ${arg} \u2014 move it, or pass the clone's path`);
|
|
28055
29122
|
log(`cloning ${arg} into ${dest}`);
|
|
28056
29123
|
const cloned = await (deps.cloneRepo ?? cloneFromGitHub)(arg, dest);
|
|
@@ -28422,8 +29489,8 @@ var init_dispatch7 = __esm({
|
|
|
28422
29489
|
});
|
|
28423
29490
|
|
|
28424
29491
|
// src/doctor/collect.ts
|
|
28425
|
-
import { readFile as
|
|
28426
|
-
import { join as
|
|
29492
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
29493
|
+
import { join as join33 } from "node:path";
|
|
28427
29494
|
function binaryPresent(name) {
|
|
28428
29495
|
try {
|
|
28429
29496
|
requireBinary(name);
|
|
@@ -28439,7 +29506,7 @@ async function git3(cwd, args) {
|
|
|
28439
29506
|
async function readMcpPin(repoRoot) {
|
|
28440
29507
|
const empty = { installed: false, pin: { project: null, branch: null } };
|
|
28441
29508
|
try {
|
|
28442
|
-
const raw = await
|
|
29509
|
+
const raw = await readFile18(join33(repoRoot, ".mcp.json"), "utf8");
|
|
28443
29510
|
const parsed = JSON.parse(raw);
|
|
28444
29511
|
const servers = parsed.mcpServers ?? {};
|
|
28445
29512
|
const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
|
|
@@ -28813,9 +29880,9 @@ __export(dispatch_exports8, {
|
|
|
28813
29880
|
parseDoctorArgs: () => parseDoctorArgs,
|
|
28814
29881
|
printDoctorHelp: () => printDoctorHelp
|
|
28815
29882
|
});
|
|
28816
|
-
import { mkdtemp as
|
|
28817
|
-
import { tmpdir as
|
|
28818
|
-
import { join as
|
|
29883
|
+
import { mkdtemp as mkdtemp7, rm as rm11, writeFile as writeFile18 } from "node:fs/promises";
|
|
29884
|
+
import { tmpdir as tmpdir7 } from "node:os";
|
|
29885
|
+
import { join as join34 } from "node:path";
|
|
28819
29886
|
function parseDoctorArgs(argv) {
|
|
28820
29887
|
const args = { plain: false, help: false };
|
|
28821
29888
|
for (const a2 of argv) {
|
|
@@ -28876,9 +29943,9 @@ async function dispatchDoctor(argv) {
|
|
|
28876
29943
|
);
|
|
28877
29944
|
return EX_OK6;
|
|
28878
29945
|
}
|
|
28879
|
-
const dir = await
|
|
28880
|
-
const mcpConfigPath =
|
|
28881
|
-
await
|
|
29946
|
+
const dir = await mkdtemp7(join34(tmpdir7(), "archme-doctor-"));
|
|
29947
|
+
const mcpConfigPath = join34(dir, "mcp.json");
|
|
29948
|
+
await writeFile18(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
|
|
28882
29949
|
try {
|
|
28883
29950
|
const result = await spawnClaude({
|
|
28884
29951
|
prompt: buildDoctorPrompt(facts, issues),
|
|
@@ -29045,7 +30112,7 @@ var init_conciergeToken = __esm({
|
|
|
29045
30112
|
});
|
|
29046
30113
|
|
|
29047
30114
|
// src/daemon/sandboxJob/input.ts
|
|
29048
|
-
import { readFile as
|
|
30115
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
29049
30116
|
function isObj2(v) {
|
|
29050
30117
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
29051
30118
|
}
|
|
@@ -29154,6 +30221,31 @@ function parseRepoScope(raw) {
|
|
|
29154
30221
|
}
|
|
29155
30222
|
fail("repoScope must be { kind: all | container | excluding }");
|
|
29156
30223
|
}
|
|
30224
|
+
function parseProbe(data) {
|
|
30225
|
+
const timeoutMs = data.timeoutMs;
|
|
30226
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) fail("timeoutMs must be a positive integer");
|
|
30227
|
+
if (!Array.isArray(data.targets) || data.targets.length > 64) fail("targets must be an array of at most 64");
|
|
30228
|
+
const targets = data.targets.map((raw, i2) => {
|
|
30229
|
+
const where = `targets[${i2}]`;
|
|
30230
|
+
if (!isObj2(raw)) fail(`${where} must be an object`);
|
|
30231
|
+
const id = str3(raw, "id", where);
|
|
30232
|
+
if (!PROBE_ID_RE.test(id)) fail(`${where}.id must be [a-z0-9_]`);
|
|
30233
|
+
const kind = raw.kind;
|
|
30234
|
+
if (!PROBE_KINDS.has(kind)) fail(`${where}.kind is not a probe kind`);
|
|
30235
|
+
const host = str3(raw, "host", where);
|
|
30236
|
+
if (kind === "ipv6_socket" ? host !== "::" : !IPV4_RE2.test(host)) fail(`${where}.host must be an IPv4 address`);
|
|
30237
|
+
const port = raw.port;
|
|
30238
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) fail(`${where}.port is not a port`);
|
|
30239
|
+
const target = { id, kind, host, port };
|
|
30240
|
+
if (raw.authority !== void 0) {
|
|
30241
|
+
const a2 = raw.authority;
|
|
30242
|
+
if (kind !== "connect" || typeof a2 !== "string" || !/^[A-Za-z0-9.\-[\]:]{1,300}$/.test(a2)) fail(`${where}.authority is not host:port`);
|
|
30243
|
+
target.authority = a2;
|
|
30244
|
+
}
|
|
30245
|
+
return target;
|
|
30246
|
+
});
|
|
30247
|
+
return { kind: "probe", targets, timeoutMs };
|
|
30248
|
+
}
|
|
29157
30249
|
function parseInput(raw) {
|
|
29158
30250
|
let data;
|
|
29159
30251
|
try {
|
|
@@ -29162,6 +30254,7 @@ function parseInput(raw) {
|
|
|
29162
30254
|
fail("not valid JSON");
|
|
29163
30255
|
}
|
|
29164
30256
|
if (!isObj2(data)) fail("must be a JSON object");
|
|
30257
|
+
if (data.kind === "probe") return parseProbe(data);
|
|
29165
30258
|
const job = parseJob(data.job);
|
|
29166
30259
|
if (data.kind === "clone") {
|
|
29167
30260
|
const input = {
|
|
@@ -29192,9 +30285,9 @@ function parseInput(raw) {
|
|
|
29192
30285
|
};
|
|
29193
30286
|
return input;
|
|
29194
30287
|
}
|
|
29195
|
-
fail("kind must be clone or
|
|
30288
|
+
fail("kind must be clone, agent or probe");
|
|
29196
30289
|
}
|
|
29197
|
-
async function readInput(path7 = INPUT_PATH, read = (p) =>
|
|
30290
|
+
async function readInput(path7 = INPUT_PATH, read = (p) => readFile19(p, "utf8")) {
|
|
29198
30291
|
let raw;
|
|
29199
30292
|
try {
|
|
29200
30293
|
raw = await read(path7);
|
|
@@ -29203,7 +30296,7 @@ async function readInput(path7 = INPUT_PATH, read = (p) => readFile18(p, "utf8")
|
|
|
29203
30296
|
}
|
|
29204
30297
|
return parseInput(raw);
|
|
29205
30298
|
}
|
|
29206
|
-
var InputSchemaError, SHA_RE3, REPO_PART_RE, IPV4_RE2, SOURCE_MODES;
|
|
30299
|
+
var InputSchemaError, SHA_RE3, REPO_PART_RE, IPV4_RE2, SOURCE_MODES, PROBE_KINDS, PROBE_ID_RE;
|
|
29207
30300
|
var init_input = __esm({
|
|
29208
30301
|
"src/daemon/sandboxJob/input.ts"() {
|
|
29209
30302
|
"use strict";
|
|
@@ -29218,6 +30311,135 @@ var init_input = __esm({
|
|
|
29218
30311
|
REPO_PART_RE = /^[A-Za-z0-9_.-]+$/;
|
|
29219
30312
|
IPV4_RE2 = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
|
|
29220
30313
|
SOURCE_MODES = /* @__PURE__ */ new Set(["diff", "reconcile", "pr_update"]);
|
|
30314
|
+
PROBE_KINDS = /* @__PURE__ */ new Set(["tcp", "udp_dns", "http", "connect", "ipv6_socket"]);
|
|
30315
|
+
PROBE_ID_RE = /^[a-z0-9_]{1,64}$/;
|
|
30316
|
+
}
|
|
30317
|
+
});
|
|
30318
|
+
|
|
30319
|
+
// src/daemon/sandboxJob/probe.ts
|
|
30320
|
+
import dgram from "node:dgram";
|
|
30321
|
+
import http2 from "node:http";
|
|
30322
|
+
import net2 from "node:net";
|
|
30323
|
+
function tcp(t, timeoutMs) {
|
|
30324
|
+
return new Promise((resolve7) => {
|
|
30325
|
+
const s = net2.connect({ host: t.host, port: t.port });
|
|
30326
|
+
const finish = (outcome) => {
|
|
30327
|
+
s.destroy();
|
|
30328
|
+
resolve7(outcome);
|
|
30329
|
+
};
|
|
30330
|
+
s.setTimeout(timeoutMs, () => finish("timeout"));
|
|
30331
|
+
s.once("connect", () => finish("connected"));
|
|
30332
|
+
s.once("error", (e) => {
|
|
30333
|
+
finish(e.code === "ECONNREFUSED" ? "refused" : e.code === "ETIMEDOUT" ? "timeout" : "unreachable");
|
|
30334
|
+
});
|
|
30335
|
+
});
|
|
30336
|
+
}
|
|
30337
|
+
function udpDns(t, timeoutMs) {
|
|
30338
|
+
return new Promise((resolve7) => {
|
|
30339
|
+
const s = dgram.createSocket("udp4");
|
|
30340
|
+
let done = false;
|
|
30341
|
+
const finish = (outcome) => {
|
|
30342
|
+
if (done) return;
|
|
30343
|
+
done = true;
|
|
30344
|
+
clearTimeout(timer);
|
|
30345
|
+
s.close();
|
|
30346
|
+
resolve7(outcome);
|
|
30347
|
+
};
|
|
30348
|
+
const timer = setTimeout(() => finish("silent"), timeoutMs);
|
|
30349
|
+
s.on("message", () => finish("answered"));
|
|
30350
|
+
s.on("error", () => finish("silent"));
|
|
30351
|
+
s.send(DNS_QUERY, t.port, t.host, (err4) => {
|
|
30352
|
+
if (err4) finish("silent");
|
|
30353
|
+
});
|
|
30354
|
+
});
|
|
30355
|
+
}
|
|
30356
|
+
function httpStatus(t, timeoutMs, method) {
|
|
30357
|
+
return new Promise((resolve7) => {
|
|
30358
|
+
let done = false;
|
|
30359
|
+
const finish = (outcome) => {
|
|
30360
|
+
if (done) return;
|
|
30361
|
+
done = true;
|
|
30362
|
+
req.destroy();
|
|
30363
|
+
resolve7(outcome);
|
|
30364
|
+
};
|
|
30365
|
+
const req = http2.request({
|
|
30366
|
+
host: t.host,
|
|
30367
|
+
port: t.port,
|
|
30368
|
+
method,
|
|
30369
|
+
agent: false,
|
|
30370
|
+
timeout: timeoutMs,
|
|
30371
|
+
path: method === "CONNECT" ? t.authority ?? "github.com:443" : "/"
|
|
30372
|
+
});
|
|
30373
|
+
req.on("response", (res) => {
|
|
30374
|
+
res.resume();
|
|
30375
|
+
finish(String(res.statusCode ?? "none"));
|
|
30376
|
+
});
|
|
30377
|
+
req.on("connect", (res, socket) => {
|
|
30378
|
+
socket.destroy();
|
|
30379
|
+
finish(String(res.statusCode ?? "none"));
|
|
30380
|
+
});
|
|
30381
|
+
req.on("timeout", () => finish("none"));
|
|
30382
|
+
req.on("error", () => finish("none"));
|
|
30383
|
+
req.end();
|
|
30384
|
+
});
|
|
30385
|
+
}
|
|
30386
|
+
function ipv6Socket() {
|
|
30387
|
+
return new Promise((resolve7) => {
|
|
30388
|
+
const s = dgram.createSocket("udp6");
|
|
30389
|
+
s.once("error", () => {
|
|
30390
|
+
s.close();
|
|
30391
|
+
resolve7("unsupported");
|
|
30392
|
+
});
|
|
30393
|
+
s.bind(0, "::", () => {
|
|
30394
|
+
s.close();
|
|
30395
|
+
resolve7("created");
|
|
30396
|
+
});
|
|
30397
|
+
});
|
|
30398
|
+
}
|
|
30399
|
+
async function runProbe(input) {
|
|
30400
|
+
const results = await Promise.all(input.targets.map(async (t) => {
|
|
30401
|
+
switch (t.kind) {
|
|
30402
|
+
case "tcp":
|
|
30403
|
+
return [t.id, await tcp(t, input.timeoutMs)];
|
|
30404
|
+
case "udp_dns":
|
|
30405
|
+
return [t.id, await udpDns(t, input.timeoutMs)];
|
|
30406
|
+
case "http":
|
|
30407
|
+
return [t.id, await httpStatus(t, input.timeoutMs, "GET")];
|
|
30408
|
+
case "connect":
|
|
30409
|
+
return [t.id, await httpStatus(t, input.timeoutMs, "CONNECT")];
|
|
30410
|
+
case "ipv6_socket":
|
|
30411
|
+
return [t.id, await ipv6Socket()];
|
|
30412
|
+
}
|
|
30413
|
+
}));
|
|
30414
|
+
return Object.fromEntries(results);
|
|
30415
|
+
}
|
|
30416
|
+
var DNS_QUERY;
|
|
30417
|
+
var init_probe = __esm({
|
|
30418
|
+
"src/daemon/sandboxJob/probe.ts"() {
|
|
30419
|
+
"use strict";
|
|
30420
|
+
DNS_QUERY = Buffer.from([
|
|
30421
|
+
18,
|
|
30422
|
+
52,
|
|
30423
|
+
1,
|
|
30424
|
+
0,
|
|
30425
|
+
0,
|
|
30426
|
+
1,
|
|
30427
|
+
0,
|
|
30428
|
+
0,
|
|
30429
|
+
0,
|
|
30430
|
+
0,
|
|
30431
|
+
0,
|
|
30432
|
+
0,
|
|
30433
|
+
7,
|
|
30434
|
+
...Buffer.from("example"),
|
|
30435
|
+
3,
|
|
30436
|
+
...Buffer.from("com"),
|
|
30437
|
+
0,
|
|
30438
|
+
0,
|
|
30439
|
+
1,
|
|
30440
|
+
0,
|
|
30441
|
+
1
|
|
30442
|
+
]);
|
|
29221
30443
|
}
|
|
29222
30444
|
});
|
|
29223
30445
|
|
|
@@ -29227,7 +30449,7 @@ __export(main_exports, {
|
|
|
29227
30449
|
readNonceLine: () => readNonceLine,
|
|
29228
30450
|
runSandboxJob: () => runSandboxJob
|
|
29229
30451
|
});
|
|
29230
|
-
import { mkdir as
|
|
30452
|
+
import { mkdir as mkdir17 } from "node:fs/promises";
|
|
29231
30453
|
function readNonceLine(stream = process.stdin) {
|
|
29232
30454
|
return new Promise((resolve7) => {
|
|
29233
30455
|
let buf = "";
|
|
@@ -29252,6 +30474,10 @@ function readNonceLine(stream = process.stdin) {
|
|
|
29252
30474
|
});
|
|
29253
30475
|
}
|
|
29254
30476
|
function emptyResult(step, error) {
|
|
30477
|
+
if (step === "probe") {
|
|
30478
|
+
const r2 = { kind: "probe", outcomes: {}, memoryPeakBytes: null, durations_ms: {}, error };
|
|
30479
|
+
return r2;
|
|
30480
|
+
}
|
|
29255
30481
|
if (step === "clone") {
|
|
29256
30482
|
const r2 = { kind: "clone", outcome: "diff", memoryPeakBytes: null, durations_ms: {}, error };
|
|
29257
30483
|
return r2;
|
|
@@ -29299,8 +30525,8 @@ async function runSandboxJob(argv, deps = {}) {
|
|
|
29299
30525
|
return code;
|
|
29300
30526
|
};
|
|
29301
30527
|
const step = argv[0];
|
|
29302
|
-
if (step !== "clone" && step !== "agent") {
|
|
29303
|
-
const msg = `unknown step ${JSON.stringify(step ?? "")}; expected clone or
|
|
30528
|
+
if (step !== "clone" && step !== "agent" && step !== "probe") {
|
|
30529
|
+
const msg = `unknown step ${JSON.stringify(step ?? "")}; expected clone, agent or probe`;
|
|
29304
30530
|
log(msg);
|
|
29305
30531
|
return emit2(emptyResult("clone", msg), EXIT_INPUT_SCHEMA);
|
|
29306
30532
|
}
|
|
@@ -29314,9 +30540,19 @@ async function runSandboxJob(argv, deps = {}) {
|
|
|
29314
30540
|
return emit2(emptyResult(step, msg), err4 instanceof InputSchemaError ? EXIT_INPUT_SCHEMA : 1);
|
|
29315
30541
|
}
|
|
29316
30542
|
mark("input");
|
|
30543
|
+
if (input.kind === "probe") {
|
|
30544
|
+
try {
|
|
30545
|
+
const outcomes = await (deps.probe ?? runProbe)(input);
|
|
30546
|
+
return emit2({ kind: "probe", outcomes, memoryPeakBytes: null, durations_ms: {} }, 0);
|
|
30547
|
+
} catch (err4) {
|
|
30548
|
+
const msg = err4 instanceof Error ? err4.message : String(err4);
|
|
30549
|
+
log(`failed: ${msg}`);
|
|
30550
|
+
return emit2(emptyResult("probe", msg), 1);
|
|
30551
|
+
}
|
|
30552
|
+
}
|
|
29317
30553
|
log(`${step} job=${input.job.id} repo=${input.job.repo_owner}/${input.job.repo_name} after=${input.job.after_sha.slice(0, 8)}`);
|
|
29318
30554
|
try {
|
|
29319
|
-
await
|
|
30555
|
+
await mkdir17(deps.homeDir ?? process.env.HOME ?? BOX_HOME, { recursive: true, mode: 448 });
|
|
29320
30556
|
if (input.kind === "clone") {
|
|
29321
30557
|
const { result: result2, exitCode: exitCode2 } = await runCloneStep(input, { log, mark, ...deps.clone });
|
|
29322
30558
|
return emit2({ ...result2, memoryPeakBytes: null, durations_ms: {} }, exitCode2);
|
|
@@ -29338,6 +30574,7 @@ var init_main2 = __esm({
|
|
|
29338
30574
|
init_result2();
|
|
29339
30575
|
init_clone();
|
|
29340
30576
|
init_agent();
|
|
30577
|
+
init_probe();
|
|
29341
30578
|
NONCE_LINE_MAX_BYTES = 256;
|
|
29342
30579
|
}
|
|
29343
30580
|
});
|
|
@@ -29416,7 +30653,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
29416
30653
|
"doctor",
|
|
29417
30654
|
// Hidden: operator-only (platform admins), deliberately absent from printHelp.
|
|
29418
30655
|
"concierge-token",
|
|
29419
|
-
// Hidden: the per-job sandbox image's entrypoint (`sandbox-job clone|agent`).
|
|
30656
|
+
// Hidden: the per-job sandbox image's entrypoint (`sandbox-job clone|agent|probe`).
|
|
29420
30657
|
"sandbox-job"
|
|
29421
30658
|
]);
|
|
29422
30659
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v"]);
|
|
@@ -29439,7 +30676,8 @@ var DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
29439
30676
|
"disable",
|
|
29440
30677
|
"install",
|
|
29441
30678
|
"uninstall",
|
|
29442
|
-
"run"
|
|
30679
|
+
"run",
|
|
30680
|
+
"sandbox"
|
|
29443
30681
|
]);
|
|
29444
30682
|
function routeDaemon(args) {
|
|
29445
30683
|
const sub = args[0];
|
|
@@ -29473,6 +30711,11 @@ function routeDaemon(args) {
|
|
|
29473
30711
|
return { action: "uninstall", purge: hasFlag("--purge") };
|
|
29474
30712
|
case "run":
|
|
29475
30713
|
return { action: "run" };
|
|
30714
|
+
case "sandbox": {
|
|
30715
|
+
if (rest[0] === "setup") return { action: "sandbox-setup", yes: rest.includes("--yes") || rest.includes("-y") };
|
|
30716
|
+
if (rest[0] === "status") return { action: "sandbox-status" };
|
|
30717
|
+
return { action: "unknown", arg: ["sandbox", ...rest.slice(0, 1)].join(" ") };
|
|
30718
|
+
}
|
|
29476
30719
|
}
|
|
29477
30720
|
return { action: "unknown", arg: sub };
|
|
29478
30721
|
}
|
|
@@ -29613,4 +30856,10 @@ Run 'archme init --help', 'archme daemon --help', 'archme analyze --help', or
|
|
|
29613
30856
|
}
|
|
29614
30857
|
|
|
29615
30858
|
// src/bin.ts
|
|
29616
|
-
main(process.argv.slice(2)).then(
|
|
30859
|
+
main(process.argv.slice(2)).then(
|
|
30860
|
+
(code) => process.exit(code),
|
|
30861
|
+
(err4) => {
|
|
30862
|
+
console.error(err4 instanceof Error ? err4.message : String(err4));
|
|
30863
|
+
process.exit(1);
|
|
30864
|
+
}
|
|
30865
|
+
);
|