@archme/cli 0.13.0 → 0.14.1
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 +2588 -1323
- 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
|
}
|
|
@@ -10741,7 +10807,9 @@ function digestSpawnOpts(o2) {
|
|
|
10741
10807
|
// tool-less unless the caller wired MCP reads
|
|
10742
10808
|
builtinTools: [],
|
|
10743
10809
|
timeoutMs: DIGEST_TIMEOUT_MS,
|
|
10744
|
-
model: o2.model
|
|
10810
|
+
model: o2.model,
|
|
10811
|
+
settingSources: "user"
|
|
10812
|
+
// never a project's .claude/settings.json hooks on the host
|
|
10745
10813
|
};
|
|
10746
10814
|
}
|
|
10747
10815
|
function isCleanDigestExit(res) {
|
|
@@ -16391,15 +16459,23 @@ var init_sessionAuth = __esm({
|
|
|
16391
16459
|
this.userId = resolveUserId(userInfo2, ctx.fallbackUserId);
|
|
16392
16460
|
this.lastUserInfo = userInfo2;
|
|
16393
16461
|
}
|
|
16394
|
-
|
|
16395
|
-
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16462
|
+
let saved = null;
|
|
16463
|
+
try {
|
|
16464
|
+
saved = await saveCredentials({
|
|
16465
|
+
kind: this.kind,
|
|
16466
|
+
refresh_token: nextRefreshToken,
|
|
16467
|
+
access_token: this.accessToken,
|
|
16468
|
+
org_id: ctx.orgId,
|
|
16469
|
+
device_id: ctx.deviceId,
|
|
16470
|
+
user_id: this.userId ?? void 0,
|
|
16471
|
+
supabase_url: this.cfg.supabaseUrl
|
|
16472
|
+
});
|
|
16473
|
+
} catch (err4) {
|
|
16474
|
+
if (!(err4 instanceof CredentialStoreError)) throw err4;
|
|
16475
|
+
console.error(
|
|
16476
|
+
`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()}`
|
|
16477
|
+
);
|
|
16478
|
+
}
|
|
16403
16479
|
return { userInfo: userInfo2, saved, nextRefreshToken };
|
|
16404
16480
|
}
|
|
16405
16481
|
// The timer closure reads the LIVE this.refreshToken/deviceId/orgId at fire
|
|
@@ -16492,11 +16568,11 @@ var init_sessionAuth = __esm({
|
|
|
16492
16568
|
const refreshed = await this.refreshOrRecover(refreshToken, this.userId);
|
|
16493
16569
|
if (!refreshed.ok) {
|
|
16494
16570
|
if (refreshed.permanent) {
|
|
16495
|
-
await
|
|
16571
|
+
const notCleared = await this.clearStoredCredential(deviceId);
|
|
16496
16572
|
this.clearTokensOnly();
|
|
16497
16573
|
throw new LoadStoredAndRefreshError(
|
|
16498
16574
|
"invalid_grant",
|
|
16499
|
-
`${errorPrefix} failed permanently: ${refreshed.error}. ${this.reloginHint()}`
|
|
16575
|
+
`${errorPrefix} failed permanently: ${refreshed.error}. ${this.reloginHint()}${notCleared}`
|
|
16500
16576
|
);
|
|
16501
16577
|
}
|
|
16502
16578
|
throw new LoadStoredAndRefreshError("transient", `${errorPrefix} failed: ${refreshed.error}`);
|
|
@@ -16507,7 +16583,7 @@ var init_sessionAuth = __esm({
|
|
|
16507
16583
|
orgId,
|
|
16508
16584
|
fallbackUserId: this.userId ?? void 0
|
|
16509
16585
|
});
|
|
16510
|
-
console.log(`auth: persisted refreshed token to ${describeCredentialSource(saved)}`);
|
|
16586
|
+
if (saved) console.log(`auth: persisted refreshed token to ${describeCredentialSource(saved)}`);
|
|
16511
16587
|
})();
|
|
16512
16588
|
this.refreshInFlight = p;
|
|
16513
16589
|
return p.finally(() => {
|
|
@@ -16520,6 +16596,18 @@ var init_sessionAuth = __esm({
|
|
|
16520
16596
|
if (refreshed.ok || !refreshed.permanent) return refreshed;
|
|
16521
16597
|
return this.signInWithSavedKey(userId, refreshed.error);
|
|
16522
16598
|
}
|
|
16599
|
+
// Remove a dead login from the store. The token is revoked whether or not
|
|
16600
|
+
// this works, so a store that refuses the delete must not turn a permanent
|
|
16601
|
+
// failure into a transient one (which would retry a dead token forever).
|
|
16602
|
+
async clearStoredCredential(deviceId) {
|
|
16603
|
+
try {
|
|
16604
|
+
await clearCredential(this.cfg.supabaseUrl, deviceId);
|
|
16605
|
+
return "";
|
|
16606
|
+
} catch (err4) {
|
|
16607
|
+
if (!(err4 instanceof CredentialStoreError)) throw err4;
|
|
16608
|
+
return ` (${err4.message})`;
|
|
16609
|
+
}
|
|
16610
|
+
}
|
|
16523
16611
|
reloginHint() {
|
|
16524
16612
|
return this.kind === "key" ? "Run `archme login --key` to sign in with your key again." : "Run `archme login` to pair again.";
|
|
16525
16613
|
}
|
|
@@ -16564,10 +16652,10 @@ var init_sessionAuth = __esm({
|
|
|
16564
16652
|
const refreshed = await this.refreshOrRecover(stored.refresh_token, stored.user_id ?? null);
|
|
16565
16653
|
if (!refreshed.ok) {
|
|
16566
16654
|
if (refreshed.permanent) {
|
|
16567
|
-
await
|
|
16655
|
+
const notCleared = await this.clearStoredCredential(stored.device_id);
|
|
16568
16656
|
throw new LoadStoredAndRefreshError(
|
|
16569
16657
|
"invalid_grant",
|
|
16570
|
-
`stored archme credentials are invalid or revoked: ${refreshed.error}. ${this.reloginHint()}`
|
|
16658
|
+
`stored archme credentials are invalid or revoked: ${refreshed.error}. ${this.reloginHint()}${notCleared}`
|
|
16571
16659
|
);
|
|
16572
16660
|
}
|
|
16573
16661
|
throw new LoadStoredAndRefreshError("transient", `token refresh failed (transient): ${refreshed.error}`);
|
|
@@ -16941,12 +17029,12 @@ function defaultReadFile(path7) {
|
|
|
16941
17029
|
return readFileSync4(path7, "utf8");
|
|
16942
17030
|
}
|
|
16943
17031
|
function findPackageRoot2(startDir, opts = {}) {
|
|
16944
|
-
const
|
|
17032
|
+
const readFile20 = opts.readFile ?? defaultReadFile;
|
|
16945
17033
|
const stopAt = parse(startDir).root;
|
|
16946
17034
|
let dir = startDir;
|
|
16947
17035
|
for (; ; ) {
|
|
16948
17036
|
try {
|
|
16949
|
-
const parsed = JSON.parse(
|
|
17037
|
+
const parsed = JSON.parse(readFile20(join16(dir, "package.json")));
|
|
16950
17038
|
if (parsed.name === PACKAGE_NAME) return dir;
|
|
16951
17039
|
} catch {
|
|
16952
17040
|
}
|
|
@@ -17157,7 +17245,9 @@ function observeGithubPull(p) {
|
|
|
17157
17245
|
mergedAt: p.merged_at ?? null,
|
|
17158
17246
|
closedAt: p.closed_at ?? null,
|
|
17159
17247
|
// Before the merge it is GitHub's throwaway test merge.
|
|
17160
|
-
mergeCommitSha: p.merged_at ? p.merge_commit_sha ?? null : null
|
|
17248
|
+
mergeCommitSha: p.merged_at ? p.merge_commit_sha ?? null : null,
|
|
17249
|
+
headRepo: p.head.repo?.full_name ?? null,
|
|
17250
|
+
baseRepo: p.base.repo?.full_name ?? null
|
|
17161
17251
|
};
|
|
17162
17252
|
}
|
|
17163
17253
|
function metaKey(o2) {
|
|
@@ -17194,6 +17284,8 @@ function toItem(o2, action, sent) {
|
|
|
17194
17284
|
closed_at: o2.closedAt,
|
|
17195
17285
|
merge_commit_sha: o2.mergeCommitSha,
|
|
17196
17286
|
body: o2.body,
|
|
17287
|
+
head_repo: o2.headRepo,
|
|
17288
|
+
...o2.baseRepo ? { base_repo: o2.baseRepo } : {},
|
|
17197
17289
|
...action === "synchronize" && sent.seq !== void 0 ? { seq: sent.seq } : {}
|
|
17198
17290
|
};
|
|
17199
17291
|
}
|
|
@@ -17921,7 +18013,12 @@ async function pollPrsOnce(entry, deps) {
|
|
|
17921
18013
|
const kinds = new Map(results.map((r) => [r.number, r.kind]));
|
|
17922
18014
|
for (const item of plan.send) {
|
|
17923
18015
|
const kind = kinds.get(item.number);
|
|
17924
|
-
if (kind
|
|
18016
|
+
if (kind === "held") {
|
|
18017
|
+
const fork = !!item.head_repo && !!item.base_repo && item.head_repo.toLowerCase() !== item.base_repo.toLowerCase();
|
|
18018
|
+
deps.log(
|
|
18019
|
+
fork ? ` ${repo}: PR #${item.number} ${item.action} \u2192 held: it comes from a fork, and a locally watched repo does not review fork pull requests (there is no way to approve one without the GitHub App).` : ` ${repo}: PR #${item.number} ${item.action} \u2192 held: the server did not approve it for review (its head repo could not be confirmed as this repo, e.g. GitHub did not report it).`
|
|
18020
|
+
);
|
|
18021
|
+
} else if (kind !== void 0) {
|
|
17925
18022
|
deps.log(` ${repo}: PR #${item.number} ${item.action} \u2192 ${kind}`);
|
|
17926
18023
|
}
|
|
17927
18024
|
if (kind !== void 0 && !UNDELIVERED.has(kind)) continue;
|
|
@@ -18343,6 +18440,26 @@ function expandHome(p, home2) {
|
|
|
18343
18440
|
if (p.startsWith("~/")) return join20(home2, p.slice(2));
|
|
18344
18441
|
return p;
|
|
18345
18442
|
}
|
|
18443
|
+
function sandboxTokenLocation(env, home2 = homedir6()) {
|
|
18444
|
+
const raw = (env.ARCHME_SANDBOX_CLAUDE_TOKEN_FILE ?? "").trim();
|
|
18445
|
+
return raw ? { path: expandHome(raw, home2), override: true } : { path: join20(home2, ".archme", "sandbox-claude-token"), override: false };
|
|
18446
|
+
}
|
|
18447
|
+
function resolveSandboxImage(env, shippedImage) {
|
|
18448
|
+
let image = (env.ARCHME_SANDBOX_IMAGE ?? "").trim();
|
|
18449
|
+
if (!image) {
|
|
18450
|
+
try {
|
|
18451
|
+
image = shippedImage() ?? "";
|
|
18452
|
+
} catch (e) {
|
|
18453
|
+
throw new SandboxSettingsError(`the shipped sandbox image is unusable: ${e.message}`);
|
|
18454
|
+
}
|
|
18455
|
+
}
|
|
18456
|
+
if (!image) {
|
|
18457
|
+
throw new SandboxSettingsError(
|
|
18458
|
+
"no sandbox image: this install ships no sandbox-image.json, so set ARCHME_SANDBOX_IMAGE"
|
|
18459
|
+
);
|
|
18460
|
+
}
|
|
18461
|
+
return image;
|
|
18462
|
+
}
|
|
18346
18463
|
function readSandboxSettings(env, concurrency, home2 = homedir6(), shippedImage = () => null) {
|
|
18347
18464
|
const modeRaw = (env.ARCHME_SANDBOX ?? "").trim().toLowerCase() || "off";
|
|
18348
18465
|
if (modeRaw !== "off" && modeRaw !== "shadow" && modeRaw !== "required") {
|
|
@@ -18358,14 +18475,10 @@ function readSandboxSettings(env, concurrency, home2 = homedir6(), shippedImage
|
|
|
18358
18475
|
boxCpus: SANDBOX_DEFAULTS.boxCpus,
|
|
18359
18476
|
volumeGiB: SANDBOX_DEFAULTS.volumeGiB,
|
|
18360
18477
|
maxModelRequests: SANDBOX_DEFAULTS.maxModelRequests,
|
|
18361
|
-
claudeTokenPath:
|
|
18478
|
+
claudeTokenPath: sandboxTokenLocation({}, home2).path,
|
|
18479
|
+
claudeTokenOverride: false
|
|
18362
18480
|
};
|
|
18363
18481
|
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
18482
|
if (!Number.isInteger(concurrency)) {
|
|
18370
18483
|
throw new SandboxSettingsError(`MAX_CONCURRENT_JOBS must be a whole number (got ${concurrency})`);
|
|
18371
18484
|
}
|
|
@@ -18378,23 +18491,11 @@ function readSandboxSettings(env, concurrency, home2 = homedir6(), shippedImage
|
|
|
18378
18491
|
if (runtime !== "apple-container") {
|
|
18379
18492
|
throw new SandboxSettingsError(`ARCHME_SANDBOX_RUNTIME must be apple-container (got "${runtime}")`);
|
|
18380
18493
|
}
|
|
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(
|
|
18494
|
+
const image = resolveSandboxImage(env, shippedImage);
|
|
18495
|
+
const projects = mode === "required" ? /* @__PURE__ */ new Set() : new Set(
|
|
18395
18496
|
(env.ARCHME_SANDBOX_PROJECTS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
18396
18497
|
);
|
|
18397
|
-
const
|
|
18498
|
+
const token = sandboxTokenLocation(env, home2);
|
|
18398
18499
|
return {
|
|
18399
18500
|
mode,
|
|
18400
18501
|
projects,
|
|
@@ -18407,7 +18508,8 @@ function readSandboxSettings(env, concurrency, home2 = homedir6(), shippedImage
|
|
|
18407
18508
|
boxCpus: SANDBOX_DEFAULTS.boxCpus,
|
|
18408
18509
|
volumeGiB: intEnv(env, "ARCHME_SANDBOX_VOLUME_GIB", SANDBOX_DEFAULTS.volumeGiB, { min: 1 }),
|
|
18409
18510
|
maxModelRequests: intEnv(env, "ARCHME_SANDBOX_MAX_MODEL_REQUESTS", SANDBOX_DEFAULTS.maxModelRequests, { min: 1 }),
|
|
18410
|
-
claudeTokenPath:
|
|
18511
|
+
claudeTokenPath: token.path,
|
|
18512
|
+
claudeTokenOverride: token.override
|
|
18411
18513
|
};
|
|
18412
18514
|
}
|
|
18413
18515
|
function shouldSandbox(settings, job, { isLocalRepo }) {
|
|
@@ -18483,6 +18585,132 @@ var init_imageManifest = __esm({
|
|
|
18483
18585
|
}
|
|
18484
18586
|
});
|
|
18485
18587
|
|
|
18588
|
+
// src/daemon/sandboxJob/result.ts
|
|
18589
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
18590
|
+
function isValidNonce(nonce) {
|
|
18591
|
+
return typeof nonce === "string" && RESULT_NONCE_RE.test(nonce);
|
|
18592
|
+
}
|
|
18593
|
+
function encode(result, nonce) {
|
|
18594
|
+
return `${RESULT_PREFIX}${nonce} ${Buffer.from(JSON.stringify(result), "utf8").toString("base64")}`;
|
|
18595
|
+
}
|
|
18596
|
+
function fits(line, cap) {
|
|
18597
|
+
return Buffer.byteLength(line, "utf8") <= cap;
|
|
18598
|
+
}
|
|
18599
|
+
function withTruncatedError(r) {
|
|
18600
|
+
return { ...r, error: r.error ? `${r.error}; ${RESULT_TRUNCATED}` : RESULT_TRUNCATED };
|
|
18601
|
+
}
|
|
18602
|
+
function minimal(r) {
|
|
18603
|
+
if (r.kind === "probe") {
|
|
18604
|
+
const m2 = { kind: "probe", outcomes: {}, memoryPeakBytes: r.memoryPeakBytes, durations_ms: {}, error: RESULT_TRUNCATED };
|
|
18605
|
+
return m2;
|
|
18606
|
+
}
|
|
18607
|
+
if (r.kind === "clone") {
|
|
18608
|
+
const m2 = { kind: "clone", outcome: r.outcome, memoryPeakBytes: r.memoryPeakBytes, durations_ms: {}, error: RESULT_TRUNCATED };
|
|
18609
|
+
return m2;
|
|
18610
|
+
}
|
|
18611
|
+
const m = {
|
|
18612
|
+
kind: "agent",
|
|
18613
|
+
exitCode: r.exitCode,
|
|
18614
|
+
signal: r.signal,
|
|
18615
|
+
claudeStdout: "",
|
|
18616
|
+
affected: null,
|
|
18617
|
+
promptBytes: r.promptBytes,
|
|
18618
|
+
memoryPeakBytes: r.memoryPeakBytes,
|
|
18619
|
+
durations_ms: {},
|
|
18620
|
+
error: RESULT_TRUNCATED
|
|
18621
|
+
};
|
|
18622
|
+
return m;
|
|
18623
|
+
}
|
|
18624
|
+
function formatResultLine(result, nonce, cap = RESULT_MAX_BYTES) {
|
|
18625
|
+
if (!isValidNonce(nonce)) throw new Error("result: invalid nonce");
|
|
18626
|
+
const encode_ = (r) => encode(r, nonce);
|
|
18627
|
+
let line = encode_(result);
|
|
18628
|
+
if (fits(line, cap)) return line;
|
|
18629
|
+
if (result.kind === "agent") {
|
|
18630
|
+
const noAffected = { ...result, affected: null };
|
|
18631
|
+
line = encode_(withTruncatedError(noAffected));
|
|
18632
|
+
if (fits(line, cap)) return line;
|
|
18633
|
+
line = encode_(withTruncatedError({ ...noAffected, claudeStdout: "" }));
|
|
18634
|
+
if (fits(line, cap)) return line;
|
|
18635
|
+
}
|
|
18636
|
+
return encode_(minimal(result));
|
|
18637
|
+
}
|
|
18638
|
+
function isObj(v) {
|
|
18639
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
18640
|
+
}
|
|
18641
|
+
function isNumOrNull(v) {
|
|
18642
|
+
return v === null || typeof v === "number" && Number.isFinite(v);
|
|
18643
|
+
}
|
|
18644
|
+
function shapeError(o2) {
|
|
18645
|
+
if (!isNumOrNull(o2.memoryPeakBytes)) return "memoryPeakBytes";
|
|
18646
|
+
if (!isObj(o2.durations_ms) || !Object.values(o2.durations_ms).every((v) => typeof v === "number")) return "durations_ms";
|
|
18647
|
+
if (o2.error !== void 0 && typeof o2.error !== "string") return "error";
|
|
18648
|
+
if (o2.kind === "clone") {
|
|
18649
|
+
if (o2.outcome !== "diff" && o2.outcome !== "skip" && o2.outcome !== "rejected") return "outcome";
|
|
18650
|
+
if (o2.diff !== void 0 && !isObj(o2.diff)) return "diff";
|
|
18651
|
+
return null;
|
|
18652
|
+
}
|
|
18653
|
+
if (o2.kind === "probe") {
|
|
18654
|
+
if (!isObj(o2.outcomes) || !Object.values(o2.outcomes).every((v) => typeof v === "string")) return "outcomes";
|
|
18655
|
+
return null;
|
|
18656
|
+
}
|
|
18657
|
+
if (o2.kind === "agent") {
|
|
18658
|
+
if (!isNumOrNull(o2.exitCode)) return "exitCode";
|
|
18659
|
+
if (o2.signal !== null && typeof o2.signal !== "string") return "signal";
|
|
18660
|
+
if (typeof o2.claudeStdout !== "string") return "claudeStdout";
|
|
18661
|
+
if (o2.affected !== null && !(isObj(o2.affected) && ["stories", "components", "unresolvedFiles"].every((k) => Array.isArray(o2.affected[k])))) {
|
|
18662
|
+
return "affected";
|
|
18663
|
+
}
|
|
18664
|
+
if (typeof o2.promptBytes !== "number") return "promptBytes";
|
|
18665
|
+
return null;
|
|
18666
|
+
}
|
|
18667
|
+
return "kind";
|
|
18668
|
+
}
|
|
18669
|
+
function parseResultLine(stdout, nonce, cap = RESULT_MAX_BYTES) {
|
|
18670
|
+
if (!isValidNonce(nonce)) return { ok: false, reason: "no result line" };
|
|
18671
|
+
const prefix = `${RESULT_PREFIX}${nonce} `;
|
|
18672
|
+
const lines = stdout.split("\n");
|
|
18673
|
+
let line;
|
|
18674
|
+
for (let i2 = lines.length - 1; i2 >= 0; i2--) {
|
|
18675
|
+
const l = lines[i2].replace(/\r$/, "");
|
|
18676
|
+
if (l.startsWith(prefix)) {
|
|
18677
|
+
line = l;
|
|
18678
|
+
break;
|
|
18679
|
+
}
|
|
18680
|
+
}
|
|
18681
|
+
if (line === void 0) return { ok: false, reason: "no result line" };
|
|
18682
|
+
if (Buffer.byteLength(line, "utf8") > cap) return { ok: false, reason: "result line exceeds cap" };
|
|
18683
|
+
const b64 = line.slice(prefix.length).trim();
|
|
18684
|
+
if (b64 === "" || b64.length % 4 !== 0 || !BASE64_RE.test(b64)) return { ok: false, reason: "result is not base64" };
|
|
18685
|
+
let data;
|
|
18686
|
+
try {
|
|
18687
|
+
data = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
|
|
18688
|
+
} catch {
|
|
18689
|
+
return { ok: false, reason: "result is not JSON" };
|
|
18690
|
+
}
|
|
18691
|
+
if (!isObj(data)) return { ok: false, reason: "result is not an object" };
|
|
18692
|
+
const bad = shapeError(data);
|
|
18693
|
+
if (bad) return { ok: false, reason: `result has a malformed ${bad}` };
|
|
18694
|
+
return { ok: true, result: data };
|
|
18695
|
+
}
|
|
18696
|
+
async function readMemoryPeak(path7 = "/sys/fs/cgroup/memory.peak", read = (p) => readFile13(p, "utf8")) {
|
|
18697
|
+
try {
|
|
18698
|
+
const n2 = Number.parseInt((await read(path7)).trim(), 10);
|
|
18699
|
+
return Number.isFinite(n2) && n2 >= 0 ? n2 : null;
|
|
18700
|
+
} catch {
|
|
18701
|
+
return null;
|
|
18702
|
+
}
|
|
18703
|
+
}
|
|
18704
|
+
var RESULT_TRUNCATED, BASE64_RE;
|
|
18705
|
+
var init_result2 = __esm({
|
|
18706
|
+
"src/daemon/sandboxJob/result.ts"() {
|
|
18707
|
+
"use strict";
|
|
18708
|
+
init_types();
|
|
18709
|
+
RESULT_TRUNCATED = "result truncated";
|
|
18710
|
+
BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
18711
|
+
}
|
|
18712
|
+
});
|
|
18713
|
+
|
|
18486
18714
|
// src/daemon/sandbox/inputDir.ts
|
|
18487
18715
|
import { mkdtemp as mkdtemp3, readdir as readdir2, rm as rm7, writeFile as writeFile10, chmod as chmod4 } from "node:fs/promises";
|
|
18488
18716
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
@@ -18514,893 +18742,1272 @@ var init_inputDir = __esm({
|
|
|
18514
18742
|
}
|
|
18515
18743
|
});
|
|
18516
18744
|
|
|
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
|
-
);
|
|
18745
|
+
// src/daemon/sandbox/sweep.ts
|
|
18746
|
+
async function sweepJobs(deps) {
|
|
18747
|
+
const now = deps.now ?? Date.now;
|
|
18748
|
+
const t0 = now();
|
|
18749
|
+
const failures = [];
|
|
18750
|
+
const boxes = [];
|
|
18751
|
+
const volumes = [];
|
|
18752
|
+
const jobBoxes = (await deps.list()).filter(isJobBox);
|
|
18753
|
+
await Promise.all(jobBoxes.map(async (b) => {
|
|
18754
|
+
try {
|
|
18755
|
+
await deps.kill(b.id).catch(() => void 0);
|
|
18756
|
+
await deps.remove(b.id);
|
|
18757
|
+
boxes.push(b.id);
|
|
18758
|
+
} catch (e) {
|
|
18759
|
+
failures.push(`box ${b.id}: ${e.message}`);
|
|
18540
18760
|
}
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
);
|
|
18761
|
+
}));
|
|
18762
|
+
const jobVolumes = (await deps.listVolumes()).filter(isJobVolume);
|
|
18763
|
+
for (const v of jobVolumes) {
|
|
18764
|
+
try {
|
|
18765
|
+
await deps.removeVolume(v.name);
|
|
18766
|
+
volumes.push(v.name);
|
|
18767
|
+
} catch (e) {
|
|
18768
|
+
failures.push(`volume ${v.name}: ${e.message}`);
|
|
18547
18769
|
}
|
|
18548
|
-
return token;
|
|
18549
|
-
} finally {
|
|
18550
|
-
await fh.close();
|
|
18551
18770
|
}
|
|
18771
|
+
const report = { boxes: boxes.sort(), volumes, ms: now() - t0 };
|
|
18772
|
+
if (failures.length > 0) throw new SweepError(report, failures);
|
|
18773
|
+
return report;
|
|
18552
18774
|
}
|
|
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"() {
|
|
18775
|
+
var JOB_VOLUME_PREFIX, SweepError, isJobBox, isJobVolume;
|
|
18776
|
+
var init_sweep = __esm({
|
|
18777
|
+
"src/daemon/sandbox/sweep.ts"() {
|
|
18591
18778
|
"use strict";
|
|
18592
|
-
|
|
18593
|
-
|
|
18594
|
-
|
|
18595
|
-
|
|
18596
|
-
|
|
18779
|
+
init_types();
|
|
18780
|
+
JOB_VOLUME_PREFIX = "archme-job-";
|
|
18781
|
+
SweepError = class extends Error {
|
|
18782
|
+
constructor(report, failures) {
|
|
18783
|
+
super(`sandbox sweep left ${failures.length} item(s): ${failures.join("; ")}`);
|
|
18784
|
+
this.report = report;
|
|
18785
|
+
this.failures = failures;
|
|
18786
|
+
this.name = "SweepError";
|
|
18597
18787
|
}
|
|
18598
18788
|
};
|
|
18789
|
+
isJobBox = (b) => JOB_LABEL in b.labels;
|
|
18790
|
+
isJobVolume = (v) => v.name.startsWith(JOB_VOLUME_PREFIX) || JOB_LABEL in v.labels;
|
|
18599
18791
|
}
|
|
18600
18792
|
});
|
|
18601
18793
|
|
|
18602
|
-
// src/daemon/sandbox/
|
|
18603
|
-
|
|
18794
|
+
// src/daemon/sandbox/appleContainer.ts
|
|
18795
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
18796
|
+
import { homedir as homedir7 } from "node:os";
|
|
18797
|
+
import { isAbsolute, normalize } from "node:path";
|
|
18798
|
+
function assertName(kind, value) {
|
|
18799
|
+
if (!NAME_RE.test(value)) throw new Error(`sandbox: invalid ${kind} name ${JSON.stringify(value)}`);
|
|
18800
|
+
}
|
|
18801
|
+
function assertImage(image) {
|
|
18802
|
+
if (!image || image.startsWith("-") || /\s/.test(image)) {
|
|
18803
|
+
throw new Error(`sandbox: invalid image ${JSON.stringify(image)}`);
|
|
18804
|
+
}
|
|
18805
|
+
}
|
|
18806
|
+
function labelArgs(labels) {
|
|
18807
|
+
const out = [];
|
|
18808
|
+
for (const [k, v] of Object.entries(labels)) {
|
|
18809
|
+
if (!LABEL_KEY_RE.test(k)) throw new Error(`sandbox: invalid label key ${JSON.stringify(k)}`);
|
|
18810
|
+
if (CONTROL_RE.test(v)) throw new Error(`sandbox: label ${k} value contains a control character`);
|
|
18811
|
+
out.push("--label", `${k}=${v}`);
|
|
18812
|
+
}
|
|
18813
|
+
return out;
|
|
18814
|
+
}
|
|
18815
|
+
function assertInputDir(dir, home2 = homedir7()) {
|
|
18816
|
+
if (!isAbsolute(dir)) throw new Error(`sandbox: inputDir must be absolute: ${dir}`);
|
|
18817
|
+
const strip2 = (p) => p.replace(/\/+$/, "") || "/";
|
|
18818
|
+
const n2 = strip2(normalize(dir));
|
|
18819
|
+
if (n2 !== strip2(dir)) throw new Error(`sandbox: inputDir must be normalized: ${dir}`);
|
|
18820
|
+
if (/[:,]/.test(n2) || CONTROL_RE.test(n2)) throw new Error(`sandbox: inputDir has a forbidden character: ${dir}`);
|
|
18821
|
+
const homeN = strip2(normalize(home2));
|
|
18822
|
+
if (n2 === "/" || n2 === homeN || /^\/(Users|home)(\/[^/]+)?$/.test(n2) || n2 === "/root" || n2 === "/var/root") {
|
|
18823
|
+
throw new Error(`sandbox: inputDir must not be / or a home directory: ${dir}`);
|
|
18824
|
+
}
|
|
18825
|
+
}
|
|
18826
|
+
function envArgs(env) {
|
|
18827
|
+
const out = [];
|
|
18828
|
+
for (const [k, v] of Object.entries(env)) {
|
|
18829
|
+
if (!ENV_KEY_RE.test(k)) throw new Error(`sandbox: invalid env key ${JSON.stringify(k)}`);
|
|
18830
|
+
if (CONTROL_RE.test(v)) throw new Error(`sandbox: env ${k} value contains a newline or NUL`);
|
|
18831
|
+
out.push("-e", `${k}=${v}`);
|
|
18832
|
+
}
|
|
18833
|
+
return out;
|
|
18834
|
+
}
|
|
18835
|
+
function slotFor(index) {
|
|
18836
|
+
if (!Number.isInteger(index) || index < 0 || index >= MAX_SLOTS) {
|
|
18837
|
+
throw new Error(`sandbox: slot index out of range: ${index}`);
|
|
18838
|
+
}
|
|
18839
|
+
const third = SLOT_RANGE_BASE_THIRD_OCTET + index;
|
|
18604
18840
|
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
|
|
18841
|
+
index,
|
|
18842
|
+
network: `archme-slot-${index}`,
|
|
18843
|
+
subnet: `192.168.${third}.0/24`,
|
|
18844
|
+
gateway: `192.168.${third}.1`
|
|
18620
18845
|
};
|
|
18621
18846
|
}
|
|
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],
|
|
18847
|
+
function slotNetwork(slot) {
|
|
18848
|
+
if (!slot || typeof slot !== "object") throw new Error("sandbox: a step needs a slot network");
|
|
18849
|
+
const want = slotFor(slot.index);
|
|
18850
|
+
if (slot.network !== want.network) throw new Error(`sandbox: ${slot.network} is not slot ${slot.index}'s network`);
|
|
18851
|
+
return want.network;
|
|
18852
|
+
}
|
|
18853
|
+
function buildRunArgs(spec, home2) {
|
|
18854
|
+
assertName("box", spec.name);
|
|
18855
|
+
if (spec.volume !== void 0) assertName("volume", spec.volume);
|
|
18856
|
+
assertImage(spec.image);
|
|
18857
|
+
assertInputDir(spec.inputDir, home2);
|
|
18858
|
+
if (!spec.labels[JOB_LABEL]) throw new Error(`sandbox: step labels must include ${JOB_LABEL}`);
|
|
18859
|
+
const { cpus, memoryMiB } = spec.limits;
|
|
18860
|
+
if (!Number.isInteger(cpus) || cpus < 1) throw new Error(`sandbox: invalid cpus ${cpus}`);
|
|
18861
|
+
if (!Number.isInteger(memoryMiB) || memoryMiB < MIN_MEMORY_MIB) {
|
|
18862
|
+
throw new Error(`sandbox: memoryMiB must be an integer \u2265 ${MIN_MEMORY_MIB}: ${memoryMiB}`);
|
|
18863
|
+
}
|
|
18864
|
+
const network = slotNetwork(spec.network);
|
|
18865
|
+
return [
|
|
18866
|
+
// `-i`: stdin carries the per-step result nonce (one line, then EOF) to
|
|
18867
|
+
// the wrapper — the only channel that is neither a file, env nor argv.
|
|
18868
|
+
"run",
|
|
18869
|
+
"-i",
|
|
18870
|
+
"--rm",
|
|
18871
|
+
"--init",
|
|
18872
|
+
"--read-only",
|
|
18873
|
+
"--cap-drop",
|
|
18874
|
+
"ALL",
|
|
18875
|
+
"--user",
|
|
18876
|
+
`${BOX_UID}:${BOX_UID}`,
|
|
18877
|
+
"--tmpfs",
|
|
18878
|
+
"/tmp",
|
|
18879
|
+
"--no-dns",
|
|
18880
|
+
"--kernel-arg",
|
|
18881
|
+
"ipv6.disable=1",
|
|
18882
|
+
// Progress lines otherwise land on stderr ahead of the box's own output.
|
|
18883
|
+
"--progress",
|
|
18884
|
+
"none",
|
|
18885
|
+
"--cpus",
|
|
18886
|
+
String(cpus),
|
|
18887
|
+
"--memory",
|
|
18888
|
+
`${memoryMiB}M`,
|
|
18889
|
+
"--name",
|
|
18890
|
+
spec.name,
|
|
18891
|
+
...labelArgs(spec.labels),
|
|
18892
|
+
"--network",
|
|
18716
18893
|
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
|
-
|
|
18894
|
+
// Verified: `:ro` on a directory bind is a read-only virtiofs mount — even
|
|
18895
|
+
// root in the box gets EROFS on create/mkdir.
|
|
18896
|
+
"-v",
|
|
18897
|
+
`${spec.inputDir}:/job:ro`,
|
|
18898
|
+
...spec.volume !== void 0 ? ["-v", `${spec.volume}:/work`] : [],
|
|
18899
|
+
...envArgs(spec.env),
|
|
18900
|
+
spec.image,
|
|
18901
|
+
...spec.args
|
|
18902
|
+
];
|
|
18903
|
+
}
|
|
18904
|
+
function buildInitVolumeArgs(volume, uid, labels, image, slot) {
|
|
18905
|
+
assertName("volume", volume);
|
|
18906
|
+
assertImage(image);
|
|
18907
|
+
const network = slotNetwork(slot);
|
|
18908
|
+
if (!Number.isInteger(uid) || uid <= 0) throw new Error(`sandbox: invalid uid ${uid}`);
|
|
18909
|
+
return [
|
|
18910
|
+
"run",
|
|
18911
|
+
"--rm",
|
|
18912
|
+
"--init",
|
|
18913
|
+
"--read-only",
|
|
18914
|
+
"--cap-drop",
|
|
18915
|
+
"ALL",
|
|
18916
|
+
"--cap-add",
|
|
18917
|
+
"CHOWN",
|
|
18918
|
+
"--user",
|
|
18919
|
+
"0:0",
|
|
18920
|
+
"--no-dns",
|
|
18921
|
+
"--kernel-arg",
|
|
18922
|
+
"ipv6.disable=1",
|
|
18923
|
+
"--progress",
|
|
18924
|
+
"none",
|
|
18925
|
+
"--network",
|
|
18926
|
+
network,
|
|
18927
|
+
"--memory",
|
|
18928
|
+
`${INIT_MEMORY_MIB}M`,
|
|
18929
|
+
...labelArgs(labels),
|
|
18930
|
+
"-v",
|
|
18931
|
+
`${volume}:/work`,
|
|
18932
|
+
"--entrypoint",
|
|
18933
|
+
"chown",
|
|
18934
|
+
image,
|
|
18935
|
+
`${uid}:${uid}`,
|
|
18936
|
+
"/work"
|
|
18937
|
+
];
|
|
18938
|
+
}
|
|
18939
|
+
function buildCreateVolumeArgs(name, sizeGiB, labels) {
|
|
18940
|
+
assertName("volume", name);
|
|
18941
|
+
if (!Number.isInteger(sizeGiB) || sizeGiB < 1) throw new Error(`sandbox: invalid volume size ${sizeGiB}`);
|
|
18942
|
+
return ["volume", "create", "-s", `${sizeGiB}G`, ...labelArgs(labels), name];
|
|
18943
|
+
}
|
|
18944
|
+
function buildEnsureNetworkArgs(slot) {
|
|
18945
|
+
return [
|
|
18946
|
+
"network",
|
|
18947
|
+
"create",
|
|
18948
|
+
"--internal",
|
|
18949
|
+
"--subnet",
|
|
18950
|
+
slot.subnet,
|
|
18951
|
+
"--label",
|
|
18952
|
+
`${SLOT_LABEL}=${slot.index}`,
|
|
18953
|
+
slot.network
|
|
18954
|
+
];
|
|
18955
|
+
}
|
|
18956
|
+
function labelsOf(v) {
|
|
18957
|
+
const out = {};
|
|
18958
|
+
for (const [k, x] of Object.entries(obj(v))) if (typeof x === "string") out[k] = x;
|
|
18959
|
+
return out;
|
|
18960
|
+
}
|
|
18961
|
+
function parseArray(json, what) {
|
|
18962
|
+
const parsed = JSON.parse(json.trim() || "[]");
|
|
18963
|
+
if (!Array.isArray(parsed)) throw new Error(`sandbox: ${what} is not a JSON array`);
|
|
18964
|
+
return parsed;
|
|
18965
|
+
}
|
|
18966
|
+
function parseBoxList(json) {
|
|
18967
|
+
return parseArray(json, "container ls").flatMap((raw) => {
|
|
18968
|
+
const r = obj(raw);
|
|
18969
|
+
const id = str(r.id) ?? str(obj(r.configuration).id);
|
|
18970
|
+
if (!id) return [];
|
|
18971
|
+
return [{ id, state: str(obj(r.status).state) ?? "unknown", labels: labelsOf(obj(r.configuration).labels) }];
|
|
18972
|
+
});
|
|
18973
|
+
}
|
|
18974
|
+
function parseVolumeList(json) {
|
|
18975
|
+
return parseArray(json, "container volume ls").flatMap((raw) => {
|
|
18976
|
+
const r = obj(raw);
|
|
18977
|
+
const name = str(obj(r.configuration).name) ?? str(r.id);
|
|
18978
|
+
if (!name) return [];
|
|
18979
|
+
return [{ name, labels: labelsOf(obj(r.configuration).labels) }];
|
|
18980
|
+
});
|
|
18981
|
+
}
|
|
18982
|
+
function parseNetworkList(json) {
|
|
18983
|
+
return parseArray(json, "container network ls").flatMap((raw) => {
|
|
18984
|
+
const r = obj(raw);
|
|
18985
|
+
const c3 = obj(r.configuration);
|
|
18986
|
+
const name = str(c3.name) ?? str(r.id);
|
|
18987
|
+
if (!name) return [];
|
|
18988
|
+
return [{
|
|
18989
|
+
name,
|
|
18990
|
+
subnet: str(c3.ipv4Subnet) ?? str(obj(r.status).ipv4Subnet),
|
|
18991
|
+
mode: str(c3.mode),
|
|
18992
|
+
labels: labelsOf(c3.labels)
|
|
18993
|
+
}];
|
|
18994
|
+
});
|
|
18995
|
+
}
|
|
18996
|
+
function isBoxNotFound(stderr) {
|
|
18997
|
+
return /notFound: "container with ID .* not found"/.test(stderr);
|
|
18998
|
+
}
|
|
18999
|
+
function isBoxStopped(stderr) {
|
|
19000
|
+
return /invalidState: "no runtime client exists: container is stopped"/.test(stderr);
|
|
19001
|
+
}
|
|
19002
|
+
function defaultExec2(bin = "container") {
|
|
19003
|
+
return async (args) => {
|
|
19004
|
+
const r = await execa(bin, args, { reject: false, stdin: "ignore" });
|
|
19005
|
+
const stderr = typeof r.stderr === "string" ? r.stderr : "";
|
|
18806
19006
|
return {
|
|
18807
|
-
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18811
|
-
stderr: agent.exit.stderr,
|
|
18812
|
-
affected: ar.affected,
|
|
18813
|
-
metrics
|
|
19007
|
+
exitCode: r.exitCode ?? -1,
|
|
19008
|
+
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
19009
|
+
stderr: r.exitCode === void 0 ? `${stderr}
|
|
19010
|
+
${r.message}`.trim() : stderr
|
|
18814
19011
|
};
|
|
18815
19012
|
};
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
}
|
|
18829
|
-
deps.log(`sandbox cleanup (${what}) failed: ${e.message}`);
|
|
18830
|
-
}
|
|
19013
|
+
}
|
|
19014
|
+
function defaultSpawn(bin = "container") {
|
|
19015
|
+
return (args) => {
|
|
19016
|
+
const p = execa(bin, args, { reject: false, stdin: "pipe", buffer: false });
|
|
19017
|
+
return {
|
|
19018
|
+
stdin: p.stdin,
|
|
19019
|
+
stdout: p.stdout,
|
|
19020
|
+
stderr: p.stderr,
|
|
19021
|
+
exited: p.then((r) => ({
|
|
19022
|
+
exitCode: r.exitCode ?? null,
|
|
19023
|
+
signal: r.signal ?? null,
|
|
19024
|
+
error: r.exitCode === void 0 && !r.signal ? r.message : void 0
|
|
19025
|
+
}))
|
|
18831
19026
|
};
|
|
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
|
-
}
|
|
19027
|
+
};
|
|
18840
19028
|
}
|
|
18841
|
-
var
|
|
18842
|
-
var
|
|
18843
|
-
"src/daemon/sandbox/
|
|
19029
|
+
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;
|
|
19030
|
+
var init_appleContainer = __esm({
|
|
19031
|
+
"src/daemon/sandbox/appleContainer.ts"() {
|
|
18844
19032
|
"use strict";
|
|
19033
|
+
init_execa();
|
|
19034
|
+
init_sweep();
|
|
18845
19035
|
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
|
-
|
|
19036
|
+
MIN_MEMORY_MIB = 200;
|
|
19037
|
+
STDOUT_CAP_BYTES = 4 * 1024 * 1024;
|
|
19038
|
+
STDERR_CAP_BYTES = 1024 * 1024;
|
|
19039
|
+
INIT_MEMORY_MIB = 256;
|
|
19040
|
+
NAME_RE = /^[a-z0-9][a-z0-9_.-]*$/;
|
|
19041
|
+
ENV_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
19042
|
+
LABEL_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
19043
|
+
CONTROL_RE = /[\u0000\n\r]/;
|
|
19044
|
+
obj = (v) => v && typeof v === "object" ? v : {};
|
|
19045
|
+
str = (v) => typeof v === "string" ? v : null;
|
|
19046
|
+
TailBuffer = class {
|
|
19047
|
+
constructor(cap) {
|
|
19048
|
+
this.cap = cap;
|
|
19049
|
+
}
|
|
19050
|
+
chunks = [];
|
|
19051
|
+
size = 0;
|
|
19052
|
+
truncated = false;
|
|
19053
|
+
push(chunk) {
|
|
19054
|
+
this.chunks.push(chunk);
|
|
19055
|
+
this.size += chunk.length;
|
|
19056
|
+
while (this.size > this.cap && this.chunks.length > 0) {
|
|
19057
|
+
const head = this.chunks[0];
|
|
19058
|
+
const over = this.size - this.cap;
|
|
19059
|
+
this.truncated = true;
|
|
19060
|
+
if (head.length <= over) {
|
|
19061
|
+
this.chunks.shift();
|
|
19062
|
+
this.size -= head.length;
|
|
19063
|
+
} else {
|
|
19064
|
+
this.chunks[0] = head.subarray(over);
|
|
19065
|
+
this.size -= over;
|
|
19066
|
+
}
|
|
19067
|
+
}
|
|
19068
|
+
}
|
|
19069
|
+
toString() {
|
|
19070
|
+
let buf = Buffer.concat(this.chunks);
|
|
19071
|
+
if (this.truncated) {
|
|
19072
|
+
let i2 = 0;
|
|
19073
|
+
while (i2 < buf.length && i2 < 3 && (buf[i2] & 192) === 128) i2++;
|
|
19074
|
+
buf = buf.subarray(i2);
|
|
19075
|
+
}
|
|
19076
|
+
return buf.toString("utf8");
|
|
19077
|
+
}
|
|
19078
|
+
};
|
|
19079
|
+
AppleContainerRuntime = class {
|
|
19080
|
+
constructor(opts = {}) {
|
|
19081
|
+
this.opts = opts;
|
|
19082
|
+
this.exec = opts.exec ?? defaultExec2(opts.bin);
|
|
19083
|
+
this.spawn = opts.spawn ?? defaultSpawn(opts.bin);
|
|
19084
|
+
}
|
|
19085
|
+
name = "apple-container";
|
|
19086
|
+
exec;
|
|
19087
|
+
spawn;
|
|
19088
|
+
/** Boxes `kill()` was called for while their step was running. */
|
|
19089
|
+
killedNames = /* @__PURE__ */ new Set();
|
|
19090
|
+
running = /* @__PURE__ */ new Set();
|
|
19091
|
+
async must(args, what) {
|
|
19092
|
+
const r = await this.exec(args);
|
|
19093
|
+
if (r.exitCode !== 0) throw new Error(`sandbox: ${what} failed (exit ${r.exitCode}): ${r.stderr.trim()}`);
|
|
19094
|
+
return r;
|
|
19095
|
+
}
|
|
19096
|
+
async preflight() {
|
|
19097
|
+
const checks = [];
|
|
19098
|
+
const status = await this.exec(["system", "status"]);
|
|
19099
|
+
const up = status.exitCode === 0 && /^status\s+running\b/m.test(status.stdout);
|
|
19100
|
+
checks.push({ name: "runtime_status", ok: up, detail: up ? void 0 : (status.stderr || status.stdout).trim() });
|
|
19101
|
+
if (this.opts.image) {
|
|
19102
|
+
let img = await this.exec(["image", "inspect", this.opts.image]);
|
|
19103
|
+
if (img.exitCode !== 0 && up && this.opts.image.includes("@sha256:")) {
|
|
19104
|
+
const pull = await this.exec(["image", "pull", this.opts.image]);
|
|
19105
|
+
img = pull.exitCode === 0 ? await this.exec(["image", "inspect", this.opts.image]) : pull;
|
|
19106
|
+
}
|
|
19107
|
+
checks.push({ name: "image_present", ok: img.exitCode === 0, detail: img.exitCode === 0 ? void 0 : img.stderr.trim() });
|
|
19108
|
+
} else {
|
|
19109
|
+
checks.push({ name: "image_present", ok: false, detail: "no image configured" });
|
|
19110
|
+
}
|
|
19111
|
+
const want = this.opts.slots ?? 0;
|
|
19112
|
+
if (up) {
|
|
19113
|
+
const nets = await this.listNetworks();
|
|
19114
|
+
const missing = [];
|
|
19115
|
+
for (let i2 = 0; i2 < want; i2++) {
|
|
19116
|
+
const s = slotFor(i2);
|
|
19117
|
+
const n2 = nets.find((x) => x.name === s.network);
|
|
19118
|
+
if (!n2 || n2.subnet !== s.subnet) missing.push(n2 ? `${s.network} (subnet ${n2.subnet})` : s.network);
|
|
19119
|
+
}
|
|
19120
|
+
checks.push({ name: "slot_networks", ok: missing.length === 0, detail: missing.length ? `missing/wrong: ${missing.join(", ")}` : void 0 });
|
|
19121
|
+
} else {
|
|
19122
|
+
checks.push({ name: "slot_networks", ok: false, detail: "runtime not running" });
|
|
19123
|
+
}
|
|
19124
|
+
return { ok: checks.every((c3) => c3.ok), checks };
|
|
19125
|
+
}
|
|
19126
|
+
async listNetworks() {
|
|
19127
|
+
const r = await this.must(["network", "ls", "--format", "json"], "network ls");
|
|
19128
|
+
return parseNetworkList(r.stdout);
|
|
19129
|
+
}
|
|
19130
|
+
async ensureSlots(n2) {
|
|
19131
|
+
if (!Number.isInteger(n2) || n2 < 1 || n2 > MAX_SLOTS) throw new Error(`sandbox: slot count out of range: ${n2}`);
|
|
19132
|
+
const slots = Array.from({ length: n2 }, (_, i2) => slotFor(i2));
|
|
19133
|
+
let nets = await this.listNetworks();
|
|
19134
|
+
for (const s of slots) {
|
|
19135
|
+
const existing = nets.find((x) => x.name === s.network);
|
|
19136
|
+
if (existing) {
|
|
19137
|
+
if (existing.subnet !== s.subnet) {
|
|
19138
|
+
throw new Error(`sandbox: ${s.network} exists with subnet ${existing.subnet}, expected ${s.subnet}`);
|
|
19139
|
+
}
|
|
19140
|
+
continue;
|
|
19141
|
+
}
|
|
19142
|
+
const r = await this.exec(buildEnsureNetworkArgs(s));
|
|
19143
|
+
if (r.exitCode !== 0) {
|
|
19144
|
+
nets = await this.listNetworks();
|
|
19145
|
+
const now = nets.find((x) => x.name === s.network);
|
|
19146
|
+
if (!now || now.subnet !== s.subnet) {
|
|
19147
|
+
throw new Error(`sandbox: creating ${s.network} failed: ${r.stderr.trim()}`);
|
|
19148
|
+
}
|
|
19149
|
+
}
|
|
19150
|
+
}
|
|
19151
|
+
return slots;
|
|
19152
|
+
}
|
|
19153
|
+
async createVolume(name, sizeGiB, labels) {
|
|
19154
|
+
await this.must(buildCreateVolumeArgs(name, sizeGiB, labels), `volume create ${name}`);
|
|
19155
|
+
}
|
|
19156
|
+
async initVolume(name, uid, labels, image, slot) {
|
|
19157
|
+
await this.must(buildInitVolumeArgs(name, uid, labels, image, slot), `volume init ${name}`);
|
|
19158
|
+
}
|
|
19159
|
+
async runStep(spec) {
|
|
19160
|
+
const args = buildRunArgs(spec, this.opts.homeDir);
|
|
19161
|
+
const name = spec.name;
|
|
19162
|
+
this.running.add(name);
|
|
19163
|
+
this.killedNames.delete(name);
|
|
19164
|
+
const out = new TailBuffer(STDOUT_CAP_BYTES);
|
|
19165
|
+
const err4 = new TailBuffer(STDERR_CAP_BYTES);
|
|
19166
|
+
const nonce = randomBytes2(16).toString("hex");
|
|
19167
|
+
const proc = this.spawn(args);
|
|
19168
|
+
if (proc.stdin) {
|
|
19169
|
+
proc.stdin.on("error", () => {
|
|
19170
|
+
});
|
|
19171
|
+
proc.stdin.end(`${nonce}
|
|
19172
|
+
`);
|
|
19173
|
+
}
|
|
19174
|
+
const tee = (stream, buf, which) => new Promise((resolve7) => {
|
|
19175
|
+
if (!stream) return resolve7();
|
|
19176
|
+
stream.on("data", (c3) => {
|
|
19177
|
+
const b = typeof c3 === "string" ? Buffer.from(c3) : c3;
|
|
19178
|
+
buf.push(b);
|
|
19179
|
+
this.opts.onOutput?.(name, which, b);
|
|
19180
|
+
});
|
|
19181
|
+
stream.on("end", () => resolve7());
|
|
19182
|
+
stream.on("close", () => resolve7());
|
|
19183
|
+
stream.on("error", () => resolve7());
|
|
19184
|
+
});
|
|
19185
|
+
const done = (async () => {
|
|
19186
|
+
const [exit] = await Promise.all([proc.exited, tee(proc.stdout, out, "stdout"), tee(proc.stderr, err4, "stderr")]);
|
|
19187
|
+
const killed = this.killedNames.has(name);
|
|
19188
|
+
this.killedNames.delete(name);
|
|
19189
|
+
this.running.delete(name);
|
|
19190
|
+
const stderr = err4.toString();
|
|
19191
|
+
return {
|
|
19192
|
+
exitCode: exit.exitCode,
|
|
19193
|
+
signal: exit.signal,
|
|
19194
|
+
stdout: out.toString(),
|
|
19195
|
+
stderr: exit.error ? `${stderr}
|
|
19196
|
+
${exit.error}`.trim() : stderr,
|
|
19197
|
+
killed,
|
|
19198
|
+
nonce
|
|
19199
|
+
};
|
|
19200
|
+
})();
|
|
19201
|
+
return { name, done };
|
|
19202
|
+
}
|
|
19203
|
+
/** Not-found and already-stopped both count as success: the box is not running. */
|
|
19204
|
+
async kill(name) {
|
|
19205
|
+
assertName("box", name);
|
|
19206
|
+
if (this.running.has(name)) this.killedNames.add(name);
|
|
19207
|
+
const r = await this.exec(["kill", name]);
|
|
19208
|
+
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr) && !isBoxStopped(r.stderr)) {
|
|
19209
|
+
throw new Error(`sandbox: kill ${name} failed: ${r.stderr.trim()}`);
|
|
19210
|
+
}
|
|
19211
|
+
}
|
|
19212
|
+
async stop(name, graceSeconds) {
|
|
19213
|
+
assertName("box", name);
|
|
19214
|
+
const t = Math.max(0, Math.floor(graceSeconds));
|
|
19215
|
+
const r = await this.exec(["stop", "-t", String(t), name]);
|
|
19216
|
+
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr) && !isBoxStopped(r.stderr)) {
|
|
19217
|
+
throw new Error(`sandbox: stop ${name} failed: ${r.stderr.trim()}`);
|
|
19218
|
+
}
|
|
19219
|
+
}
|
|
19220
|
+
/** `container rm`; not-found counts as removed (a `--rm` box is gone once stopped). */
|
|
19221
|
+
async remove(name) {
|
|
19222
|
+
assertName("box", name);
|
|
19223
|
+
const r = await this.exec(["rm", name]);
|
|
19224
|
+
if (r.exitCode !== 0 && !isBoxNotFound(r.stderr)) {
|
|
19225
|
+
throw new Error(`sandbox: rm ${name} failed: ${r.stderr.trim()}`);
|
|
19226
|
+
}
|
|
19227
|
+
}
|
|
19228
|
+
async removeVolume(name) {
|
|
19229
|
+
assertName("volume", name);
|
|
19230
|
+
const r = await this.exec(["volume", "rm", name]);
|
|
19231
|
+
if (r.exitCode === 0) return;
|
|
19232
|
+
const still = (await this.listVolumes()).some((v) => v.name === name);
|
|
19233
|
+
if (still) throw new Error(`sandbox: volume rm ${name} failed: ${r.stderr.trim()}`);
|
|
19234
|
+
}
|
|
19235
|
+
async list() {
|
|
19236
|
+
const r = await this.must(["ls", "-a", "--format", "json"], "ls");
|
|
19237
|
+
return parseBoxList(r.stdout);
|
|
19238
|
+
}
|
|
19239
|
+
async listVolumes() {
|
|
19240
|
+
const r = await this.must(["volume", "ls", "--format", "json"], "volume ls");
|
|
19241
|
+
return parseVolumeList(r.stdout);
|
|
19242
|
+
}
|
|
19243
|
+
sweep() {
|
|
19244
|
+
return sweepJobs(this);
|
|
19245
|
+
}
|
|
18870
19246
|
};
|
|
18871
19247
|
}
|
|
18872
|
-
|
|
18873
|
-
|
|
19248
|
+
});
|
|
19249
|
+
|
|
19250
|
+
// src/daemon/sandbox/slotMap.ts
|
|
19251
|
+
function parseIpv4(addr) {
|
|
19252
|
+
const m = IPV4_RE.exec(addr);
|
|
19253
|
+
if (!m) return null;
|
|
19254
|
+
let v = 0;
|
|
19255
|
+
for (let i2 = 1; i2 <= 4; i2++) {
|
|
19256
|
+
const o2 = Number(m[i2]);
|
|
19257
|
+
if (o2 > 255) return null;
|
|
19258
|
+
v = v * 256 + o2;
|
|
18874
19259
|
}
|
|
18875
|
-
return
|
|
19260
|
+
return v >>> 0;
|
|
18876
19261
|
}
|
|
18877
|
-
function
|
|
18878
|
-
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
|
|
18885
|
-
};
|
|
18886
|
-
}
|
|
18887
|
-
return { kind: "fail" };
|
|
19262
|
+
function parseCidr(cidr) {
|
|
19263
|
+
const [ip, bits, extra] = cidr.split("/");
|
|
19264
|
+
if (ip === void 0 || bits === void 0 || extra !== void 0 || !/^\d{1,2}$/.test(bits)) return null;
|
|
19265
|
+
const n2 = Number(bits);
|
|
19266
|
+
const base = parseIpv4(ip);
|
|
19267
|
+
if (base === null || n2 > 32) return null;
|
|
19268
|
+
const mask2 = n2 === 0 ? 0 : ~0 << 32 - n2 >>> 0;
|
|
19269
|
+
return { base: (base & mask2) >>> 0, mask: mask2 };
|
|
18888
19270
|
}
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
|
|
19271
|
+
function stripMapped(addr) {
|
|
19272
|
+
return (addr ?? "").replace(/^::ffff:/i, "");
|
|
19273
|
+
}
|
|
19274
|
+
var IPV4_RE, SlotMap;
|
|
19275
|
+
var init_slotMap = __esm({
|
|
19276
|
+
"src/daemon/sandbox/slotMap.ts"() {
|
|
18892
19277
|
"use strict";
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
this.
|
|
19278
|
+
init_appleContainer();
|
|
19279
|
+
IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
19280
|
+
SlotMap = class {
|
|
19281
|
+
byIndex = /* @__PURE__ */ new Map();
|
|
19282
|
+
byGateway = /* @__PURE__ */ new Map();
|
|
19283
|
+
allowGatewayAsPeer;
|
|
19284
|
+
constructor(slots, opts = {}) {
|
|
19285
|
+
this.allowGatewayAsPeer = opts.allowGatewayAsPeer ?? false;
|
|
19286
|
+
for (const slot of slots) {
|
|
19287
|
+
const gateway = parseIpv4(slot.gateway);
|
|
19288
|
+
const cidr = parseCidr(slot.subnet);
|
|
19289
|
+
if (gateway === null) throw new Error(`slot ${slot.index}: gateway is not an IPv4 address`);
|
|
19290
|
+
if (cidr === null) throw new Error(`slot ${slot.index}: subnet is not an IPv4 CIDR`);
|
|
19291
|
+
if (this.byIndex.has(slot.index)) throw new Error(`slot ${slot.index}: duplicate index`);
|
|
19292
|
+
if (this.byGateway.has(slot.gateway)) throw new Error(`slot ${slot.index}: duplicate gateway`);
|
|
19293
|
+
const entry = { slot, gateway, cidr, binding: null };
|
|
19294
|
+
this.byIndex.set(slot.index, entry);
|
|
19295
|
+
this.byGateway.set(slot.gateway, entry);
|
|
19296
|
+
}
|
|
19297
|
+
}
|
|
19298
|
+
/**
|
|
19299
|
+
* The slot a connection belongs to, or null (drop it). `local` must be a slot
|
|
19300
|
+
* gateway; `remote` must be a plain IPv4 inside THAT slot's subnet — never
|
|
19301
|
+
* IPv6, never another slot's range.
|
|
19302
|
+
*/
|
|
19303
|
+
resolve(localAddress, remoteAddress) {
|
|
19304
|
+
const entry = this.byGateway.get(stripMapped(localAddress));
|
|
19305
|
+
if (!entry) return null;
|
|
19306
|
+
const remoteStr = stripMapped(remoteAddress);
|
|
19307
|
+
const remote = parseIpv4(remoteStr);
|
|
19308
|
+
if (remote === null) return null;
|
|
19309
|
+
if ((remote & entry.cidr.mask) >>> 0 !== entry.cidr.base) return null;
|
|
19310
|
+
if (remote === entry.gateway && !this.allowGatewayAsPeer) return null;
|
|
19311
|
+
return entry.slot;
|
|
19312
|
+
}
|
|
19313
|
+
slots() {
|
|
19314
|
+
return [...this.byIndex.values()].map((e) => e.slot);
|
|
19315
|
+
}
|
|
19316
|
+
slot(index) {
|
|
19317
|
+
return this.byIndex.get(index)?.slot ?? null;
|
|
19318
|
+
}
|
|
19319
|
+
bind(index, binding) {
|
|
19320
|
+
const entry = this.entry(index);
|
|
19321
|
+
if (entry.binding && entry.binding.jobId !== binding.jobId) {
|
|
19322
|
+
throw new Error(`slot ${index} is bound to another job`);
|
|
19323
|
+
}
|
|
19324
|
+
entry.binding = binding;
|
|
19325
|
+
}
|
|
19326
|
+
clear(index) {
|
|
19327
|
+
this.entry(index).binding = null;
|
|
19328
|
+
}
|
|
19329
|
+
binding(index) {
|
|
19330
|
+
return this.byIndex.get(index)?.binding ?? null;
|
|
19331
|
+
}
|
|
19332
|
+
bindingForJob(jobId) {
|
|
19333
|
+
for (const e of this.byIndex.values()) {
|
|
19334
|
+
if (e.binding?.jobId === jobId) return { slot: e.slot, binding: e.binding };
|
|
19335
|
+
}
|
|
19336
|
+
return null;
|
|
19337
|
+
}
|
|
19338
|
+
entry(index) {
|
|
19339
|
+
const e = this.byIndex.get(index);
|
|
19340
|
+
if (!e) throw new Error(`unknown slot ${index}`);
|
|
19341
|
+
return e;
|
|
18901
19342
|
}
|
|
18902
19343
|
};
|
|
18903
19344
|
}
|
|
18904
19345
|
});
|
|
18905
19346
|
|
|
18906
|
-
// src/daemon/sandbox/
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
const
|
|
19347
|
+
// src/daemon/sandbox/confinement.ts
|
|
19348
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
19349
|
+
function probeTargets(slot, lanIp) {
|
|
19350
|
+
const gw = slot.gateway;
|
|
19351
|
+
return [
|
|
19352
|
+
...HOST_SERVICE_PORTS.map((port) => ({ id: `host_${port}`, kind: "tcp", host: gw, port })),
|
|
19353
|
+
...lanIp ? LAN_PORTS.map((port) => ({ id: `lan_${port}`, kind: "tcp", host: lanIp, port })) : [],
|
|
19354
|
+
{ id: "public_443", kind: "tcp", host: PUBLIC_PROBE_IP, port: 443 },
|
|
19355
|
+
{ id: "public_dns", kind: "udp_dns", host: PUBLIC_PROBE_IP, port: 53 },
|
|
19356
|
+
{ id: "ipv6", kind: "ipv6_socket", host: "::", port: 0 },
|
|
19357
|
+
{ id: "gate_anthropic", kind: "http", host: gw, port: GATE_PORTS.anthropic },
|
|
19358
|
+
{ id: "gate_mcp", kind: "http", host: gw, port: GATE_PORTS.mcp },
|
|
19359
|
+
{ id: "gate_github", kind: "connect", host: gw, port: GATE_PORTS.github }
|
|
19360
|
+
];
|
|
19361
|
+
}
|
|
19362
|
+
function expectation(id) {
|
|
19363
|
+
if (id.startsWith("host_") || id.startsWith("lan_")) return { want: "timeout", ok: (o2) => o2 === "timeout" };
|
|
19364
|
+
if (id === "public_dns") return { want: "silent", ok: (o2) => o2 === "silent" };
|
|
19365
|
+
if (id.startsWith("public_")) return { want: "not connected", ok: (o2) => o2 !== "connected" };
|
|
19366
|
+
if (id === "ipv6") return { want: "unsupported", ok: (o2) => o2 === "unsupported" };
|
|
19367
|
+
if (id.startsWith("gate_")) return { want: "an HTTP status", ok: (o2) => /^\d{3}$/.test(o2) };
|
|
19368
|
+
return { want: "a known target", ok: () => false };
|
|
19369
|
+
}
|
|
19370
|
+
function judgeProbe(outcomes, targets) {
|
|
19371
|
+
const ids = targets ? targets.map((t) => t.id) : Object.keys(outcomes);
|
|
19372
|
+
for (const id of ["host_22", "public_443", "public_dns", "ipv6", "gate_anthropic", "gate_mcp", "gate_github"]) {
|
|
19373
|
+
if (!ids.includes(id)) ids.push(id);
|
|
19374
|
+
}
|
|
18910
19375
|
const failures = [];
|
|
18911
|
-
const
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
18920
|
-
|
|
19376
|
+
for (const id of ids) {
|
|
19377
|
+
const o2 = outcomes[id];
|
|
19378
|
+
const e = expectation(id);
|
|
19379
|
+
if (o2 === void 0) failures.push(`${id}: no outcome (want: ${e.want})`);
|
|
19380
|
+
else if (!e.ok(o2)) failures.push(`${id}: ${o2} (want: ${e.want})`);
|
|
19381
|
+
}
|
|
19382
|
+
return { ok: failures.length === 0, failures };
|
|
19383
|
+
}
|
|
19384
|
+
function primaryLanIpv4(ifaces) {
|
|
19385
|
+
const lo = parseIpv4(`192.168.${SLOT_RANGE_BASE_THIRD_OCTET}.0`);
|
|
19386
|
+
for (const list2 of Object.values(ifaces)) {
|
|
19387
|
+
for (const i2 of list2 ?? []) {
|
|
19388
|
+
if (i2.internal || i2.family !== "IPv4") continue;
|
|
19389
|
+
const v = parseIpv4(i2.address);
|
|
19390
|
+
if (v === null || (v & 4294965248) >>> 0 === lo) continue;
|
|
19391
|
+
return i2.address;
|
|
18921
19392
|
}
|
|
18922
|
-
}
|
|
18923
|
-
|
|
18924
|
-
|
|
18925
|
-
|
|
18926
|
-
|
|
18927
|
-
|
|
18928
|
-
|
|
18929
|
-
|
|
19393
|
+
}
|
|
19394
|
+
return null;
|
|
19395
|
+
}
|
|
19396
|
+
async function runProbeBox(deps, slot, targets, targetTimeoutMs = TARGET_TIMEOUT_MS) {
|
|
19397
|
+
const id = randomBytes3(6).toString("hex");
|
|
19398
|
+
const name = `archme-probe-${id}`;
|
|
19399
|
+
const input = { kind: "probe", targets, timeoutMs: targetTimeoutMs };
|
|
19400
|
+
let cleanup = null;
|
|
19401
|
+
let timer;
|
|
19402
|
+
try {
|
|
19403
|
+
const dir = await (deps.writeInputDir ?? writeInputDir)("probe", input);
|
|
19404
|
+
cleanup = dir.cleanup;
|
|
19405
|
+
const handle = await deps.runtime.runStep({
|
|
19406
|
+
name,
|
|
19407
|
+
image: deps.image,
|
|
19408
|
+
args: ["probe"],
|
|
19409
|
+
network: slot,
|
|
19410
|
+
env: {},
|
|
19411
|
+
inputDir: dir.dir,
|
|
19412
|
+
limits: { cpus: 1, memoryMiB: 256 },
|
|
19413
|
+
labels: { [JOB_LABEL]: `probe-${id}`, [SLOT_LABEL]: String(slot.index) }
|
|
19414
|
+
});
|
|
19415
|
+
const timeout = new Promise((r) => {
|
|
19416
|
+
timer = setTimeout(() => r("timeout"), deps.boxTimeoutMs ?? 6e4);
|
|
19417
|
+
});
|
|
19418
|
+
const exit = await Promise.race([handle.done, timeout]);
|
|
19419
|
+
if (exit === "timeout") return { outcomes: {}, error: "probe box timed out" };
|
|
19420
|
+
const parsed = (deps.parseResultLine ?? parseResultLine)(exit.stdout, exit.nonce);
|
|
19421
|
+
if (!parsed.ok || parsed.result.kind !== "probe") {
|
|
19422
|
+
return { outcomes: {}, error: `probe result: ${parsed.ok ? `unexpected kind ${parsed.result.kind}` : parsed.reason}` };
|
|
18930
19423
|
}
|
|
19424
|
+
return { outcomes: parsed.result.outcomes, error: parsed.result.error ? `probe box: ${parsed.result.error}` : null };
|
|
19425
|
+
} catch (e) {
|
|
19426
|
+
return { outcomes: {}, error: `probe box failed: ${e.message}` };
|
|
19427
|
+
} finally {
|
|
19428
|
+
clearTimeout(timer);
|
|
19429
|
+
await deps.runtime.kill(name).catch((e) => deps.log?.(`kill ${name} failed: ${e.message}`));
|
|
19430
|
+
await cleanup?.();
|
|
18931
19431
|
}
|
|
18932
|
-
const report = { boxes: boxes.sort(), volumes, ms: now() - t0 };
|
|
18933
|
-
if (failures.length > 0) throw new SweepError(report, failures);
|
|
18934
|
-
return report;
|
|
18935
19432
|
}
|
|
18936
|
-
|
|
18937
|
-
|
|
18938
|
-
|
|
19433
|
+
async function runConfinementProbe(deps, slot, lanIp) {
|
|
19434
|
+
const now = deps.now ?? Date.now;
|
|
19435
|
+
const started = now();
|
|
19436
|
+
const targets = probeTargets(slot, lanIp);
|
|
19437
|
+
const { outcomes, error } = await runProbeBox(deps, slot, targets);
|
|
19438
|
+
const j = judgeProbe(outcomes, targets);
|
|
19439
|
+
const failures = error ? [error, ...Object.keys(outcomes).length ? j.failures : []] : j.failures;
|
|
19440
|
+
return { ok: !error && j.ok, slot: slot.index, failures, outcomes, ms: now() - started };
|
|
19441
|
+
}
|
|
19442
|
+
function formatConfinementReport(r) {
|
|
19443
|
+
return r.ok ? `confinement probe passed on slot ${r.slot} (${r.ms}ms)` : `confinement probe failed on slot ${r.slot}: ${r.failures.join("; ")}`;
|
|
19444
|
+
}
|
|
19445
|
+
var PUBLIC_PROBE_IP, HOST_SERVICE_PORTS, LAN_PORTS, TARGET_TIMEOUT_MS, CONFINEMENT_PROBE_INTERVAL_MS, CONFINEMENT_RETRY_MS, realTimers, ConfinementMonitor;
|
|
19446
|
+
var init_confinement = __esm({
|
|
19447
|
+
"src/daemon/sandbox/confinement.ts"() {
|
|
18939
19448
|
"use strict";
|
|
19449
|
+
init_result2();
|
|
19450
|
+
init_inputDir();
|
|
19451
|
+
init_slotMap();
|
|
18940
19452
|
init_types();
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
|
|
18944
|
-
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
19453
|
+
PUBLIC_PROBE_IP = "1.1.1.1";
|
|
19454
|
+
HOST_SERVICE_PORTS = [22, 88, 5e3, 7e3];
|
|
19455
|
+
LAN_PORTS = [22, 5e3];
|
|
19456
|
+
TARGET_TIMEOUT_MS = 2e3;
|
|
19457
|
+
CONFINEMENT_PROBE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
19458
|
+
CONFINEMENT_RETRY_MS = 5 * 60 * 1e3;
|
|
19459
|
+
realTimers = {
|
|
19460
|
+
setTimeout: (fn, ms) => {
|
|
19461
|
+
const t = setTimeout(fn, ms);
|
|
19462
|
+
t.unref?.();
|
|
19463
|
+
return t;
|
|
19464
|
+
},
|
|
19465
|
+
clearTimeout: (h2) => clearTimeout(h2)
|
|
19466
|
+
};
|
|
19467
|
+
ConfinementMonitor = class {
|
|
19468
|
+
constructor(opts) {
|
|
19469
|
+
this.opts = opts;
|
|
19470
|
+
this.timers = opts.timers ?? realTimers;
|
|
19471
|
+
}
|
|
19472
|
+
timers;
|
|
19473
|
+
handle = null;
|
|
19474
|
+
failing = false;
|
|
19475
|
+
probing = false;
|
|
19476
|
+
stopped = false;
|
|
19477
|
+
/** The boot probe already ran; the next is an hour out. */
|
|
19478
|
+
start() {
|
|
19479
|
+
this.stopped = false;
|
|
19480
|
+
this.schedule(CONFINEMENT_PROBE_INTERVAL_MS);
|
|
19481
|
+
}
|
|
19482
|
+
stop() {
|
|
19483
|
+
this.stopped = true;
|
|
19484
|
+
if (this.handle !== null) this.timers.clearTimeout(this.handle);
|
|
19485
|
+
this.handle = null;
|
|
19486
|
+
}
|
|
19487
|
+
allowsClaims() {
|
|
19488
|
+
return this.opts.mode !== "required" || !this.failing && !this.probing;
|
|
19489
|
+
}
|
|
19490
|
+
schedule(ms) {
|
|
19491
|
+
if (this.stopped) return;
|
|
19492
|
+
if (this.handle !== null) this.timers.clearTimeout(this.handle);
|
|
19493
|
+
this.handle = this.timers.setTimeout(() => {
|
|
19494
|
+
this.handle = null;
|
|
19495
|
+
void this.runOnce();
|
|
19496
|
+
}, ms);
|
|
19497
|
+
}
|
|
19498
|
+
async runOnce() {
|
|
19499
|
+
const slot = this.opts.pool.lease();
|
|
19500
|
+
if (!slot) {
|
|
19501
|
+
this.opts.log("confinement probe: no free slot, retrying in 5 minutes");
|
|
19502
|
+
return this.schedule(CONFINEMENT_RETRY_MS);
|
|
19503
|
+
}
|
|
19504
|
+
this.probing = true;
|
|
19505
|
+
let report;
|
|
19506
|
+
try {
|
|
19507
|
+
report = await this.opts.probe(slot);
|
|
19508
|
+
} catch (e) {
|
|
19509
|
+
report = { ok: false, slot: slot.index, failures: [`probe threw: ${e.message}`], outcomes: {}, ms: 0 };
|
|
19510
|
+
} finally {
|
|
19511
|
+
this.opts.pool.free(slot.index);
|
|
19512
|
+
this.probing = false;
|
|
19513
|
+
}
|
|
19514
|
+
this.opts.log(formatConfinementReport(report));
|
|
19515
|
+
if (report.ok) {
|
|
19516
|
+
if (this.failing) this.opts.log("confinement holds again; claiming resumes");
|
|
19517
|
+
this.failing = false;
|
|
19518
|
+
return this.schedule(CONFINEMENT_PROBE_INTERVAL_MS);
|
|
19519
|
+
}
|
|
19520
|
+
if (this.opts.mode === "required") {
|
|
19521
|
+
if (!this.failing) this.opts.log("ARCHME_SANDBOX=required: not claiming jobs until confinement holds (re-probing every 5 minutes)");
|
|
19522
|
+
this.failing = true;
|
|
19523
|
+
return this.schedule(CONFINEMENT_RETRY_MS);
|
|
19524
|
+
}
|
|
19525
|
+
this.schedule(CONFINEMENT_PROBE_INTERVAL_MS);
|
|
18948
19526
|
}
|
|
18949
19527
|
};
|
|
18950
|
-
isJobBox = (b) => JOB_LABEL in b.labels;
|
|
18951
|
-
isJobVolume = (v) => v.name.startsWith(JOB_VOLUME_PREFIX) || JOB_LABEL in v.labels;
|
|
18952
19528
|
}
|
|
18953
19529
|
});
|
|
18954
19530
|
|
|
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)}`);
|
|
19531
|
+
// src/daemon/sandbox/claudeToken.ts
|
|
19532
|
+
import { constants as fsConstants } from "node:fs";
|
|
19533
|
+
import { chmod as chmod5, mkdir as mkdir11, open as open3 } from "node:fs/promises";
|
|
19534
|
+
import { dirname as dirname15 } from "node:path";
|
|
19535
|
+
function assertTokenShape(raw, where) {
|
|
19536
|
+
const token = raw.trim();
|
|
19537
|
+
if (!token) throw new SandboxBootError(`sandbox Claude token ${where} is empty`);
|
|
19538
|
+
if (!/^[\x21-\x7e]+$/.test(token)) {
|
|
19539
|
+
throw new SandboxBootError(`sandbox Claude token ${where} contains whitespace or control characters; save it as one line`);
|
|
18965
19540
|
}
|
|
19541
|
+
return token;
|
|
18966
19542
|
}
|
|
18967
|
-
function
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
|
|
18972
|
-
|
|
19543
|
+
async function readClaudeTokenFile(path7) {
|
|
19544
|
+
let fh;
|
|
19545
|
+
try {
|
|
19546
|
+
fh = await open3(path7, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
19547
|
+
} catch (e) {
|
|
19548
|
+
throw new SandboxBootError(`cannot read the sandbox Claude token at ${path7}: ${e.message}`);
|
|
18973
19549
|
}
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
|
|
18978
|
-
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
19550
|
+
try {
|
|
19551
|
+
const st = await fh.stat();
|
|
19552
|
+
if (!st.isFile()) throw new SandboxBootError(`sandbox Claude token ${path7} is not a regular file`);
|
|
19553
|
+
if ((st.mode & 63) !== 0) {
|
|
19554
|
+
throw new SandboxBootError(
|
|
19555
|
+
`sandbox Claude token ${path7} is readable by group or others (mode ${(st.mode & 511).toString(8)}); chmod 600 it`
|
|
19556
|
+
);
|
|
19557
|
+
}
|
|
19558
|
+
return assertTokenShape(await fh.readFile("utf8"), path7);
|
|
19559
|
+
} finally {
|
|
19560
|
+
await fh.close();
|
|
18985
19561
|
}
|
|
18986
19562
|
}
|
|
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}`);
|
|
19563
|
+
async function readSandboxClaudeToken(settings, deps = {}) {
|
|
19564
|
+
if (settings.claudeTokenOverride) {
|
|
19565
|
+
return { token: await readClaudeTokenFile(settings.claudeTokenPath), source: "file-override" };
|
|
18993
19566
|
}
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
19567
|
+
const keytar = await (deps.loadKeytar ?? loadKeytar)();
|
|
19568
|
+
const stored = keytar ? await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT).catch(() => null) : null;
|
|
19569
|
+
if (stored) return { token: assertTokenShape(stored, "in the keychain"), source: "keychain" };
|
|
19570
|
+
try {
|
|
19571
|
+
return { token: await readClaudeTokenFile(settings.claudeTokenPath), source: "file" };
|
|
19572
|
+
} catch (e) {
|
|
19573
|
+
throw new SandboxBootError(
|
|
19574
|
+
`${e.message}; no token in the keychain either \u2014 run \`archme daemon sandbox setup\` to store one`
|
|
19575
|
+
);
|
|
18999
19576
|
}
|
|
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
19577
|
}
|
|
19008
|
-
function
|
|
19009
|
-
|
|
19010
|
-
|
|
19011
|
-
|
|
19012
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19578
|
+
async function storeSandboxClaudeToken(raw, opts) {
|
|
19579
|
+
const token = assertTokenShape(raw, "(entered)");
|
|
19580
|
+
let note;
|
|
19581
|
+
const keytar = opts.override ? null : await (opts.loadKeytar ?? loadKeytar)();
|
|
19582
|
+
if (opts.override) {
|
|
19583
|
+
note = `ARCHME_SANDBOX_CLAUDE_TOKEN_FILE is set: ${opts.path}`;
|
|
19584
|
+
} else if (keytar) {
|
|
19585
|
+
try {
|
|
19586
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, token);
|
|
19587
|
+
return { source: "keychain" };
|
|
19588
|
+
} catch (e) {
|
|
19589
|
+
note = `the keychain refused it (${e.message})`;
|
|
19590
|
+
}
|
|
19591
|
+
} else {
|
|
19592
|
+
note = "no keychain access on this machine";
|
|
19018
19593
|
}
|
|
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
|
-
});
|
|
19594
|
+
await mkdir11(dirname15(opts.path), { recursive: true, mode: 448 });
|
|
19595
|
+
const fh = await open3(
|
|
19596
|
+
opts.path,
|
|
19597
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | fsConstants.O_NOFOLLOW,
|
|
19598
|
+
384
|
|
19599
|
+
);
|
|
19600
|
+
try {
|
|
19601
|
+
await fh.writeFile(`${token}
|
|
19602
|
+
`);
|
|
19603
|
+
} finally {
|
|
19604
|
+
await fh.close();
|
|
19605
|
+
}
|
|
19606
|
+
await chmod5(opts.path, 384);
|
|
19607
|
+
return { source: "file", note };
|
|
19137
19608
|
}
|
|
19138
|
-
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
19145
|
-
|
|
19146
|
-
|
|
19147
|
-
|
|
19148
|
-
|
|
19149
|
-
|
|
19609
|
+
var SandboxBootError, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT;
|
|
19610
|
+
var init_claudeToken = __esm({
|
|
19611
|
+
"src/daemon/sandbox/claudeToken.ts"() {
|
|
19612
|
+
"use strict";
|
|
19613
|
+
init_credentials();
|
|
19614
|
+
SandboxBootError = class extends Error {
|
|
19615
|
+
constructor(message2) {
|
|
19616
|
+
super(message2);
|
|
19617
|
+
this.name = "SandboxBootError";
|
|
19618
|
+
}
|
|
19619
|
+
};
|
|
19620
|
+
KEYCHAIN_SERVICE = "archme-daemon";
|
|
19621
|
+
KEYCHAIN_ACCOUNT = "sandbox-claude-token";
|
|
19622
|
+
}
|
|
19623
|
+
});
|
|
19624
|
+
|
|
19625
|
+
// src/daemon/sandbox/host.ts
|
|
19626
|
+
import { networkInterfaces } from "node:os";
|
|
19627
|
+
function sandboxBootFailurePolicy(mode) {
|
|
19628
|
+
return mode === "shadow" ? "run_unboxed" : "refuse";
|
|
19629
|
+
}
|
|
19630
|
+
function shutdownKillTargets(boxedJobs2, registry) {
|
|
19631
|
+
return [...boxedJobs2].filter((id) => registry.hasLiveBoxes(id));
|
|
19632
|
+
}
|
|
19633
|
+
async function bootSandbox(settings, parts, deps) {
|
|
19634
|
+
const sweep = await parts.runtime.sweep();
|
|
19635
|
+
const inputDirs = await (deps.sweepInputDirs ?? sweepInputDirs)();
|
|
19636
|
+
deps.log(
|
|
19637
|
+
` sandbox: swept ${sweep.boxes.length} box(es), ${sweep.volumes.length} volume(s) in ${sweep.ms}ms, ${inputDirs} input dir(s)`
|
|
19638
|
+
);
|
|
19639
|
+
const slots = await parts.runtime.ensureSlots(settings.slots);
|
|
19640
|
+
const pool = parts.makePool(slots);
|
|
19641
|
+
const { token: claudeToken, source } = await (deps.readClaudeToken ?? ((s) => readSandboxClaudeToken(s)))(settings);
|
|
19642
|
+
deps.log(` sandbox: Claude token from ${source === "keychain" ? "the macOS keychain" : source === "file-override" ? `ARCHME_SANDBOX_CLAUDE_TOKEN_FILE (${settings.claudeTokenPath})` : settings.claudeTokenPath}`);
|
|
19643
|
+
const allSlots = Array.from({ length: MAX_SLOTS }, (_, i2) => slotFor(i2));
|
|
19644
|
+
const gates = await parts.startGates(allSlots, {
|
|
19645
|
+
mcpBaseUrl: deps.mcpBaseUrl,
|
|
19646
|
+
claudeToken,
|
|
19647
|
+
remint: deps.remint,
|
|
19648
|
+
log: deps.log
|
|
19150
19649
|
});
|
|
19650
|
+
let preflight;
|
|
19651
|
+
try {
|
|
19652
|
+
preflight = await parts.runtime.preflight();
|
|
19653
|
+
} catch (e) {
|
|
19654
|
+
await gates.close().catch(() => {
|
|
19655
|
+
});
|
|
19656
|
+
throw e;
|
|
19657
|
+
}
|
|
19658
|
+
if (!preflight.ok) {
|
|
19659
|
+
await gates.close().catch(() => {
|
|
19660
|
+
});
|
|
19661
|
+
throw new SandboxBootError(`sandbox preflight failed: ${failedChecks(preflight)}`);
|
|
19662
|
+
}
|
|
19663
|
+
const probe = deps.probe ?? ((slot) => runConfinementProbe({ runtime: parts.runtime, image: settings.image, log: deps.log }, slot, primaryLanIpv4(networkInterfaces())));
|
|
19664
|
+
let report;
|
|
19665
|
+
try {
|
|
19666
|
+
report = await probe(slots[0]);
|
|
19667
|
+
} catch (e) {
|
|
19668
|
+
report = { ok: false, slot: 0, failures: [`probe threw: ${e.message}`], outcomes: {}, ms: 0 };
|
|
19669
|
+
}
|
|
19670
|
+
preflight = {
|
|
19671
|
+
ok: preflight.ok && report.ok,
|
|
19672
|
+
checks: [...preflight.checks, { name: "confinement_probe", ok: report.ok, detail: report.ok ? void 0 : report.failures.join("; ") }]
|
|
19673
|
+
};
|
|
19674
|
+
deps.log(` sandbox: ${formatConfinementReport(report)}`);
|
|
19675
|
+
if (!report.ok) {
|
|
19676
|
+
if (settings.mode === "required") {
|
|
19677
|
+
await gates.close().catch(() => {
|
|
19678
|
+
});
|
|
19679
|
+
throw new SandboxBootError(`sandbox preflight failed: ${failedChecks(preflight)}`);
|
|
19680
|
+
}
|
|
19681
|
+
deps.log(" sandbox: shadow mode boots anyway; run `archme daemon sandbox setup` to install the confinement");
|
|
19682
|
+
}
|
|
19683
|
+
const monitor = new ConfinementMonitor({ mode: settings.mode, probe, pool, log: deps.log });
|
|
19684
|
+
return {
|
|
19685
|
+
host: { settings, runtime: parts.runtime, registry: parts.registry, gates, pool, io: parts.io },
|
|
19686
|
+
sweep,
|
|
19687
|
+
preflight,
|
|
19688
|
+
monitor
|
|
19689
|
+
};
|
|
19151
19690
|
}
|
|
19152
|
-
function
|
|
19153
|
-
return
|
|
19691
|
+
function failedChecks(p) {
|
|
19692
|
+
return p.checks.filter((c3) => !c3.ok).map((c3) => `${c3.name}${c3.detail ? ` (${c3.detail})` : ""}`).join(", ") || "unknown";
|
|
19154
19693
|
}
|
|
19155
|
-
|
|
19156
|
-
|
|
19694
|
+
var init_host = __esm({
|
|
19695
|
+
"src/daemon/sandbox/host.ts"() {
|
|
19696
|
+
"use strict";
|
|
19697
|
+
init_confinement();
|
|
19698
|
+
init_appleContainer();
|
|
19699
|
+
init_claudeToken();
|
|
19700
|
+
init_inputDir();
|
|
19701
|
+
init_types();
|
|
19702
|
+
init_claudeToken();
|
|
19703
|
+
}
|
|
19704
|
+
});
|
|
19705
|
+
|
|
19706
|
+
// src/daemon/sandbox/runSandboxedJob.ts
|
|
19707
|
+
function sandboxJobOf(job) {
|
|
19708
|
+
return {
|
|
19709
|
+
id: job.id,
|
|
19710
|
+
project_id: job.project_id,
|
|
19711
|
+
repo_owner: job.repo_owner,
|
|
19712
|
+
repo_name: job.repo_name,
|
|
19713
|
+
repo_branch: job.repo_branch,
|
|
19714
|
+
archme_branch: job.archme_branch,
|
|
19715
|
+
before_sha: job.before_sha,
|
|
19716
|
+
after_sha: job.after_sha,
|
|
19717
|
+
forced: job.forced,
|
|
19718
|
+
pr_number: job.pr_number ?? null,
|
|
19719
|
+
source_mode: job.source_mode,
|
|
19720
|
+
commit_message: job.commit_message ?? null,
|
|
19721
|
+
head_commit_timestamp: job.head_commit_timestamp ?? null,
|
|
19722
|
+
author_name: job.author_name ?? null,
|
|
19723
|
+
author_login: job.author_login ?? null
|
|
19724
|
+
};
|
|
19157
19725
|
}
|
|
19158
|
-
function
|
|
19159
|
-
|
|
19160
|
-
|
|
19161
|
-
|
|
19162
|
-
|
|
19163
|
-
|
|
19164
|
-
|
|
19165
|
-
|
|
19166
|
-
|
|
19726
|
+
async function runSandboxedJob(deps, job, opts) {
|
|
19727
|
+
const { slot } = opts;
|
|
19728
|
+
const t = { ...DEFAULT_SANDBOX_TIMEOUTS, ...opts.timeouts };
|
|
19729
|
+
const pollMs = opts.pollMs ?? 1e3;
|
|
19730
|
+
const now = deps.now ?? Date.now;
|
|
19731
|
+
const { settings } = deps;
|
|
19732
|
+
const volume = `archme-job-${job.id}`;
|
|
19733
|
+
const labels = { [JOB_LABEL]: job.id, [SLOT_LABEL]: String(slot.index) };
|
|
19734
|
+
const sjob = sandboxJobOf(job);
|
|
19735
|
+
const sandbox = {
|
|
19736
|
+
slot: slot.index,
|
|
19737
|
+
memory_peak_bytes: { clone: null, agent: null },
|
|
19738
|
+
model_requests: 0,
|
|
19739
|
+
upstream_429: 0,
|
|
19740
|
+
last_upstream_status: null,
|
|
19741
|
+
cap_hits: 0,
|
|
19742
|
+
mcp_requests: 0,
|
|
19743
|
+
remints: 0,
|
|
19744
|
+
egress_denied: 0,
|
|
19745
|
+
first_egress_denied: null,
|
|
19746
|
+
clone_ms: null,
|
|
19747
|
+
box_ms: 0
|
|
19748
|
+
};
|
|
19749
|
+
const durations = {};
|
|
19750
|
+
const metrics = { sandbox, durations_ms: durations };
|
|
19751
|
+
const liveBoxes = /* @__PURE__ */ new Set();
|
|
19752
|
+
const inputCleanups = [];
|
|
19753
|
+
let volumeTouched = false;
|
|
19754
|
+
let agentStarted = false;
|
|
19755
|
+
const readCounters = () => {
|
|
19756
|
+
const c3 = deps.gates.countersFor(slot);
|
|
19757
|
+
if (!c3) return;
|
|
19758
|
+
sandbox.model_requests = c3.model_requests;
|
|
19759
|
+
sandbox.upstream_429 = c3.upstream_429;
|
|
19760
|
+
sandbox.last_upstream_status = c3.last_upstream_status;
|
|
19761
|
+
sandbox.cap_hits = c3.cap_hits;
|
|
19762
|
+
sandbox.mcp_requests = c3.mcp_requests;
|
|
19763
|
+
sandbox.remints = c3.remints;
|
|
19764
|
+
sandbox.egress_denied = c3.egress_denied;
|
|
19765
|
+
sandbox.first_egress_denied = c3.first_egress_denied;
|
|
19766
|
+
};
|
|
19767
|
+
const failed = (step, reason, stderr = "") => {
|
|
19768
|
+
deps.log(`sandbox ${step} step failed: ${reason}`);
|
|
19769
|
+
return { kind: "step_failed", step, reason, stderr, metrics };
|
|
19770
|
+
};
|
|
19771
|
+
const currentKillCause = () => {
|
|
19772
|
+
if (deps.isCancelled(job.id)) return "cancel";
|
|
19773
|
+
if (deps.killReason(job.id) === "shutdown") return "shutdown";
|
|
19774
|
+
if (deps.isLeaseLost(job.id)) return "lease_lost";
|
|
19775
|
+
return null;
|
|
19776
|
+
};
|
|
19777
|
+
const writeInput = async (step, input) => {
|
|
19778
|
+
const w = await deps.writeInputDir(step, input);
|
|
19779
|
+
let done = false;
|
|
19780
|
+
const cleanup = async () => {
|
|
19781
|
+
if (done) return;
|
|
19782
|
+
done = true;
|
|
19783
|
+
await w.cleanup().catch(() => {
|
|
19784
|
+
});
|
|
19167
19785
|
};
|
|
19786
|
+
inputCleanups.push(cleanup);
|
|
19787
|
+
return { dir: w.dir, cleanup };
|
|
19168
19788
|
};
|
|
19169
|
-
|
|
19170
|
-
|
|
19171
|
-
|
|
19172
|
-
|
|
19789
|
+
const runBox = async (spec2, timeoutMs) => {
|
|
19790
|
+
if (deps.isCancelled(job.id)) return { kind: "refused", reason: "cancel" };
|
|
19791
|
+
if (!await deps.registry.beginStep(job.id, spec2.name)) return { kind: "refused", reason: "shutting_down" };
|
|
19792
|
+
liveBoxes.add(spec2.name);
|
|
19793
|
+
const started = now();
|
|
19794
|
+
let killCause = null;
|
|
19795
|
+
const kill = (cause) => {
|
|
19796
|
+
if (killCause) return;
|
|
19797
|
+
killCause = cause;
|
|
19798
|
+
deps.log(`killing ${spec2.name} (${cause})`);
|
|
19799
|
+
if (cause === "lease_lost") deps.gates.clearJob(slot);
|
|
19800
|
+
void deps.runtime.kill(spec2.name).catch((e) => deps.log(`kill ${spec2.name} failed: ${e.message}`));
|
|
19801
|
+
};
|
|
19802
|
+
if (spec2.args[0] === "agent") agentStarted = true;
|
|
19803
|
+
const handle = await deps.runtime.runStep(spec2);
|
|
19804
|
+
const timer = setTimeout(() => kill("timeout"), timeoutMs);
|
|
19805
|
+
const poll = setInterval(() => {
|
|
19806
|
+
const c3 = currentKillCause();
|
|
19807
|
+
if (c3) kill(c3);
|
|
19808
|
+
}, pollMs);
|
|
19809
|
+
try {
|
|
19810
|
+
const exit = await handle.done;
|
|
19811
|
+
liveBoxes.delete(spec2.name);
|
|
19812
|
+
deps.registry.endStep(job.id, spec2.name);
|
|
19813
|
+
if (!killCause && exit.killed) killCause = currentKillCause() ?? "shutdown";
|
|
19814
|
+
return { kind: "ran", exit, killCause, ms: now() - started };
|
|
19815
|
+
} finally {
|
|
19816
|
+
clearTimeout(timer);
|
|
19817
|
+
clearInterval(poll);
|
|
19818
|
+
}
|
|
19819
|
+
};
|
|
19820
|
+
const spec = (step, env, inputDir) => ({
|
|
19821
|
+
name: `archme-job-${job.id}-${step}`,
|
|
19822
|
+
image: settings.image,
|
|
19823
|
+
args: [step],
|
|
19824
|
+
network: slot,
|
|
19825
|
+
env,
|
|
19826
|
+
inputDir,
|
|
19827
|
+
volume,
|
|
19828
|
+
limits: { cpus: settings.boxCpus, memoryMiB: settings.boxMemoryMiB },
|
|
19829
|
+
labels: { ...labels }
|
|
19830
|
+
});
|
|
19831
|
+
const runBoxes = async (pre) => {
|
|
19832
|
+
deps.registry.setVolume(job.id, volume);
|
|
19833
|
+
volumeTouched = true;
|
|
19834
|
+
await deps.runtime.createVolume(volume, settings.volumeGiB, labels);
|
|
19835
|
+
await deps.runtime.initVolume(volume, BOX_UID, labels, settings.image, slot);
|
|
19836
|
+
deps.gates.bindJob(slot, {
|
|
19837
|
+
jobId: job.id,
|
|
19838
|
+
maxModelRequests: settings.maxModelRequests,
|
|
19839
|
+
mcp: {
|
|
19840
|
+
token: pre.jobToken.token,
|
|
19841
|
+
expiresAt: pre.jobToken.expires_at ? Date.parse(pre.jobToken.expires_at) || null : null,
|
|
19842
|
+
projectSlug: pre.projectSlug,
|
|
19843
|
+
branch: job.archme_branch
|
|
19844
|
+
}
|
|
19845
|
+
});
|
|
19846
|
+
const cloneIn = await writeInput("clone", {
|
|
19847
|
+
kind: "clone",
|
|
19848
|
+
job: sjob,
|
|
19849
|
+
marker: pre.marker,
|
|
19850
|
+
credentials: {
|
|
19851
|
+
https_url: pre.credentials.https_url,
|
|
19852
|
+
username: pre.credentials.username,
|
|
19853
|
+
password: pre.credentials.password
|
|
19854
|
+
}
|
|
19855
|
+
});
|
|
19856
|
+
deps.gates.setStep(slot, "clone");
|
|
19857
|
+
const clone = await runBox(spec("clone", deps.cloneBoxEnv(slot.gateway), cloneIn.dir), t.cloneMs);
|
|
19858
|
+
await cloneIn.cleanup();
|
|
19859
|
+
if (clone.kind === "refused") return failed("clone", clone.reason);
|
|
19860
|
+
sandbox.clone_ms = clone.ms;
|
|
19861
|
+
sandbox.box_ms += clone.ms;
|
|
19862
|
+
durations.sandbox_clone = clone.ms;
|
|
19863
|
+
if (clone.killCause) return failed("clone", clone.killCause, clone.exit.stderr);
|
|
19864
|
+
const cp = deps.parseResultLine(clone.exit.stdout, clone.exit.nonce);
|
|
19865
|
+
if (!cp.ok || cp.result.kind !== "clone") {
|
|
19866
|
+
deps.log(`clone result line: ${cp.ok ? `unexpected kind ${cp.result.kind}` : cp.reason}`);
|
|
19867
|
+
return failed("clone", "result_malformed", clone.exit.stderr);
|
|
19868
|
+
}
|
|
19869
|
+
const cr = cp.result;
|
|
19870
|
+
sandbox.memory_peak_bytes.clone = cr.memoryPeakBytes;
|
|
19871
|
+
for (const [k, v] of Object.entries(cr.durations_ms ?? {})) durations[`clone_${k}`] = v;
|
|
19872
|
+
if (cr.outcome === "skip") return { kind: "skip", metrics };
|
|
19873
|
+
if (cr.outcome === "rejected") return { kind: "rejected", message: cr.error ?? "job rejected by the clone step", metrics };
|
|
19874
|
+
if (cr.error || clone.exit.exitCode !== 0 || !cr.diff) {
|
|
19875
|
+
return failed("clone", cr.error ?? `clone step exited ${clone.exit.exitCode}`, clone.exit.stderr);
|
|
19876
|
+
}
|
|
19877
|
+
Object.assign(metrics, {
|
|
19878
|
+
mode: job.source_mode === "pr_update" ? "pr_update" : "diff",
|
|
19879
|
+
files: cr.diff.files,
|
|
19880
|
+
bytes: cr.diff.bytes,
|
|
19881
|
+
materialized: cr.diff.materialized,
|
|
19882
|
+
range: cr.diff.rangeLabel,
|
|
19883
|
+
model: pre.model
|
|
19884
|
+
});
|
|
19885
|
+
const claudeMs = cr.diff.materialized ? t.largeDiffClaudeMs : t.claudeMs;
|
|
19886
|
+
const agentIn = await writeInput("agent", {
|
|
19887
|
+
kind: "agent",
|
|
19888
|
+
job: sjob,
|
|
19889
|
+
projectSlug: pre.projectSlug,
|
|
19890
|
+
seedArch: pre.seedArch,
|
|
19891
|
+
repoScope: pre.repoScope,
|
|
19892
|
+
repoContainer: pre.repoContainer,
|
|
19893
|
+
bugsFolderId: pre.bugsFolderId,
|
|
19894
|
+
model: pre.model,
|
|
19895
|
+
timeoutMs: claudeMs,
|
|
19896
|
+
gateway: slot.gateway
|
|
19897
|
+
});
|
|
19898
|
+
deps.gates.setStep(slot, "agent");
|
|
19899
|
+
const agent = await runBox(spec("agent", deps.agentBoxEnv(slot.gateway), agentIn.dir), claudeMs + t.graceMs);
|
|
19900
|
+
await agentIn.cleanup();
|
|
19901
|
+
if (agent.kind === "refused") return failed("agent", agent.reason);
|
|
19902
|
+
sandbox.box_ms += agent.ms;
|
|
19903
|
+
durations.sandbox_agent = agent.ms;
|
|
19904
|
+
readCounters();
|
|
19905
|
+
if (agent.killCause) return failed("agent", agent.killCause, agent.exit.stderr);
|
|
19906
|
+
const ap = deps.parseResultLine(agent.exit.stdout, agent.exit.nonce);
|
|
19907
|
+
if (!ap.ok || ap.result.kind !== "agent") {
|
|
19908
|
+
deps.log(`agent result line: ${ap.ok ? `unexpected kind ${ap.result.kind}` : ap.reason}`);
|
|
19909
|
+
return failed("agent", "result_malformed", agent.exit.stderr);
|
|
19910
|
+
}
|
|
19911
|
+
const ar = ap.result;
|
|
19912
|
+
sandbox.memory_peak_bytes.agent = ar.memoryPeakBytes;
|
|
19913
|
+
for (const [k, v] of Object.entries(ar.durations_ms ?? {})) durations[`agent_${k}`] = v;
|
|
19914
|
+
if (ar.seed) metrics.seed = ar.seed;
|
|
19915
|
+
if (ar.error) return failed("agent", ar.error, agent.exit.stderr);
|
|
19173
19916
|
return {
|
|
19174
|
-
|
|
19175
|
-
|
|
19176
|
-
|
|
19177
|
-
|
|
19178
|
-
|
|
19179
|
-
|
|
19180
|
-
|
|
19181
|
-
}))
|
|
19917
|
+
kind: "claude",
|
|
19918
|
+
exitCode: ar.exitCode,
|
|
19919
|
+
signal: ar.signal,
|
|
19920
|
+
claudeStdout: ar.claudeStdout,
|
|
19921
|
+
stderr: agent.exit.stderr,
|
|
19922
|
+
affected: ar.affected,
|
|
19923
|
+
metrics
|
|
19182
19924
|
};
|
|
19183
19925
|
};
|
|
19926
|
+
try {
|
|
19927
|
+
const pre = await deps.prefetch(job);
|
|
19928
|
+
try {
|
|
19929
|
+
return await runBoxes(pre);
|
|
19930
|
+
} catch (e) {
|
|
19931
|
+
if (e instanceof SandboxInfraError) throw e;
|
|
19932
|
+
throw new SandboxInfraError(e instanceof Error ? e.message : String(e), agentStarted);
|
|
19933
|
+
}
|
|
19934
|
+
} finally {
|
|
19935
|
+
const step = async (what, fn) => {
|
|
19936
|
+
try {
|
|
19937
|
+
await fn();
|
|
19938
|
+
} catch (e) {
|
|
19939
|
+
deps.log(`sandbox cleanup (${what}) failed: ${e.message}`);
|
|
19940
|
+
}
|
|
19941
|
+
};
|
|
19942
|
+
for (const name of liveBoxes) await step(`kill ${name}`, () => deps.runtime.kill(name));
|
|
19943
|
+
if (volumeTouched) await step("remove volume", () => deps.runtime.removeVolume(volume));
|
|
19944
|
+
await step("counters", readCounters);
|
|
19945
|
+
await step("clear gates", () => deps.gates.clearJob(slot));
|
|
19946
|
+
await step("free slot", () => deps.pool.free(slot.index));
|
|
19947
|
+
for (const c3 of inputCleanups) await step("input dir", c3);
|
|
19948
|
+
await step("release", () => deps.registry.release(job.id));
|
|
19949
|
+
}
|
|
19184
19950
|
}
|
|
19185
|
-
var
|
|
19186
|
-
var
|
|
19187
|
-
"src/daemon/sandbox/
|
|
19951
|
+
var DEFAULT_SANDBOX_TIMEOUTS, SandboxInfraError;
|
|
19952
|
+
var init_runSandboxedJob = __esm({
|
|
19953
|
+
"src/daemon/sandbox/runSandboxedJob.ts"() {
|
|
19188
19954
|
"use strict";
|
|
19189
|
-
init_execa();
|
|
19190
|
-
init_sweep();
|
|
19191
19955
|
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);
|
|
19956
|
+
DEFAULT_SANDBOX_TIMEOUTS = {
|
|
19957
|
+
claudeMs: 30 * 60 * 1e3,
|
|
19958
|
+
largeDiffClaudeMs: 45 * 60 * 1e3,
|
|
19959
|
+
graceMs: 2 * 60 * 1e3,
|
|
19960
|
+
cloneMs: 10 * 60 * 1e3
|
|
19961
|
+
};
|
|
19962
|
+
SandboxInfraError = class extends Error {
|
|
19963
|
+
constructor(message2, agentStarted) {
|
|
19964
|
+
super(message2);
|
|
19965
|
+
this.agentStarted = agentStarted;
|
|
19966
|
+
this.name = "SandboxInfraError";
|
|
19401
19967
|
}
|
|
19402
|
-
|
|
19403
|
-
|
|
19968
|
+
};
|
|
19969
|
+
}
|
|
19970
|
+
});
|
|
19971
|
+
|
|
19972
|
+
// src/daemon/sandbox/classifySandboxExit.ts
|
|
19973
|
+
function classifySandboxExit(input) {
|
|
19974
|
+
const { counters, exitCode, stderr, stepFailure } = input;
|
|
19975
|
+
if ((counters?.cap_hits ?? 0) > 0 || stderr.includes(SANDBOX_CAP_BODY)) {
|
|
19976
|
+
return {
|
|
19977
|
+
kind: "terminal",
|
|
19978
|
+
reason: SANDBOX_REQUEST_CAP_REASON,
|
|
19979
|
+
message: `${SANDBOX_REQUEST_CAP_REASON}: the job exceeded the sandbox model-request cap` + (counters ? ` (${counters.model_requests} requests, ${counters.cap_hits} refused)` : "")
|
|
19980
|
+
};
|
|
19981
|
+
}
|
|
19982
|
+
if (stepFailure == null && exitCode !== null && exitCode !== 0 && counters?.last_upstream_status === 429) {
|
|
19983
|
+
return { kind: "upstream_rate_limit" };
|
|
19984
|
+
}
|
|
19985
|
+
return { kind: "normal" };
|
|
19986
|
+
}
|
|
19987
|
+
function routeSandboxFailure(verdict, planFor, nowMs = Date.now()) {
|
|
19988
|
+
if (verdict?.kind === "terminal") return { kind: "terminal", message: verdict.message };
|
|
19989
|
+
const plan = planFor();
|
|
19990
|
+
if (plan) return { kind: "defer", plan };
|
|
19991
|
+
if (verdict?.kind === "upstream_rate_limit") {
|
|
19992
|
+
return {
|
|
19993
|
+
kind: "defer",
|
|
19994
|
+
plan: { parsed: false, pauseUntil: new Date(nowMs + RATE_LIMIT_TENTATIVE_PAUSE_MS), info: null }
|
|
19995
|
+
};
|
|
19996
|
+
}
|
|
19997
|
+
return { kind: "fail" };
|
|
19998
|
+
}
|
|
19999
|
+
var SANDBOX_REQUEST_CAP_REASON, SandboxExitError;
|
|
20000
|
+
var init_classifySandboxExit = __esm({
|
|
20001
|
+
"src/daemon/sandbox/classifySandboxExit.ts"() {
|
|
20002
|
+
"use strict";
|
|
20003
|
+
init_rateLimit();
|
|
20004
|
+
init_types();
|
|
20005
|
+
SANDBOX_REQUEST_CAP_REASON = "sandbox_request_cap";
|
|
20006
|
+
SandboxExitError = class extends Error {
|
|
20007
|
+
constructor(message2, verdict) {
|
|
20008
|
+
super(message2);
|
|
20009
|
+
this.verdict = verdict;
|
|
20010
|
+
this.name = "SandboxExitError";
|
|
19404
20011
|
}
|
|
19405
20012
|
};
|
|
19406
20013
|
}
|
|
@@ -19540,6 +20147,47 @@ function createGateServer(opts) {
|
|
|
19540
20147
|
fail2(e);
|
|
19541
20148
|
}
|
|
19542
20149
|
});
|
|
20150
|
+
if (opts.connect) {
|
|
20151
|
+
const connect = opts.connect;
|
|
20152
|
+
server.on("connect", (req, socket, head) => {
|
|
20153
|
+
const target = (req.url ?? "").slice(0, 300);
|
|
20154
|
+
const slot = opts.slotMap.resolve(req.socket.localAddress, req.socket.remoteAddress);
|
|
20155
|
+
socket.on("error", () => {
|
|
20156
|
+
});
|
|
20157
|
+
if (!slot) {
|
|
20158
|
+
log(`[${opts.name}] CONNECT ${target} drop job=-`);
|
|
20159
|
+
socket.destroy();
|
|
20160
|
+
return;
|
|
20161
|
+
}
|
|
20162
|
+
const binding = opts.slotMap.binding(slot.index);
|
|
20163
|
+
if (!binding) {
|
|
20164
|
+
log(`[${opts.name}] CONNECT ${target} 403 job=- slot=${slot.index} idle`);
|
|
20165
|
+
socket.end("HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n");
|
|
20166
|
+
return;
|
|
20167
|
+
}
|
|
20168
|
+
let logged = false;
|
|
20169
|
+
const ctx = {
|
|
20170
|
+
slot,
|
|
20171
|
+
binding,
|
|
20172
|
+
path: target,
|
|
20173
|
+
done(status, note) {
|
|
20174
|
+
if (logged) return;
|
|
20175
|
+
logged = true;
|
|
20176
|
+
log(`[${opts.name}] CONNECT ${target} ${status} job=${binding.jobId}${note ? ` ${note}` : ""}`);
|
|
20177
|
+
}
|
|
20178
|
+
};
|
|
20179
|
+
const fail2 = (e) => {
|
|
20180
|
+
ctx.done(502, `handler error: ${e?.name ?? "error"}`);
|
|
20181
|
+
if (socket.writable) socket.end("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n");
|
|
20182
|
+
else socket.destroy();
|
|
20183
|
+
};
|
|
20184
|
+
try {
|
|
20185
|
+
Promise.resolve(connect(req, socket, head, ctx)).catch(fail2);
|
|
20186
|
+
} catch (e) {
|
|
20187
|
+
fail2(e);
|
|
20188
|
+
}
|
|
20189
|
+
});
|
|
20190
|
+
}
|
|
19543
20191
|
server.requestTimeout = 0;
|
|
19544
20192
|
server.headersTimeout = 6e4;
|
|
19545
20193
|
return {
|
|
@@ -19706,12 +20354,121 @@ var init_credentialGate = __esm({
|
|
|
19706
20354
|
}
|
|
19707
20355
|
});
|
|
19708
20356
|
|
|
20357
|
+
// src/daemon/sandbox/githubGate.ts
|
|
20358
|
+
import net from "node:net";
|
|
20359
|
+
function parseConnectTarget(authority) {
|
|
20360
|
+
const i2 = authority.lastIndexOf(":");
|
|
20361
|
+
if (i2 <= 0) return { ok: false };
|
|
20362
|
+
const portStr = authority.slice(i2 + 1);
|
|
20363
|
+
let host = authority.slice(0, i2).toLowerCase();
|
|
20364
|
+
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
20365
|
+
if (host.endsWith(".")) host = host.slice(0, -1);
|
|
20366
|
+
if (portStr !== "443" || net.isIP(host) !== 0 || !GITHUB_ALLOWED_HOSTS.has(host)) return { ok: false };
|
|
20367
|
+
return { ok: true, host, port: 443 };
|
|
20368
|
+
}
|
|
20369
|
+
var GITHUB_ALLOWED_HOSTS, FORBIDDEN, BAD_GATEWAY, GithubGate;
|
|
20370
|
+
var init_githubGate = __esm({
|
|
20371
|
+
"src/daemon/sandbox/githubGate.ts"() {
|
|
20372
|
+
"use strict";
|
|
20373
|
+
init_gateServer();
|
|
20374
|
+
init_types();
|
|
20375
|
+
GITHUB_ALLOWED_HOSTS = /* @__PURE__ */ new Set(["github.com", "codeload.github.com"]);
|
|
20376
|
+
FORBIDDEN = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n";
|
|
20377
|
+
BAD_GATEWAY = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n";
|
|
20378
|
+
GithubGate = class {
|
|
20379
|
+
gate;
|
|
20380
|
+
dial;
|
|
20381
|
+
/** Open tunnels per slot: a clone-step tunnel must not outlive the clone step. */
|
|
20382
|
+
tunnels = /* @__PURE__ */ new Map();
|
|
20383
|
+
constructor(opts) {
|
|
20384
|
+
this.dial = opts.dial ?? ((host, port) => net.connect({ host, port }));
|
|
20385
|
+
this.gate = createGateServer({
|
|
20386
|
+
name: "github-gate",
|
|
20387
|
+
slotMap: opts.slotMap,
|
|
20388
|
+
host: opts.host,
|
|
20389
|
+
port: opts.port ?? GATE_PORTS.github,
|
|
20390
|
+
logger: opts.logger,
|
|
20391
|
+
handler: (_req, res, ctx) => {
|
|
20392
|
+
ctx.done(404);
|
|
20393
|
+
res.writeHead(404).end();
|
|
20394
|
+
},
|
|
20395
|
+
connect: (req, socket, head, ctx) => this.tunnel(req, socket, head, ctx)
|
|
20396
|
+
});
|
|
20397
|
+
}
|
|
20398
|
+
listen() {
|
|
20399
|
+
return this.gate.listen();
|
|
20400
|
+
}
|
|
20401
|
+
async close() {
|
|
20402
|
+
for (const index of [...this.tunnels.keys()]) this.closeTunnels(index);
|
|
20403
|
+
await this.gate.close();
|
|
20404
|
+
}
|
|
20405
|
+
/** The slot's step moved (clone → agent): its tunnels go. */
|
|
20406
|
+
onStepChange(index) {
|
|
20407
|
+
this.closeTunnels(index);
|
|
20408
|
+
}
|
|
20409
|
+
onClear(index) {
|
|
20410
|
+
this.closeTunnels(index);
|
|
20411
|
+
}
|
|
20412
|
+
closeTunnels(index) {
|
|
20413
|
+
const set = this.tunnels.get(index);
|
|
20414
|
+
if (!set) return;
|
|
20415
|
+
this.tunnels.delete(index);
|
|
20416
|
+
for (const destroy of set) destroy();
|
|
20417
|
+
}
|
|
20418
|
+
tunnel(_req, socket, head, ctx) {
|
|
20419
|
+
const { counters } = ctx.binding;
|
|
20420
|
+
const refuse = (note) => {
|
|
20421
|
+
counters.egress_denied++;
|
|
20422
|
+
counters.first_egress_denied ??= ctx.path;
|
|
20423
|
+
ctx.done(403, note);
|
|
20424
|
+
socket.end(FORBIDDEN);
|
|
20425
|
+
};
|
|
20426
|
+
if (ctx.binding.step !== "clone") return refuse(`step=${ctx.binding.step ?? "none"}`);
|
|
20427
|
+
const target = parseConnectTarget(ctx.path);
|
|
20428
|
+
if (!target.ok) return refuse("not allowed");
|
|
20429
|
+
const upstream = this.dial(target.host, target.port);
|
|
20430
|
+
let set = this.tunnels.get(ctx.slot.index);
|
|
20431
|
+
if (!set) this.tunnels.set(ctx.slot.index, set = /* @__PURE__ */ new Set());
|
|
20432
|
+
const destroy = () => {
|
|
20433
|
+
upstream.destroy();
|
|
20434
|
+
socket.destroy();
|
|
20435
|
+
};
|
|
20436
|
+
set.add(destroy);
|
|
20437
|
+
let open4 = false;
|
|
20438
|
+
const cleanup = () => {
|
|
20439
|
+
set.delete(destroy);
|
|
20440
|
+
destroy();
|
|
20441
|
+
};
|
|
20442
|
+
upstream.once("connect", () => {
|
|
20443
|
+
open4 = true;
|
|
20444
|
+
ctx.done(200);
|
|
20445
|
+
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
20446
|
+
if (head.length) upstream.write(head);
|
|
20447
|
+
upstream.pipe(socket);
|
|
20448
|
+
socket.pipe(upstream);
|
|
20449
|
+
});
|
|
20450
|
+
upstream.on("error", () => {
|
|
20451
|
+
if (open4) return;
|
|
20452
|
+
ctx.done(502, "dial failed");
|
|
20453
|
+
set.delete(destroy);
|
|
20454
|
+
socket.end(BAD_GATEWAY);
|
|
20455
|
+
});
|
|
20456
|
+
upstream.on("close", () => {
|
|
20457
|
+
if (open4) cleanup();
|
|
20458
|
+
});
|
|
20459
|
+
socket.on("close", cleanup);
|
|
20460
|
+
socket.on("error", cleanup);
|
|
20461
|
+
}
|
|
20462
|
+
};
|
|
20463
|
+
}
|
|
20464
|
+
});
|
|
20465
|
+
|
|
19709
20466
|
// src/daemon/sandbox/mcpGate.ts
|
|
19710
20467
|
function redact(err4) {
|
|
19711
20468
|
const msg = err4 instanceof Error ? err4.message : "error";
|
|
19712
20469
|
return msg.replace(/\b(amj_|am_|sk-ant-|eyJ)[A-Za-z0-9._-]*/g, "[redacted]").slice(0, 200);
|
|
19713
20470
|
}
|
|
19714
|
-
var MCP_PATH, REMINT_LEAD_MS, MAX_TIMER_MS,
|
|
20471
|
+
var MCP_PATH, REMINT_LEAD_MS, MAX_TIMER_MS, realTimers2, McpGate;
|
|
19715
20472
|
var init_mcpGate = __esm({
|
|
19716
20473
|
"src/daemon/sandbox/mcpGate.ts"() {
|
|
19717
20474
|
"use strict";
|
|
@@ -19720,7 +20477,7 @@ var init_mcpGate = __esm({
|
|
|
19720
20477
|
MCP_PATH = "/functions/v1/mcp";
|
|
19721
20478
|
REMINT_LEAD_MS = 10 * 60 * 1e3;
|
|
19722
20479
|
MAX_TIMER_MS = 2 ** 31 - 1;
|
|
19723
|
-
|
|
20480
|
+
realTimers2 = {
|
|
19724
20481
|
now: () => Date.now(),
|
|
19725
20482
|
setTimeout: (fn, ms) => {
|
|
19726
20483
|
const t = setTimeout(fn, ms);
|
|
@@ -19733,7 +20490,7 @@ var init_mcpGate = __esm({
|
|
|
19733
20490
|
constructor(opts) {
|
|
19734
20491
|
this.opts = opts;
|
|
19735
20492
|
this.upstream = new URL(opts.upstream);
|
|
19736
|
-
this.timers = opts.timers ??
|
|
20493
|
+
this.timers = opts.timers ?? realTimers2;
|
|
19737
20494
|
this.log = opts.logger ?? ((line) => console.log(line));
|
|
19738
20495
|
this.gate = createGateServer({
|
|
19739
20496
|
name: "mcp-gate",
|
|
@@ -19775,177 +20532,80 @@ var init_mcpGate = __esm({
|
|
|
19775
20532
|
cancel(index) {
|
|
19776
20533
|
const s = this.schedules.get(index);
|
|
19777
20534
|
if (s) {
|
|
19778
|
-
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;
|
|
20535
|
+
this.timers.clearTimeout(s.handle);
|
|
20536
|
+
this.schedules.delete(index);
|
|
20537
|
+
}
|
|
20538
|
+
this.generation++;
|
|
19935
20539
|
}
|
|
19936
|
-
|
|
19937
|
-
|
|
20540
|
+
schedule(index, jobId, expiresAt) {
|
|
20541
|
+
const generation = ++this.generation;
|
|
20542
|
+
const arm = (fireAt) => {
|
|
20543
|
+
const delay = Math.max(0, fireAt - this.timers.now());
|
|
20544
|
+
const handle = this.timers.setTimeout(() => {
|
|
20545
|
+
if (this.schedules.get(index)?.generation !== generation) return;
|
|
20546
|
+
if (fireAt - this.timers.now() > 0) arm(fireAt);
|
|
20547
|
+
else void this.remint(index, jobId, generation);
|
|
20548
|
+
}, Math.min(delay, MAX_TIMER_MS));
|
|
20549
|
+
this.schedules.set(index, { jobId, handle, generation });
|
|
20550
|
+
};
|
|
20551
|
+
arm(expiresAt - REMINT_LEAD_MS);
|
|
19938
20552
|
}
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
|
|
20553
|
+
async remint(index, jobId, generation) {
|
|
20554
|
+
let next;
|
|
20555
|
+
try {
|
|
20556
|
+
next = await this.opts.remint(jobId);
|
|
20557
|
+
} catch (err4) {
|
|
20558
|
+
if (this.schedules.get(index)?.generation === generation) this.schedules.delete(index);
|
|
20559
|
+
this.log(`[mcp-gate] remint failed job=${jobId}: ${redact(err4)}`);
|
|
20560
|
+
return;
|
|
19942
20561
|
}
|
|
19943
|
-
|
|
20562
|
+
const binding = this.opts.slotMap.binding(index);
|
|
20563
|
+
if (this.schedules.get(index)?.generation !== generation || binding?.jobId !== jobId || !binding.mcp) {
|
|
20564
|
+
this.log(`[mcp-gate] remint discarded job=${jobId}: slot moved on`);
|
|
20565
|
+
return;
|
|
20566
|
+
}
|
|
20567
|
+
binding.mcp = { ...binding.mcp, token: next.token, expiresAt: next.expiresAt };
|
|
20568
|
+
binding.counters.remints++;
|
|
20569
|
+
this.schedules.delete(index);
|
|
20570
|
+
this.log(`[mcp-gate] remint ok job=${jobId}`);
|
|
20571
|
+
if (next.expiresAt === null) return;
|
|
20572
|
+
if (next.expiresAt - REMINT_LEAD_MS <= this.timers.now()) {
|
|
20573
|
+
this.log(`[mcp-gate] reminted token expires inside the lead window job=${jobId}; not rescheduling`);
|
|
20574
|
+
return;
|
|
20575
|
+
}
|
|
20576
|
+
this.schedule(index, jobId, next.expiresAt);
|
|
19944
20577
|
}
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
|
|
19948
|
-
|
|
20578
|
+
handle(req, res, ctx) {
|
|
20579
|
+
if (ctx.path !== MCP_PATH) {
|
|
20580
|
+
ctx.done(404);
|
|
20581
|
+
res.writeHead(404).end();
|
|
20582
|
+
return;
|
|
20583
|
+
}
|
|
20584
|
+
if (req.method === "GET") {
|
|
20585
|
+
ctx.done(405);
|
|
20586
|
+
res.writeHead(405, { allow: "POST" }).end();
|
|
20587
|
+
return;
|
|
20588
|
+
}
|
|
20589
|
+
if (req.method !== "POST") {
|
|
20590
|
+
ctx.done(404);
|
|
20591
|
+
res.writeHead(404).end();
|
|
20592
|
+
return;
|
|
20593
|
+
}
|
|
20594
|
+
const mcp = ctx.binding.mcp;
|
|
20595
|
+
if (!mcp) {
|
|
20596
|
+
ctx.done(403, "no-mcp");
|
|
20597
|
+
res.writeHead(403).end();
|
|
20598
|
+
return;
|
|
20599
|
+
}
|
|
20600
|
+
ctx.binding.counters.mcp_requests++;
|
|
20601
|
+
const headers = filterHeaders(
|
|
20602
|
+
req.headers,
|
|
20603
|
+
(k) => k === "authorization" || k === "apikey" || k.startsWith("x-archme-source-")
|
|
20604
|
+
);
|
|
20605
|
+
headers.authorization = `Bearer ${mcp.token}`;
|
|
20606
|
+
const target = new URL(this.upstream.href);
|
|
20607
|
+
target.search = new URLSearchParams({ project: mcp.projectSlug, branch: mcp.branch }).toString();
|
|
20608
|
+
void forward(req, res, target, headers).then((r) => ctx.done(r.status));
|
|
19949
20609
|
}
|
|
19950
20610
|
};
|
|
19951
20611
|
}
|
|
@@ -19953,7 +20613,16 @@ var init_slotMap = __esm({
|
|
|
19953
20613
|
|
|
19954
20614
|
// src/daemon/sandbox/slots.ts
|
|
19955
20615
|
function zeroCounters() {
|
|
19956
|
-
return {
|
|
20616
|
+
return {
|
|
20617
|
+
model_requests: 0,
|
|
20618
|
+
upstream_429: 0,
|
|
20619
|
+
last_upstream_status: null,
|
|
20620
|
+
cap_hits: 0,
|
|
20621
|
+
mcp_requests: 0,
|
|
20622
|
+
remints: 0,
|
|
20623
|
+
egress_denied: 0,
|
|
20624
|
+
first_egress_denied: null
|
|
20625
|
+
};
|
|
19957
20626
|
}
|
|
19958
20627
|
function indexOf(slot) {
|
|
19959
20628
|
return typeof slot === "number" ? slot : slot.index;
|
|
@@ -19963,6 +20632,7 @@ var init_slots = __esm({
|
|
|
19963
20632
|
"src/daemon/sandbox/slots.ts"() {
|
|
19964
20633
|
"use strict";
|
|
19965
20634
|
init_credentialGate();
|
|
20635
|
+
init_githubGate();
|
|
19966
20636
|
init_mcpGate();
|
|
19967
20637
|
init_slotMap();
|
|
19968
20638
|
init_types();
|
|
@@ -19970,6 +20640,7 @@ var init_slots = __esm({
|
|
|
19970
20640
|
slotMap;
|
|
19971
20641
|
credential;
|
|
19972
20642
|
mcp;
|
|
20643
|
+
github;
|
|
19973
20644
|
boundPorts = null;
|
|
19974
20645
|
constructor(opts) {
|
|
19975
20646
|
this.slotMap = new SlotMap(opts.slots, opts.slotMapOptions);
|
|
@@ -19990,21 +20661,32 @@ var init_slots = __esm({
|
|
|
19990
20661
|
logger: opts.logger,
|
|
19991
20662
|
timers: opts.timers
|
|
19992
20663
|
});
|
|
20664
|
+
this.github = new GithubGate({
|
|
20665
|
+
slotMap: this.slotMap,
|
|
20666
|
+
host: opts.host,
|
|
20667
|
+
port: opts.ports?.github ?? GATE_PORTS.github,
|
|
20668
|
+
logger: opts.logger,
|
|
20669
|
+
dial: opts.githubDial
|
|
20670
|
+
});
|
|
19993
20671
|
}
|
|
19994
|
-
/** Binds
|
|
20672
|
+
/** Binds all three gates; resolves with the ports actually bound. */
|
|
19995
20673
|
async start() {
|
|
19996
|
-
const
|
|
20674
|
+
const started = [];
|
|
19997
20675
|
try {
|
|
20676
|
+
const anthropic = await this.credential.listen();
|
|
20677
|
+
started.push(this.credential);
|
|
19998
20678
|
const mcp = await this.mcp.listen();
|
|
19999
|
-
this.
|
|
20679
|
+
started.push(this.mcp);
|
|
20680
|
+
const github = await this.github.listen();
|
|
20681
|
+
this.boundPorts = { anthropic, mcp, github };
|
|
20000
20682
|
return this.boundPorts;
|
|
20001
20683
|
} catch (err4) {
|
|
20002
|
-
await
|
|
20684
|
+
await Promise.all(started.map((g) => g.close()));
|
|
20003
20685
|
throw err4;
|
|
20004
20686
|
}
|
|
20005
20687
|
}
|
|
20006
20688
|
async stop() {
|
|
20007
|
-
await Promise.all([this.credential.close(), this.mcp.close()]);
|
|
20689
|
+
await Promise.all([this.credential.close(), this.mcp.close(), this.github.close()]);
|
|
20008
20690
|
this.boundPorts = null;
|
|
20009
20691
|
}
|
|
20010
20692
|
ports() {
|
|
@@ -20014,176 +20696,69 @@ var init_slots = __esm({
|
|
|
20014
20696
|
bindJob(slot, input) {
|
|
20015
20697
|
const index = indexOf(slot);
|
|
20016
20698
|
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}$/;
|
|
20699
|
+
jobId: input.jobId,
|
|
20700
|
+
maxModelRequests: input.maxModelRequests,
|
|
20701
|
+
mcp: input.mcp ? { ...input.mcp } : null,
|
|
20702
|
+
step: null,
|
|
20703
|
+
counters: zeroCounters()
|
|
20704
|
+
});
|
|
20705
|
+
this.mcp.onBind(index);
|
|
20706
|
+
}
|
|
20707
|
+
/** Which step's box is running on the slot; set before each box starts.
|
|
20708
|
+
* Leaving the clone step closes the slot's GitHub tunnels. */
|
|
20709
|
+
setStep(slot, step) {
|
|
20710
|
+
const index = indexOf(slot);
|
|
20711
|
+
const b = this.slotMap.binding(index);
|
|
20712
|
+
if (!b) throw new Error(`slot ${index} is not bound`);
|
|
20713
|
+
b.step = step;
|
|
20714
|
+
this.github.onStepChange(index);
|
|
20715
|
+
}
|
|
20716
|
+
/** Unbinds the slot (it answers 403 again) and returns the job's final counters. */
|
|
20717
|
+
clearJob(slot) {
|
|
20718
|
+
const index = indexOf(slot);
|
|
20719
|
+
const counters = this.countersFor(index);
|
|
20720
|
+
this.mcp.onClear(index);
|
|
20721
|
+
this.github.onClear(index);
|
|
20722
|
+
this.slotMap.clear(index);
|
|
20723
|
+
return counters;
|
|
20724
|
+
}
|
|
20725
|
+
/** A snapshot of the slot's counters, or null when idle. */
|
|
20726
|
+
countersFor(slot) {
|
|
20727
|
+
const b = this.slotMap.binding(indexOf(slot));
|
|
20728
|
+
return b ? { ...b.counters } : null;
|
|
20729
|
+
}
|
|
20730
|
+
};
|
|
20731
|
+
SlotPool = class {
|
|
20732
|
+
free_ = /* @__PURE__ */ new Set();
|
|
20733
|
+
byIndex = /* @__PURE__ */ new Map();
|
|
20734
|
+
constructor(slots) {
|
|
20735
|
+
for (const s of slots) {
|
|
20736
|
+
this.byIndex.set(s.index, s);
|
|
20737
|
+
this.free_.add(s.index);
|
|
20738
|
+
}
|
|
20739
|
+
}
|
|
20740
|
+
/** The lowest free slot, or null when every slot is busy. */
|
|
20741
|
+
lease() {
|
|
20742
|
+
let best = null;
|
|
20743
|
+
for (const i2 of this.free_) if (best === null || i2 < best) best = i2;
|
|
20744
|
+
if (best === null) return null;
|
|
20745
|
+
this.free_.delete(best);
|
|
20746
|
+
return this.byIndex.get(best) ?? null;
|
|
20747
|
+
}
|
|
20748
|
+
/** Idempotent; unknown indices are ignored. */
|
|
20749
|
+
free(index) {
|
|
20750
|
+
if (this.byIndex.has(index)) this.free_.add(index);
|
|
20751
|
+
}
|
|
20752
|
+
available() {
|
|
20753
|
+
return this.free_.size;
|
|
20754
|
+
}
|
|
20755
|
+
};
|
|
20181
20756
|
}
|
|
20182
20757
|
});
|
|
20183
20758
|
|
|
20184
20759
|
// src/daemon/sandboxJob/clone.ts
|
|
20185
|
-
import { mkdir as
|
|
20186
|
-
import { dirname as
|
|
20760
|
+
import { mkdir as mkdir12, writeFile as writeFile11 } from "node:fs/promises";
|
|
20761
|
+
import { dirname as dirname16 } from "node:path";
|
|
20187
20762
|
function asArchmeJob(job) {
|
|
20188
20763
|
return job;
|
|
20189
20764
|
}
|
|
@@ -20208,7 +20783,7 @@ async function runCloneStep(input, deps = {}) {
|
|
|
20208
20783
|
const { env: gitEnv, cleanup } = await buildGitEnv(input.credentials);
|
|
20209
20784
|
const restoreEnv = applyEnv(gitEnv);
|
|
20210
20785
|
try {
|
|
20211
|
-
await
|
|
20786
|
+
await mkdir12(repoDir, { recursive: true });
|
|
20212
20787
|
const must = async (args, cwd) => {
|
|
20213
20788
|
const r = await git4(args, { cwd, env: gitEnv });
|
|
20214
20789
|
if (r.exitCode !== 0) throw new Error(`git ${args[0]} failed: ${r.stderr.trim()}`);
|
|
@@ -20260,7 +20835,7 @@ async function runCloneStep(input, deps = {}) {
|
|
|
20260
20835
|
after: diff.after,
|
|
20261
20836
|
...diff.provenance ? { provenance: diff.provenance } : {}
|
|
20262
20837
|
};
|
|
20263
|
-
await
|
|
20838
|
+
await mkdir12(dirname16(diffPath), { recursive: true });
|
|
20264
20839
|
await writeFile11(diffPath, JSON.stringify(persisted), { mode: 384 });
|
|
20265
20840
|
const bytes = Buffer.byteLength(diff.patch, "utf8");
|
|
20266
20841
|
log(`diff ${diff.rangeLabel}: ${diff.changedFiles.length} file(s), ${bytes} bytes${diff.materialized ? " (materialized)" : ""}`);
|
|
@@ -20316,7 +20891,7 @@ var init_clone = __esm({
|
|
|
20316
20891
|
});
|
|
20317
20892
|
|
|
20318
20893
|
// src/daemon/sandboxJob/agent.ts
|
|
20319
|
-
import { mkdir as
|
|
20894
|
+
import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile12 } from "node:fs/promises";
|
|
20320
20895
|
import { join as join23 } from "node:path";
|
|
20321
20896
|
function agentBoxEnv(gateway) {
|
|
20322
20897
|
return {
|
|
@@ -20329,10 +20904,16 @@ function agentBoxEnv(gateway) {
|
|
|
20329
20904
|
DISABLE_ERROR_REPORTING: "1"
|
|
20330
20905
|
};
|
|
20331
20906
|
}
|
|
20332
|
-
function cloneBoxEnv() {
|
|
20907
|
+
function cloneBoxEnv(gateway) {
|
|
20908
|
+
const proxy = `http://${gateway}:${GATE_PORTS.github}`;
|
|
20333
20909
|
return {
|
|
20334
20910
|
HOME: BOX_HOME,
|
|
20335
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
20911
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
20912
|
+
https_proxy: proxy,
|
|
20913
|
+
HTTPS_PROXY: proxy,
|
|
20914
|
+
GIT_CONFIG_COUNT: "1",
|
|
20915
|
+
GIT_CONFIG_KEY_0: "http.proxy",
|
|
20916
|
+
GIT_CONFIG_VALUE_0: proxy
|
|
20336
20917
|
};
|
|
20337
20918
|
}
|
|
20338
20919
|
function boxMcpConfig(gateway) {
|
|
@@ -20343,7 +20924,7 @@ function boxMcpConfig(gateway) {
|
|
|
20343
20924
|
};
|
|
20344
20925
|
}
|
|
20345
20926
|
async function writeBoxMcpConfig(gateway, dir = BOX_MCP_DIR) {
|
|
20346
|
-
await
|
|
20927
|
+
await mkdir13(dir, { recursive: true, mode: 448 });
|
|
20347
20928
|
const path7 = join23(dir, "mcp.json");
|
|
20348
20929
|
await writeFile12(path7, JSON.stringify(boxMcpConfig(gateway), null, 2), { mode: 384 });
|
|
20349
20930
|
return path7;
|
|
@@ -20411,7 +20992,7 @@ async function runAgentStep(input, deps = {}) {
|
|
|
20411
20992
|
);
|
|
20412
20993
|
const promptBytes = Buffer.byteLength(prompt, "utf8");
|
|
20413
20994
|
const mcpConfigPath = await writeBoxMcpConfig(input.gateway, deps.mcpDir);
|
|
20414
|
-
for (const d of deps.homeDirs ?? [BOX_HOME, BOX_CLAUDE_CONFIG_DIR]) await
|
|
20995
|
+
for (const d of deps.homeDirs ?? [BOX_HOME, BOX_CLAUDE_CONFIG_DIR]) await mkdir13(d, { recursive: true, mode: 448 });
|
|
20415
20996
|
log(`spawning claude (model ${input.model}, prompt ${promptBytes} bytes)`);
|
|
20416
20997
|
const proc = spawn2({
|
|
20417
20998
|
prompt,
|
|
@@ -20515,6 +21096,7 @@ function createSandboxParts(settings) {
|
|
|
20515
21096
|
await gates.start();
|
|
20516
21097
|
return {
|
|
20517
21098
|
bindJob: (slot, b) => gates.bindJob(slot, b),
|
|
21099
|
+
setStep: (slot, step) => gates.setStep(slot, step),
|
|
20518
21100
|
clearJob: (slot) => {
|
|
20519
21101
|
gates.clearJob(slot);
|
|
20520
21102
|
},
|
|
@@ -20565,7 +21147,7 @@ __export(run_exports, {
|
|
|
20565
21147
|
});
|
|
20566
21148
|
import { createClient } from "@supabase/supabase-js";
|
|
20567
21149
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
20568
|
-
import { mkdtemp as mkdtemp4, mkdir as
|
|
21150
|
+
import { mkdtemp as mkdtemp4, mkdir as mkdir14, writeFile as writeFile13, rm as rm8, stat as stat8, chmod as chmod6 } from "node:fs/promises";
|
|
20569
21151
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
20570
21152
|
import { join as join24 } from "node:path";
|
|
20571
21153
|
import { homedir as homedir8 } from "node:os";
|
|
@@ -20732,7 +21314,7 @@ async function buildGitEnv(credentials) {
|
|
|
20732
21314
|
].join("\n"),
|
|
20733
21315
|
{ mode: 448 }
|
|
20734
21316
|
);
|
|
20735
|
-
await
|
|
21317
|
+
await chmod6(askpassPath, 448).catch(() => {
|
|
20736
21318
|
});
|
|
20737
21319
|
const gitConfigEntries = [
|
|
20738
21320
|
["credential.helper", ""],
|
|
@@ -20793,12 +21375,12 @@ async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
|
|
|
20793
21375
|
const deviceId = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId();
|
|
20794
21376
|
if (!deviceId) throw new Error("daemon is not signed in with a registered device");
|
|
20795
21377
|
const credentials = await getRepoCredentials(ctx, job.id, deviceId);
|
|
20796
|
-
await
|
|
21378
|
+
await mkdir14(ctx.cloneBaseDir, { recursive: true });
|
|
20797
21379
|
const dir = cloneDirFor(ctx.cloneBaseDir, job.repo_owner, job.repo_name);
|
|
20798
21380
|
const { env, cleanup: cleanupGitEnv } = await buildGitEnv(credentials);
|
|
20799
21381
|
await cloneMutex.runExclusive(dir, async () => {
|
|
20800
21382
|
if (!await exists(join24(dir, ".git"))) {
|
|
20801
|
-
await
|
|
21383
|
+
await mkdir14(join24(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
|
|
20802
21384
|
console.log(` cloning ${job.repo_owner}/${job.repo_name} \u2192 ${dir}`);
|
|
20803
21385
|
await execa("git", ["clone", "--no-tags", credentials.https_url, dir], { env, reject: true });
|
|
20804
21386
|
} else {
|
|
@@ -21441,7 +22023,7 @@ function sandboxDepsFor(ctx, job, deviceId, host) {
|
|
|
21441
22023
|
settings: host.settings,
|
|
21442
22024
|
parseResultLine: (stdout, nonce) => host.io.parseResultLine(stdout, nonce),
|
|
21443
22025
|
agentBoxEnv: (gateway) => host.io.agentBoxEnv(gateway),
|
|
21444
|
-
cloneBoxEnv: () => host.io.cloneBoxEnv(),
|
|
22026
|
+
cloneBoxEnv: (gateway) => host.io.cloneBoxEnv(gateway),
|
|
21445
22027
|
prefetch: (j) => (prefetchOverride ?? prefetchJobContext)(ctx, j, deviceId),
|
|
21446
22028
|
writeInputDir: (step, input) => writeInputDir(step, input),
|
|
21447
22029
|
log: (msg) => console.log(`[${job.id}] ${msg}`),
|
|
@@ -21533,7 +22115,9 @@ async function spawnTriage(jobId, mcpConfigPath, cwd, opts) {
|
|
|
21533
22115
|
model: opts.model,
|
|
21534
22116
|
allowedTools: TRIAGE_ALLOWED_TOOLS,
|
|
21535
22117
|
builtinTools: TRIAGE_BUILTIN_TOOLS,
|
|
21536
|
-
jsonOutput: true
|
|
22118
|
+
jsonOutput: true,
|
|
22119
|
+
settingSources: "user"
|
|
22120
|
+
// the checkout is the customer's; see runJob's spawn
|
|
21537
22121
|
});
|
|
21538
22122
|
inFlight.set(jobId, proc);
|
|
21539
22123
|
const result = await proc;
|
|
@@ -21585,8 +22169,13 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
21585
22169
|
const isLocalRepo = findLocalRepoForJob(await loadLocalRepos(), job) != null;
|
|
21586
22170
|
const decision = shouldSandbox(sandboxHost.settings, job, { isLocalRepo });
|
|
21587
22171
|
if (decision.reason === "local_repo") log("sandbox_skipped_local_repo: local-poll repo, running unsandboxed");
|
|
22172
|
+
const required = sandboxHost.settings.mode === "required";
|
|
22173
|
+
if (decision.reason === "local_repo" && required) log("sandbox: required mode keeps local-poll repos unboxed (\xA713)");
|
|
21588
22174
|
const slot = decision.boxed ? sandboxHost.pool.lease() : null;
|
|
21589
|
-
if (decision.boxed && !slot)
|
|
22175
|
+
if (decision.boxed && !slot) {
|
|
22176
|
+
if (required) throw new Error("sandbox_no_slot: no free sandbox slot (ARCHME_SANDBOX=required); retrying later");
|
|
22177
|
+
log("sandbox_no_slot: no free slot, running unsandboxed");
|
|
22178
|
+
}
|
|
21590
22179
|
if (slot) sandboxRun: {
|
|
21591
22180
|
boxedJobs.add(job.id);
|
|
21592
22181
|
log(`sandbox: running boxed on slot ${slot.index}`);
|
|
@@ -21597,7 +22186,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
21597
22186
|
timeouts: { claudeMs: CLAUDE_TIMEOUT_MS, largeDiffClaudeMs: CLAUDE_LARGE_DIFF_TIMEOUT_MS }
|
|
21598
22187
|
});
|
|
21599
22188
|
} catch (err4) {
|
|
21600
|
-
if (err4 instanceof SandboxInfraError && !err4.agentStarted && !shuttingDown && !cancelled.has(job.id)) {
|
|
22189
|
+
if (err4 instanceof SandboxInfraError && !err4.agentStarted && !shuttingDown && !cancelled.has(job.id) && sandboxHost.settings.mode === "shadow") {
|
|
21601
22190
|
log(`sandbox_infra_fallback: ${err4.message}, running unboxed`);
|
|
21602
22191
|
boxedJobs.delete(job.id);
|
|
21603
22192
|
break sandboxRun;
|
|
@@ -21890,7 +22479,10 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
21890
22479
|
// Single-object JSON so we can read the served model + usage/cost. stdout
|
|
21891
22480
|
// is then one raw JSON blob — tee it SILENTLY (unreadable in `daemon -f`)
|
|
21892
22481
|
// and log the parsed `.result` after exit instead.
|
|
21893
|
-
jsonOutput: true
|
|
22482
|
+
jsonOutput: true,
|
|
22483
|
+
// The checkout is the customer's: never load its .claude/settings.json
|
|
22484
|
+
// (hooks there would run as shell commands on this host).
|
|
22485
|
+
settingSources: "user"
|
|
21894
22486
|
});
|
|
21895
22487
|
inFlight.set(job.id, proc);
|
|
21896
22488
|
const getStdout = teeStream(proc.stdout, `[${job.id}] claude> `, { silent: true });
|
|
@@ -22057,6 +22649,7 @@ async function dispatch(sched) {
|
|
|
22057
22649
|
}
|
|
22058
22650
|
pauseUntilMs = null;
|
|
22059
22651
|
}
|
|
22652
|
+
if (confinementGate && !confinementGate.allowsClaims()) return;
|
|
22060
22653
|
sched.dispatching = true;
|
|
22061
22654
|
try {
|
|
22062
22655
|
let atCap = false;
|
|
@@ -22250,7 +22843,10 @@ async function runDaemon() {
|
|
|
22250
22843
|
if (process.env.ARCHME_SANDBOX_SLOTS !== void 0) {
|
|
22251
22844
|
console.log(" ARCHME_SANDBOX_SLOTS is no longer read; sandbox slots follow MAX_CONCURRENT_JOBS");
|
|
22252
22845
|
}
|
|
22253
|
-
console.log(` sandbox: ${sandboxSettings.mode}${sandboxSettings.mode === "shadow" ? ` (projects: ${sandboxSettings.projects.size ? [...sandboxSettings.projects].join(", ") : "all"}; ${sandboxSettings.slots} slot(s))` : ""}`);
|
|
22846
|
+
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))` : ""}`);
|
|
22847
|
+
if (sandboxSettings.mode === "required" && process.env.ARCHME_SANDBOX_PROJECTS?.trim()) {
|
|
22848
|
+
console.log(" ARCHME_SANDBOX_PROJECTS is ignored in required mode: every project's jobs run boxed");
|
|
22849
|
+
}
|
|
22254
22850
|
const bearerCfg = {
|
|
22255
22851
|
supabaseUrl: SUPABASE_URL,
|
|
22256
22852
|
anonKey: SUPABASE_ANON_KEY,
|
|
@@ -22323,7 +22919,7 @@ async function runDaemon() {
|
|
|
22323
22919
|
if (sandboxSettings.mode !== "off") {
|
|
22324
22920
|
try {
|
|
22325
22921
|
const { createSandboxParts: createSandboxParts2 } = await Promise.resolve().then(() => (init_parts(), parts_exports));
|
|
22326
|
-
const { host } = await bootSandbox(sandboxSettings, createSandboxParts2(sandboxSettings), {
|
|
22922
|
+
const { host, monitor } = await bootSandbox(sandboxSettings, createSandboxParts2(sandboxSettings), {
|
|
22327
22923
|
mcpBaseUrl: ARCHME_MCP_BASE_URL,
|
|
22328
22924
|
// The MCP gatekeeper re-mints a job's token before it expires (§7.2),
|
|
22329
22925
|
// with the device that claimed that job, as every job-scoped call must.
|
|
@@ -22337,6 +22933,8 @@ async function runDaemon() {
|
|
|
22337
22933
|
log: (m) => console.log(m)
|
|
22338
22934
|
});
|
|
22339
22935
|
sandboxHost = host;
|
|
22936
|
+
monitor.start();
|
|
22937
|
+
confinementGate = monitor;
|
|
22340
22938
|
} catch (err4) {
|
|
22341
22939
|
const msg = err4 instanceof Error ? err4.message : String(err4);
|
|
22342
22940
|
if (sandboxBootFailurePolicy(sandboxSettings.mode) === "run_unboxed") {
|
|
@@ -22465,6 +23063,7 @@ ${signal} received, shutting down`);
|
|
|
22465
23063
|
for (const ctx of sessions) ctx.auth.clearRefreshTimer();
|
|
22466
23064
|
clearInterval(sweepTimer);
|
|
22467
23065
|
clearInterval(localPollTimer);
|
|
23066
|
+
confinementGate?.stop?.();
|
|
22468
23067
|
for (const u2 of unsubscribers) u2();
|
|
22469
23068
|
const SHUTDOWN_DEADLINE_MS = 3e4;
|
|
22470
23069
|
const deadline = setTimeout(() => {
|
|
@@ -22507,7 +23106,7 @@ ${signal} received, shutting down`);
|
|
|
22507
23106
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
22508
23107
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
22509
23108
|
}
|
|
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;
|
|
23109
|
+
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
23110
|
var init_run = __esm({
|
|
22512
23111
|
"src/daemon/run.ts"() {
|
|
22513
23112
|
"use strict";
|
|
@@ -22587,6 +23186,7 @@ var init_run = __esm({
|
|
|
22587
23186
|
ctxByJob = /* @__PURE__ */ new Map();
|
|
22588
23187
|
deviceByJob = /* @__PURE__ */ new Map();
|
|
22589
23188
|
sandboxHost = null;
|
|
23189
|
+
confinementGate = null;
|
|
22590
23190
|
boxedJobs = /* @__PURE__ */ new Set();
|
|
22591
23191
|
leaseLost = /* @__PURE__ */ new Set();
|
|
22592
23192
|
prefetchOverride = null;
|
|
@@ -22767,6 +23367,9 @@ ${ANCHOR_VERIFICATION}`;
|
|
|
22767
23367
|
sandboxHost = host;
|
|
22768
23368
|
prefetchOverride = prefetch;
|
|
22769
23369
|
},
|
|
23370
|
+
setConfinementGate(gate) {
|
|
23371
|
+
confinementGate = gate;
|
|
23372
|
+
},
|
|
22770
23373
|
// Daemon-version hooks for tests / introspection. The real value is
|
|
22771
23374
|
// populated at runDaemon boot from package.json; tests can stub.
|
|
22772
23375
|
setDaemonVersion(v) {
|
|
@@ -22779,13 +23382,450 @@ ${ANCHOR_VERIFICATION}`;
|
|
|
22779
23382
|
}
|
|
22780
23383
|
});
|
|
22781
23384
|
|
|
23385
|
+
// src/daemon/sandbox/pfAnchor.ts
|
|
23386
|
+
function renderAnchor() {
|
|
23387
|
+
const slotLines = Array.from({ length: MAX_SLOTS }, (_, i2) => {
|
|
23388
|
+
const s = slotFor(i2);
|
|
23389
|
+
return `pass in quick inet proto tcp from ${s.subnet} to ${s.gateway} port ${PORTS} keep state`;
|
|
23390
|
+
});
|
|
23391
|
+
return [
|
|
23392
|
+
`# ${ANCHOR_NAME} \u2014 installed by \`archme daemon sandbox setup\` (L2 \xA78.2).`,
|
|
23393
|
+
"# DHCP inside each slot",
|
|
23394
|
+
"pass in quick inet proto udp from any port 68 to any port 67",
|
|
23395
|
+
"# each slot may reach only its own gateway, only on the gatekeeper ports",
|
|
23396
|
+
...slotLines,
|
|
23397
|
+
"# everything else from a slot: host services on any host address, forwarding",
|
|
23398
|
+
`block drop in quick inet from ${SLOT_RANGE} to any`,
|
|
23399
|
+
"# the gatekeepers listen on the wildcard: nobody but a slot reaches them",
|
|
23400
|
+
`block drop in quick inet proto tcp from any to any port ${PORTS}`,
|
|
23401
|
+
""
|
|
23402
|
+
].join("\n");
|
|
23403
|
+
}
|
|
23404
|
+
function addAnchorToPfConf(text) {
|
|
23405
|
+
const lines = text.replace(/\n$/, "").split("\n");
|
|
23406
|
+
if (ANCHOR_LINES.every((l) => lines.some((x) => x.trim() === l))) return { text, changed: false };
|
|
23407
|
+
const kept = lines.filter((x) => !ANCHOR_LINES.includes(x.trim()));
|
|
23408
|
+
let at = -1;
|
|
23409
|
+
kept.forEach((l, i2) => {
|
|
23410
|
+
if (!l.trim().startsWith("#") && l.includes("com.apple")) at = i2;
|
|
23411
|
+
});
|
|
23412
|
+
const insertAt = at >= 0 ? at + 1 : kept.length;
|
|
23413
|
+
kept.splice(insertAt, 0, ...ANCHOR_LINES);
|
|
23414
|
+
return { text: `${kept.join("\n")}
|
|
23415
|
+
`, changed: true };
|
|
23416
|
+
}
|
|
23417
|
+
function renderPfLaunchDaemon() {
|
|
23418
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
23419
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
23420
|
+
<plist version="1.0">
|
|
23421
|
+
<dict>
|
|
23422
|
+
<key>Label</key>
|
|
23423
|
+
<string>${PF_DAEMON_LABEL}</string>
|
|
23424
|
+
<key>ProgramArguments</key>
|
|
23425
|
+
<array>
|
|
23426
|
+
<string>/sbin/pfctl</string>
|
|
23427
|
+
<string>-E</string>
|
|
23428
|
+
<string>-f</string>
|
|
23429
|
+
<string>${PF_CONF_PATH}</string>
|
|
23430
|
+
</array>
|
|
23431
|
+
<key>RunAtLoad</key>
|
|
23432
|
+
<true/>
|
|
23433
|
+
</dict>
|
|
23434
|
+
</plist>
|
|
23435
|
+
`;
|
|
23436
|
+
}
|
|
23437
|
+
function parseDestination(dest) {
|
|
23438
|
+
const [addr, bitsStr] = dest.split("/");
|
|
23439
|
+
if (!addr || !/^\d+(\.\d+){0,3}$/.test(addr)) return null;
|
|
23440
|
+
const octets = addr.split(".");
|
|
23441
|
+
const bits = bitsStr !== void 0 ? Number(bitsStr) : octets.length * 8;
|
|
23442
|
+
while (octets.length < 4) octets.push("0");
|
|
23443
|
+
const base = parseIpv4(octets.join("."));
|
|
23444
|
+
if (base === null || !Number.isInteger(bits) || bits <= 0 || bits > 32) return null;
|
|
23445
|
+
return { base, bits };
|
|
23446
|
+
}
|
|
23447
|
+
function mask(bits) {
|
|
23448
|
+
return bits === 0 ? 0 : ~0 << 32 - bits >>> 0;
|
|
23449
|
+
}
|
|
23450
|
+
function overlaps(a2, b) {
|
|
23451
|
+
const m = mask(Math.min(a2.bits, b.bits));
|
|
23452
|
+
return (a2.base & m) >>> 0 === (b.base & m) >>> 0;
|
|
23453
|
+
}
|
|
23454
|
+
function within(inner, outer) {
|
|
23455
|
+
return inner.bits >= outer.bits && overlaps(inner, outer);
|
|
23456
|
+
}
|
|
23457
|
+
function routeCollisions(netstat, ourSlots2) {
|
|
23458
|
+
const range2 = parseDestination(SLOT_RANGE.replace(".0/21", "/21"));
|
|
23459
|
+
const ours = [...ourSlots2].map((i2) => parseDestination(slotFor(i2).subnet.replace(".0/24", "/24")));
|
|
23460
|
+
const out = [];
|
|
23461
|
+
for (const line of netstat.split("\n")) {
|
|
23462
|
+
const cols = line.trim().split(/\s+/);
|
|
23463
|
+
const net3 = parseDestination(cols[0] ?? "");
|
|
23464
|
+
if (!net3 || !overlaps(net3, range2)) continue;
|
|
23465
|
+
if (ours.some((o2) => within(net3, o2))) continue;
|
|
23466
|
+
out.push(`${cols[0]} via ${cols[3] ?? "?"}`);
|
|
23467
|
+
}
|
|
23468
|
+
return out;
|
|
23469
|
+
}
|
|
23470
|
+
function bakedUnitEnv(plist) {
|
|
23471
|
+
const m = /<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/.exec(plist);
|
|
23472
|
+
if (!m) return {};
|
|
23473
|
+
const out = {};
|
|
23474
|
+
const unescape2 = (s) => s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
23475
|
+
for (const kv of m[1].matchAll(/<key>([^<]*)<\/key>\s*<string>([^<]*)<\/string>/g)) {
|
|
23476
|
+
out[unescape2(kv[1])] = unescape2(kv[2]);
|
|
23477
|
+
}
|
|
23478
|
+
return out;
|
|
23479
|
+
}
|
|
23480
|
+
var ANCHOR_NAME, ANCHOR_PATH, PF_CONF_PATH, PF_CONF_BACKUP_PATH, PF_DAEMON_LABEL, PF_DAEMON_PATH, SLOT_RANGE, PORTS, ANCHOR_LINES;
|
|
23481
|
+
var init_pfAnchor = __esm({
|
|
23482
|
+
"src/daemon/sandbox/pfAnchor.ts"() {
|
|
23483
|
+
"use strict";
|
|
23484
|
+
init_appleContainer();
|
|
23485
|
+
init_slotMap();
|
|
23486
|
+
init_types();
|
|
23487
|
+
ANCHOR_NAME = "com.archme.sandbox";
|
|
23488
|
+
ANCHOR_PATH = `/etc/pf.anchors/${ANCHOR_NAME}`;
|
|
23489
|
+
PF_CONF_PATH = "/etc/pf.conf";
|
|
23490
|
+
PF_CONF_BACKUP_PATH = "/etc/pf.conf.archme-backup";
|
|
23491
|
+
PF_DAEMON_LABEL = "com.archme.pf";
|
|
23492
|
+
PF_DAEMON_PATH = `/Library/LaunchDaemons/${PF_DAEMON_LABEL}.plist`;
|
|
23493
|
+
SLOT_RANGE = `192.168.${SLOT_RANGE_BASE_THIRD_OCTET}.0/21`;
|
|
23494
|
+
PORTS = `{ ${GATE_PORTS.anthropic} ${GATE_PORTS.mcp} ${GATE_PORTS.github} }`;
|
|
23495
|
+
ANCHOR_LINES = [
|
|
23496
|
+
`anchor "${ANCHOR_NAME}"`,
|
|
23497
|
+
`load anchor "${ANCHOR_NAME}" from "${ANCHOR_PATH}"`
|
|
23498
|
+
];
|
|
23499
|
+
}
|
|
23500
|
+
});
|
|
23501
|
+
|
|
23502
|
+
// src/daemon/sandbox/setupCommand.ts
|
|
23503
|
+
var setupCommand_exports = {};
|
|
23504
|
+
__export(setupCommand_exports, {
|
|
23505
|
+
HARDENING_TEXT: () => HARDENING_TEXT,
|
|
23506
|
+
runSandboxSetup: () => runSandboxSetup,
|
|
23507
|
+
runSandboxStatus: () => runSandboxStatus
|
|
23508
|
+
});
|
|
23509
|
+
async function checkHost(io) {
|
|
23510
|
+
const ver = (await io.run("sw_vers", ["-productVersion"])).stdout.trim();
|
|
23511
|
+
const major = Number(ver.split(".")[0]);
|
|
23512
|
+
if (!(major >= 26)) return `macOS 26 or later is required (this is ${ver || "not macOS"})`;
|
|
23513
|
+
const arch = (await io.run("uname", ["-m"])).stdout.trim();
|
|
23514
|
+
if (arch !== "arm64") return `Apple Silicon is required (this is ${arch || "unknown"})`;
|
|
23515
|
+
if (!await runtimeUp(io)) return "Apple `container` is not running; start it with `container system start`";
|
|
23516
|
+
return null;
|
|
23517
|
+
}
|
|
23518
|
+
async function runtimeUp(io) {
|
|
23519
|
+
const s = await io.run("container", ["system", "status"]);
|
|
23520
|
+
return s.exitCode === 0 && /^status\s+running\b/m.test(s.stdout);
|
|
23521
|
+
}
|
|
23522
|
+
async function ourSlots(io) {
|
|
23523
|
+
const nets = await io.listNetworks();
|
|
23524
|
+
const ours = /* @__PURE__ */ new Set();
|
|
23525
|
+
for (let i2 = 0; i2 < MAX_SLOTS; i2++) {
|
|
23526
|
+
const s = slotFor(i2);
|
|
23527
|
+
if (nets.some((n2) => n2.name === s.network && n2.subnet === s.subnet)) ours.add(i2);
|
|
23528
|
+
}
|
|
23529
|
+
return ours;
|
|
23530
|
+
}
|
|
23531
|
+
async function currentMode(io) {
|
|
23532
|
+
const unit = await io.unitEnv();
|
|
23533
|
+
if (unit) return { mode: unit.ARCHME_SANDBOX?.trim() || "off", from: "installed service" };
|
|
23534
|
+
return { mode: io.env.ARCHME_SANDBOX?.trim() || "off", from: "this shell" };
|
|
23535
|
+
}
|
|
23536
|
+
async function runSandboxSetup(io, opts) {
|
|
23537
|
+
const fail2 = (msg) => {
|
|
23538
|
+
io.print(`\u2717 ${msg}`);
|
|
23539
|
+
return 1;
|
|
23540
|
+
};
|
|
23541
|
+
const host = await checkHost(io);
|
|
23542
|
+
if (host) return fail2(host);
|
|
23543
|
+
io.print("\u2713 macOS 26+, Apple Silicon, container running");
|
|
23544
|
+
if (!io.image) return fail2("no sandbox image: this install ships no sandbox-image.json, so set ARCHME_SANDBOX_IMAGE");
|
|
23545
|
+
if ((await io.run("container", ["image", "inspect", io.image])).exitCode !== 0) {
|
|
23546
|
+
io.print(`Pulling ${io.image}\u2026`);
|
|
23547
|
+
const pull = await io.run("container", ["image", "pull", io.image]);
|
|
23548
|
+
if (pull.exitCode !== 0) return fail2(`image pull failed: ${pull.stderr.trim()}`);
|
|
23549
|
+
}
|
|
23550
|
+
io.print(`\u2713 image ${io.image}`);
|
|
23551
|
+
const existing = await io.readToken().catch(() => null);
|
|
23552
|
+
const keep = existing && (opts.yes || await io.confirm(`A Claude token is stored (${existing.source}). Keep it?`));
|
|
23553
|
+
if (keep) {
|
|
23554
|
+
io.print(`\u2713 Claude token (${existing.source})`);
|
|
23555
|
+
} else {
|
|
23556
|
+
io.print("Run `claude setup-token` and paste the token it prints (input is hidden).");
|
|
23557
|
+
const stored = await io.storeToken(await io.promptSecret("Claude setup token: "));
|
|
23558
|
+
io.print(`\u2713 Claude token stored in ${stored.source === "keychain" ? "the macOS keychain" : "a 0600 file"}${stored.note ? ` (${stored.note})` : ""}`);
|
|
23559
|
+
}
|
|
23560
|
+
const netstat = await io.run("netstat", ["-rn", "-f", "inet"]);
|
|
23561
|
+
if (netstat.exitCode !== 0) return fail2(`netstat failed: ${netstat.stderr.trim()}`);
|
|
23562
|
+
const collisions = routeCollisions(netstat.stdout, await ourSlots(io));
|
|
23563
|
+
if (collisions.length) {
|
|
23564
|
+
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.`);
|
|
23565
|
+
}
|
|
23566
|
+
await io.ensureSlots(MAX_SLOTS);
|
|
23567
|
+
io.print(`\u2713 ${MAX_SLOTS} slot networks`);
|
|
23568
|
+
const anchor = renderAnchor();
|
|
23569
|
+
const plist = renderPfLaunchDaemon();
|
|
23570
|
+
const pfConf = await io.readFile(PF_CONF_PATH);
|
|
23571
|
+
if (pfConf === null) return fail2(`cannot read ${PF_CONF_PATH}`);
|
|
23572
|
+
const conf = addAnchorToPfConf(pfConf);
|
|
23573
|
+
const anchorChanged = await io.readFile(ANCHOR_PATH) !== anchor;
|
|
23574
|
+
const plistChanged = await io.readFile(PF_DAEMON_PATH) !== plist;
|
|
23575
|
+
const pfUnchanged = !anchorChanged && !conf.changed && !plistChanged;
|
|
23576
|
+
const reload = [
|
|
23577
|
+
// Not loaded yet on a first run.
|
|
23578
|
+
{ args: ["launchctl", "bootout", `system/${PF_DAEMON_LABEL}`], mayFail: true },
|
|
23579
|
+
// RunAtLoad: this applies the rules now.
|
|
23580
|
+
{ args: ["launchctl", "bootstrap", "system", PF_DAEMON_PATH] }
|
|
23581
|
+
];
|
|
23582
|
+
const runSudo = async (steps) => {
|
|
23583
|
+
for (const s of steps) {
|
|
23584
|
+
io.print(`$ sudo ${s.args.join(" ")}`);
|
|
23585
|
+
const r = await io.sudo(s.args);
|
|
23586
|
+
if (r.exitCode !== 0 && !s.mayFail) return `\`sudo ${s.args.join(" ")}\` failed (exit ${r.exitCode}); nothing after it ran`;
|
|
23587
|
+
}
|
|
23588
|
+
await io.sleep(2e3);
|
|
23589
|
+
return null;
|
|
23590
|
+
};
|
|
23591
|
+
if (pfUnchanged) {
|
|
23592
|
+
io.print("\u2713 pf: already installed");
|
|
23593
|
+
} else {
|
|
23594
|
+
io.print(`
|
|
23595
|
+
These rules go in ${ANCHOR_PATH}:
|
|
23596
|
+
`);
|
|
23597
|
+
for (const l of anchor.trimEnd().split("\n")) io.print(` ${l}`);
|
|
23598
|
+
if (conf.changed) {
|
|
23599
|
+
io.print(`
|
|
23600
|
+
${PF_CONF_PATH} gains (a backup goes to ${PF_CONF_BACKUP_PATH}):
|
|
23601
|
+
`);
|
|
23602
|
+
const before = new Set(pfConf.split("\n"));
|
|
23603
|
+
for (const l of conf.text.split("\n")) if (l && !before.has(l)) io.print(` + ${l}`);
|
|
23604
|
+
}
|
|
23605
|
+
io.print(`
|
|
23606
|
+
${PF_DAEMON_PATH} enables pf and loads ${PF_CONF_PATH} at every boot (${PF_DAEMON_LABEL}).
|
|
23607
|
+
`);
|
|
23608
|
+
if (!opts.yes && !await io.confirm("Install these with sudo?")) return fail2("pf not installed");
|
|
23609
|
+
const anchorTmp = await io.writeTemp("com.archme.sandbox", anchor);
|
|
23610
|
+
const confTmp = await io.writeTemp("pf.conf", conf.text);
|
|
23611
|
+
const plistTmp = await io.writeTemp(`${PF_DAEMON_LABEL}.plist`, plist);
|
|
23612
|
+
const steps = [
|
|
23613
|
+
{ args: sudoInstall(anchorTmp, ANCHOR_PATH) },
|
|
23614
|
+
// Validate the new pf.conf (it loads the anchor just installed) before it replaces the old.
|
|
23615
|
+
{ args: ["pfctl", "-nf", confTmp] },
|
|
23616
|
+
...conf.changed ? [{ args: ["cp", PF_CONF_PATH, PF_CONF_BACKUP_PATH] }, { args: sudoInstall(confTmp, PF_CONF_PATH) }] : [],
|
|
23617
|
+
{ args: sudoInstall(plistTmp, PF_DAEMON_PATH) },
|
|
23618
|
+
...reload
|
|
23619
|
+
];
|
|
23620
|
+
const err4 = await runSudo(steps);
|
|
23621
|
+
if (err4) return fail2(err4);
|
|
23622
|
+
io.print("\u2713 pf installed");
|
|
23623
|
+
}
|
|
23624
|
+
io.print("");
|
|
23625
|
+
let code = await runSandboxStatus(io);
|
|
23626
|
+
if (code !== 0 && pfUnchanged && (opts.yes || await io.confirm("Reload pf from the installed files with sudo?"))) {
|
|
23627
|
+
const err4 = await runSudo(reload);
|
|
23628
|
+
if (err4) return fail2(err4);
|
|
23629
|
+
io.print("");
|
|
23630
|
+
code = await runSandboxStatus(io);
|
|
23631
|
+
}
|
|
23632
|
+
const { mode, from } = await currentMode(io);
|
|
23633
|
+
io.print("");
|
|
23634
|
+
io.print(`ARCHME_SANDBOX is ${mode} (${from}); setup does not change it.`);
|
|
23635
|
+
io.print("To require confinement for every job: set ARCHME_SANDBOX=required in the daemon's environment, then `archme daemon restart`.");
|
|
23636
|
+
for (const l of HARDENING_TEXT) io.print(l);
|
|
23637
|
+
return code;
|
|
23638
|
+
}
|
|
23639
|
+
async function runSandboxStatus(io) {
|
|
23640
|
+
let ok = true;
|
|
23641
|
+
const line = (good, name, detail) => {
|
|
23642
|
+
if (!good) ok = false;
|
|
23643
|
+
io.print(`${good ? "\u2713" : "\u2717"} ${name.padEnd(9)} ${detail}`);
|
|
23644
|
+
};
|
|
23645
|
+
const up = await runtimeUp(io);
|
|
23646
|
+
line(up, "runtime", up ? "container running" : "container is not running");
|
|
23647
|
+
if (io.image) {
|
|
23648
|
+
const present = up && (await io.run("container", ["image", "inspect", io.image])).exitCode === 0;
|
|
23649
|
+
line(present, "image", `${io.image}${present ? "" : " (not present: run `archme daemon sandbox setup`)"}`);
|
|
23650
|
+
} else {
|
|
23651
|
+
line(false, "image", "none configured (no sandbox-image.json and no ARCHME_SANDBOX_IMAGE)");
|
|
23652
|
+
}
|
|
23653
|
+
try {
|
|
23654
|
+
const t = await io.readToken();
|
|
23655
|
+
line(true, "token", t.source === "keychain" ? "macOS keychain" : t.source === "file-override" ? "ARCHME_SANDBOX_CLAUDE_TOKEN_FILE" : "file");
|
|
23656
|
+
} catch (e) {
|
|
23657
|
+
line(false, "token", e.message);
|
|
23658
|
+
}
|
|
23659
|
+
const ours = up ? await ourSlots(io) : /* @__PURE__ */ new Set();
|
|
23660
|
+
const missing = Array.from({ length: MAX_SLOTS }, (_, i2) => i2).filter((i2) => !ours.has(i2)).map((i2) => slotFor(i2).network);
|
|
23661
|
+
line(missing.length === 0, "slots", missing.length ? `missing: ${missing.join(", ")}` : `${MAX_SLOTS} slot networks`);
|
|
23662
|
+
let probeSlot = null;
|
|
23663
|
+
if (up) {
|
|
23664
|
+
const busy = new Set((await io.listBoxes()).map((b) => b.labels[SLOT_LABEL]).filter((x) => !!x));
|
|
23665
|
+
for (let i2 = MAX_SLOTS - 1; i2 >= 0; i2--) {
|
|
23666
|
+
if (ours.has(i2) && !busy.has(String(i2))) {
|
|
23667
|
+
probeSlot = slotFor(i2);
|
|
23668
|
+
break;
|
|
23669
|
+
}
|
|
23670
|
+
}
|
|
23671
|
+
}
|
|
23672
|
+
if (!probeSlot) {
|
|
23673
|
+
line(false, "probe", "not run: no free slot network");
|
|
23674
|
+
} else {
|
|
23675
|
+
const r = await io.probe(probeSlot);
|
|
23676
|
+
line(r.ok, "probe", formatConfinementReport(r));
|
|
23677
|
+
for (const [id, o2] of Object.entries(r.outcomes)) io.print(` ${id.padEnd(15)} ${o2}`);
|
|
23678
|
+
}
|
|
23679
|
+
const { mode, from } = await currentMode(io);
|
|
23680
|
+
io.print(` mode ${mode} (${from})`);
|
|
23681
|
+
return ok ? 0 : 1;
|
|
23682
|
+
}
|
|
23683
|
+
var HARDENING_TEXT, sudoInstall;
|
|
23684
|
+
var init_setupCommand = __esm({
|
|
23685
|
+
"src/daemon/sandbox/setupCommand.ts"() {
|
|
23686
|
+
"use strict";
|
|
23687
|
+
init_appleContainer();
|
|
23688
|
+
init_confinement();
|
|
23689
|
+
init_pfAnchor();
|
|
23690
|
+
init_types();
|
|
23691
|
+
HARDENING_TEXT = [
|
|
23692
|
+
"Host hardening (L2 \xA78.5), by hand:",
|
|
23693
|
+
" - turn off SSH password authentication (PasswordAuthentication no in /etc/ssh/sshd_config)",
|
|
23694
|
+
" - keep AirPlay Receiver off (System Settings \u2192 General \u2192 AirDrop & Handoff)"
|
|
23695
|
+
];
|
|
23696
|
+
sudoInstall = (from, to) => ["install", "-m", "644", "-o", "root", "-g", "wheel", from, to];
|
|
23697
|
+
}
|
|
23698
|
+
});
|
|
23699
|
+
|
|
23700
|
+
// src/daemon/sandbox/setupIo.ts
|
|
23701
|
+
var setupIo_exports = {};
|
|
23702
|
+
__export(setupIo_exports, {
|
|
23703
|
+
createSetupIo: () => createSetupIo
|
|
23704
|
+
});
|
|
23705
|
+
import { mkdtemp as mkdtemp5, readFile as readFile15, writeFile as writeFile14 } from "node:fs/promises";
|
|
23706
|
+
import { networkInterfaces as networkInterfaces2, tmpdir as tmpdir5 } from "node:os";
|
|
23707
|
+
import { join as join25 } from "node:path";
|
|
23708
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
23709
|
+
import { Writable as Writable4 } from "node:stream";
|
|
23710
|
+
function ask(question, hidden2 = false) {
|
|
23711
|
+
let muted = false;
|
|
23712
|
+
const output = new Writable4({
|
|
23713
|
+
write(chunk, _enc, cb) {
|
|
23714
|
+
if (!muted) process.stdout.write(chunk);
|
|
23715
|
+
cb();
|
|
23716
|
+
}
|
|
23717
|
+
});
|
|
23718
|
+
const rl = createInterface2({ input: process.stdin, output, terminal: true });
|
|
23719
|
+
return new Promise((resolve7) => {
|
|
23720
|
+
rl.question(question, (answer) => {
|
|
23721
|
+
rl.close();
|
|
23722
|
+
if (hidden2) process.stdout.write("\n");
|
|
23723
|
+
resolve7(answer);
|
|
23724
|
+
});
|
|
23725
|
+
muted = hidden2;
|
|
23726
|
+
});
|
|
23727
|
+
}
|
|
23728
|
+
function createSetupIo(env = process.env) {
|
|
23729
|
+
let image = null;
|
|
23730
|
+
try {
|
|
23731
|
+
image = resolveSandboxImage(env, () => readShippedImage());
|
|
23732
|
+
} catch {
|
|
23733
|
+
image = null;
|
|
23734
|
+
}
|
|
23735
|
+
const runtime = new AppleContainerRuntime({ image: image ?? void 0 });
|
|
23736
|
+
const token = sandboxTokenLocation(env);
|
|
23737
|
+
let tempDir = null;
|
|
23738
|
+
return {
|
|
23739
|
+
env,
|
|
23740
|
+
image,
|
|
23741
|
+
run: async (cmd, args) => {
|
|
23742
|
+
const r = await execa(cmd, args, { reject: false, stdin: "ignore" });
|
|
23743
|
+
return {
|
|
23744
|
+
exitCode: r.exitCode ?? -1,
|
|
23745
|
+
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
23746
|
+
stderr: typeof r.stderr === "string" ? r.stderr : r.message ?? ""
|
|
23747
|
+
};
|
|
23748
|
+
},
|
|
23749
|
+
// On the terminal: sudo may ask for a password.
|
|
23750
|
+
sudo: async (args) => {
|
|
23751
|
+
const r = await execa("sudo", args, { reject: false, stdio: "inherit" });
|
|
23752
|
+
return { exitCode: r.exitCode ?? -1, stdout: "", stderr: "" };
|
|
23753
|
+
},
|
|
23754
|
+
print: (line) => console.log(line),
|
|
23755
|
+
confirm: async (q) => !/^n/i.test((await ask(`${q} [Y/n] `)).trim()),
|
|
23756
|
+
promptSecret: (q) => ask(q, true),
|
|
23757
|
+
readFile: (p) => readFile15(p, "utf8").catch(() => null),
|
|
23758
|
+
writeTemp: async (name, content) => {
|
|
23759
|
+
tempDir ??= await mkdtemp5(join25(tmpdir5(), "archme-sandbox-setup-"));
|
|
23760
|
+
const p = join25(tempDir, name);
|
|
23761
|
+
await writeFile14(p, content, { mode: 420 });
|
|
23762
|
+
return p;
|
|
23763
|
+
},
|
|
23764
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
23765
|
+
listNetworks: () => runtime.listNetworks(),
|
|
23766
|
+
ensureSlots: async (n2) => {
|
|
23767
|
+
await runtime.ensureSlots(n2);
|
|
23768
|
+
},
|
|
23769
|
+
listBoxes: () => runtime.list(),
|
|
23770
|
+
readToken: () => readSandboxClaudeToken({ claudeTokenPath: token.path, claudeTokenOverride: token.override }),
|
|
23771
|
+
storeToken: (t) => storeSandboxClaudeToken(t, { path: token.path, override: token.override }),
|
|
23772
|
+
probe: async (slot) => {
|
|
23773
|
+
if (!image) return { ok: false, slot: slot.index, failures: ["no sandbox image"], outcomes: {}, ms: 0 };
|
|
23774
|
+
const gates = new GatekeeperSet({
|
|
23775
|
+
slots: Array.from({ length: MAX_SLOTS }, (_, i2) => slotFor(i2)),
|
|
23776
|
+
getClaudeToken: () => "status-probe-has-no-token",
|
|
23777
|
+
mcpUpstream: "http://127.0.0.1:9/functions/v1/mcp",
|
|
23778
|
+
remint: async () => {
|
|
23779
|
+
throw new Error("no job");
|
|
23780
|
+
},
|
|
23781
|
+
logger: () => {
|
|
23782
|
+
}
|
|
23783
|
+
});
|
|
23784
|
+
let own = false;
|
|
23785
|
+
try {
|
|
23786
|
+
await gates.start();
|
|
23787
|
+
own = true;
|
|
23788
|
+
} catch {
|
|
23789
|
+
console.log(" (the daemon's gatekeepers are answering)");
|
|
23790
|
+
}
|
|
23791
|
+
try {
|
|
23792
|
+
return await runConfinementProbe({ runtime, image }, slot, primaryLanIpv4(networkInterfaces2()));
|
|
23793
|
+
} finally {
|
|
23794
|
+
if (own) await gates.stop();
|
|
23795
|
+
}
|
|
23796
|
+
},
|
|
23797
|
+
unitEnv: async () => {
|
|
23798
|
+
if (process.platform !== "darwin") return null;
|
|
23799
|
+
const plist = await readFile15(resolveUnitPath("darwin", env), "utf8").catch(() => null);
|
|
23800
|
+
return plist === null ? null : bakedUnitEnv(plist);
|
|
23801
|
+
}
|
|
23802
|
+
};
|
|
23803
|
+
}
|
|
23804
|
+
var init_setupIo = __esm({
|
|
23805
|
+
"src/daemon/sandbox/setupIo.ts"() {
|
|
23806
|
+
"use strict";
|
|
23807
|
+
init_execa();
|
|
23808
|
+
init_paths();
|
|
23809
|
+
init_appleContainer();
|
|
23810
|
+
init_claudeToken();
|
|
23811
|
+
init_confinement();
|
|
23812
|
+
init_imageManifest();
|
|
23813
|
+
init_pfAnchor();
|
|
23814
|
+
init_setting();
|
|
23815
|
+
init_slots();
|
|
23816
|
+
init_types();
|
|
23817
|
+
}
|
|
23818
|
+
});
|
|
23819
|
+
|
|
22782
23820
|
// src/daemon/service/dispatch.ts
|
|
22783
23821
|
var dispatch_exports2 = {};
|
|
22784
23822
|
__export(dispatch_exports2, {
|
|
22785
23823
|
dispatchDaemon: () => dispatchDaemon
|
|
22786
23824
|
});
|
|
22787
23825
|
import { rm as rm9 } from "node:fs/promises";
|
|
22788
|
-
async function dispatchDaemon(route2) {
|
|
23826
|
+
async function dispatchDaemon(route2, deps = {}) {
|
|
23827
|
+
const { platform: platform3, env } = deps;
|
|
23828
|
+
const controller = () => deps.controller ?? pickController(platform3, env);
|
|
22789
23829
|
switch (route2.action) {
|
|
22790
23830
|
case "help":
|
|
22791
23831
|
printDaemonHelp();
|
|
@@ -22803,7 +23843,7 @@ async function dispatchDaemon(route2) {
|
|
|
22803
23843
|
}
|
|
22804
23844
|
case "start": {
|
|
22805
23845
|
try {
|
|
22806
|
-
const out = await runStartFlow({ noLogin: route2.noLogin });
|
|
23846
|
+
const out = await runStartFlow({ noLogin: route2.noLogin, controller: controller(), platform: platform3, env });
|
|
22807
23847
|
console.log(
|
|
22808
23848
|
`${out.result.actions.includes("installed") ? "Installing " : out.result.actions.includes("reinstalled") ? "Reinstalling " : ""}service\u2026`
|
|
22809
23849
|
);
|
|
@@ -22816,58 +23856,82 @@ async function dispatchDaemon(route2) {
|
|
|
22816
23856
|
console.error(String(err4.message));
|
|
22817
23857
|
return 75;
|
|
22818
23858
|
}
|
|
23859
|
+
if (err4 instanceof CredentialStoreUnreadableError) {
|
|
23860
|
+
console.error(err4.message);
|
|
23861
|
+
console.error(
|
|
23862
|
+
"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."
|
|
23863
|
+
);
|
|
23864
|
+
return 1;
|
|
23865
|
+
}
|
|
22819
23866
|
console.error(String(err4));
|
|
22820
23867
|
return 1;
|
|
22821
23868
|
}
|
|
22822
23869
|
}
|
|
22823
23870
|
case "stop": {
|
|
22824
|
-
const c3 =
|
|
23871
|
+
const c3 = controller();
|
|
22825
23872
|
await orchestrateStop(c3);
|
|
22826
23873
|
console.log("Stopped.");
|
|
22827
23874
|
return 0;
|
|
22828
23875
|
}
|
|
22829
23876
|
case "restart": {
|
|
22830
23877
|
try {
|
|
22831
|
-
await ensureStartPrerequisites({ noLogin: route2.noLogin });
|
|
23878
|
+
await ensureStartPrerequisites({ noLogin: route2.noLogin, controller: controller(), platform: platform3, env });
|
|
22832
23879
|
} catch (err4) {
|
|
22833
23880
|
if (err4 instanceof NotSignedInError) {
|
|
22834
23881
|
console.error(String(err4.message));
|
|
22835
23882
|
console.error("(daemon left running unchanged)");
|
|
22836
23883
|
return 75;
|
|
22837
23884
|
}
|
|
22838
|
-
console.error(String(err4));
|
|
23885
|
+
console.error(err4 instanceof CredentialStoreUnreadableError ? err4.message : String(err4));
|
|
22839
23886
|
console.error("(daemon left running unchanged)");
|
|
22840
23887
|
return 1;
|
|
22841
23888
|
}
|
|
22842
|
-
const c3 =
|
|
23889
|
+
const c3 = controller();
|
|
22843
23890
|
await orchestrateStop(c3);
|
|
22844
|
-
const out = await runStartFlow({
|
|
23891
|
+
const out = await runStartFlow({
|
|
23892
|
+
noLogin: route2.noLogin,
|
|
23893
|
+
controller: c3,
|
|
23894
|
+
platform: platform3,
|
|
23895
|
+
env,
|
|
23896
|
+
isSignedIn: async () => true
|
|
23897
|
+
});
|
|
22845
23898
|
console.log(`Restarted (${out.result.actions.join(" \u2192 ")}).`);
|
|
22846
23899
|
return 0;
|
|
22847
23900
|
}
|
|
22848
23901
|
case "status": {
|
|
22849
|
-
const c3 = pickController();
|
|
22850
23902
|
const status = await gatherStatus({
|
|
22851
|
-
controller:
|
|
23903
|
+
controller: controller(),
|
|
22852
23904
|
loadCredentials: defaultLoadCredentials,
|
|
22853
23905
|
loadSupabaseState: defaultLoadSupabaseState,
|
|
22854
|
-
loadUptime: defaultLoadUptime
|
|
23906
|
+
loadUptime: defaultLoadUptime,
|
|
23907
|
+
platform: platform3,
|
|
23908
|
+
env
|
|
22855
23909
|
});
|
|
22856
23910
|
const out = renderStatus(status);
|
|
22857
|
-
process.stdout.write(out.rendered);
|
|
23911
|
+
(deps.stdout ?? process.stdout).write(out.rendered);
|
|
22858
23912
|
return out.exitCode;
|
|
22859
23913
|
}
|
|
23914
|
+
case "sandbox-setup": {
|
|
23915
|
+
const { runSandboxSetup: runSandboxSetup2 } = await Promise.resolve().then(() => (init_setupCommand(), setupCommand_exports));
|
|
23916
|
+
const { createSetupIo: createSetupIo2 } = await Promise.resolve().then(() => (init_setupIo(), setupIo_exports));
|
|
23917
|
+
return await runSandboxSetup2(createSetupIo2(), { yes: route2.yes });
|
|
23918
|
+
}
|
|
23919
|
+
case "sandbox-status": {
|
|
23920
|
+
const { runSandboxStatus: runSandboxStatus2 } = await Promise.resolve().then(() => (init_setupCommand(), setupCommand_exports));
|
|
23921
|
+
const { createSetupIo: createSetupIo2 } = await Promise.resolve().then(() => (init_setupIo(), setupIo_exports));
|
|
23922
|
+
return await runSandboxStatus2(createSetupIo2());
|
|
23923
|
+
}
|
|
22860
23924
|
case "logs":
|
|
22861
23925
|
await tailLog({ lines: route2.lines, follow: route2.follow });
|
|
22862
23926
|
return 0;
|
|
22863
23927
|
case "enable": {
|
|
22864
|
-
const c3 =
|
|
23928
|
+
const c3 = controller();
|
|
22865
23929
|
await c3.enable();
|
|
22866
23930
|
console.log("Enabled at login.");
|
|
22867
23931
|
return 0;
|
|
22868
23932
|
}
|
|
22869
23933
|
case "disable": {
|
|
22870
|
-
const c3 =
|
|
23934
|
+
const c3 = controller();
|
|
22871
23935
|
await c3.disable();
|
|
22872
23936
|
console.log("Disabled at login. Existing instance keeps running until `archme daemon stop`.");
|
|
22873
23937
|
return 0;
|
|
@@ -22878,7 +23942,7 @@ async function dispatchDaemon(route2) {
|
|
|
22878
23942
|
return 0;
|
|
22879
23943
|
}
|
|
22880
23944
|
case "uninstall": {
|
|
22881
|
-
const c3 =
|
|
23945
|
+
const c3 = controller();
|
|
22882
23946
|
await orchestrateStop(c3);
|
|
22883
23947
|
await c3.uninstall();
|
|
22884
23948
|
if (route2.purge) {
|
|
@@ -22900,7 +23964,13 @@ async function defaultLoadCredentials() {
|
|
|
22900
23964
|
return null;
|
|
22901
23965
|
}
|
|
22902
23966
|
const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
22903
|
-
|
|
23967
|
+
let credentials;
|
|
23968
|
+
try {
|
|
23969
|
+
({ credentials } = await loadCredentials2(supabaseUrl));
|
|
23970
|
+
} catch (err4) {
|
|
23971
|
+
if (err4 instanceof CredentialStoreUnreadableError) return { unreadable: err4.message };
|
|
23972
|
+
throw err4;
|
|
23973
|
+
}
|
|
22904
23974
|
if (!credentials) return null;
|
|
22905
23975
|
const email = decodeJwtEmail(credentials.access_token) ?? credentials.user_id ?? null;
|
|
22906
23976
|
return { email, deviceId: credentials.device_id ?? null };
|
|
@@ -22923,8 +23993,8 @@ async function defaultLoadSupabaseState(_deviceId) {
|
|
|
22923
23993
|
async function defaultLoadUptime(pid) {
|
|
22924
23994
|
if (process.platform === "linux") {
|
|
22925
23995
|
try {
|
|
22926
|
-
const { readFile:
|
|
22927
|
-
const stat12 = await
|
|
23996
|
+
const { readFile: readFile20 } = await import("node:fs/promises");
|
|
23997
|
+
const stat12 = await readFile20(`/proc/${pid}/stat`, "utf8");
|
|
22928
23998
|
const parts = stat12.split(") ").pop()?.split(" ") ?? [];
|
|
22929
23999
|
const ticks = Number(parts[19]);
|
|
22930
24000
|
if (!Number.isFinite(ticks)) return null;
|
|
@@ -22967,6 +24037,8 @@ Usage:
|
|
|
22967
24037
|
archme daemon install [--no-login] Write unit file, do not load
|
|
22968
24038
|
archme daemon uninstall [--purge] Unload + remove unit (logs kept; --purge removes them too)
|
|
22969
24039
|
archme daemon run Foreground worker (what the service unit invokes)
|
|
24040
|
+
archme daemon sandbox setup [--yes] Once per machine: image, Claude token, slot networks, pf (sudo)
|
|
24041
|
+
archme daemon sandbox status Check the sandbox confinement holds (runs a probe box)
|
|
22970
24042
|
`);
|
|
22971
24043
|
}
|
|
22972
24044
|
var init_dispatch2 = __esm({
|
|
@@ -22977,6 +24049,7 @@ var init_dispatch2 = __esm({
|
|
|
22977
24049
|
init_logs();
|
|
22978
24050
|
init_status();
|
|
22979
24051
|
init_paths();
|
|
24052
|
+
init_credentials();
|
|
22980
24053
|
}
|
|
22981
24054
|
});
|
|
22982
24055
|
|
|
@@ -23035,6 +24108,7 @@ async function createAuthedClient(opts) {
|
|
|
23035
24108
|
try {
|
|
23036
24109
|
loaded = await loadStoredAndRefresh();
|
|
23037
24110
|
} catch (err4) {
|
|
24111
|
+
if (err4 instanceof CredentialStoreError) throw err4;
|
|
23038
24112
|
const msg = err4 instanceof Error ? err4.message : String(err4);
|
|
23039
24113
|
throw new Error(`${msg}
|
|
23040
24114
|
\u2192 run \`archme login\` first`);
|
|
@@ -23063,6 +24137,7 @@ var init_authedClient = __esm({
|
|
|
23063
24137
|
"use strict";
|
|
23064
24138
|
init_bearer();
|
|
23065
24139
|
init_identity();
|
|
24140
|
+
init_credentials();
|
|
23066
24141
|
SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
|
|
23067
24142
|
SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
|
|
23068
24143
|
ORCHESTRATED_SCAN_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -23114,8 +24189,8 @@ __export(open_exports, {
|
|
|
23114
24189
|
runOpen: () => runOpen,
|
|
23115
24190
|
safeNextPath: () => safeNextPath
|
|
23116
24191
|
});
|
|
23117
|
-
import { readFile as
|
|
23118
|
-
import { join as
|
|
24192
|
+
import { readFile as readFile16 } from "node:fs/promises";
|
|
24193
|
+
import { join as join26 } from "node:path";
|
|
23119
24194
|
function safeNextPath(path7) {
|
|
23120
24195
|
if (!path7 || !path7.startsWith("/") || path7.startsWith("//") || path7.startsWith("/\\")) return "/";
|
|
23121
24196
|
return path7;
|
|
@@ -23202,7 +24277,7 @@ async function defaultOpenBrowser(url) {
|
|
|
23202
24277
|
}
|
|
23203
24278
|
async function defaultReadMcpJson(cwd) {
|
|
23204
24279
|
try {
|
|
23205
|
-
return await
|
|
24280
|
+
return await readFile16(join26(cwd, ".mcp.json"), "utf8");
|
|
23206
24281
|
} catch {
|
|
23207
24282
|
return null;
|
|
23208
24283
|
}
|
|
@@ -23299,7 +24374,14 @@ function pickKey(keys, handle, active) {
|
|
|
23299
24374
|
}
|
|
23300
24375
|
async function runKey(cmd, deps) {
|
|
23301
24376
|
const keys = await deps.listKeys();
|
|
23302
|
-
|
|
24377
|
+
let active;
|
|
24378
|
+
try {
|
|
24379
|
+
active = await deps.loadActive();
|
|
24380
|
+
} catch (err4) {
|
|
24381
|
+
if (!(err4 instanceof CredentialStoreError)) throw err4;
|
|
24382
|
+
deps.error(`${err4.message} Which key is signed in can't be shown.`);
|
|
24383
|
+
active = null;
|
|
24384
|
+
}
|
|
23303
24385
|
if (cmd.action === "list") {
|
|
23304
24386
|
if (keys.length === 0) {
|
|
23305
24387
|
deps.log("No saved keys on this machine.");
|
|
@@ -23369,6 +24451,7 @@ Usage: archme key [list|show|forget] [--handle <handle>] [--yes]`);
|
|
|
23369
24451
|
var init_key = __esm({
|
|
23370
24452
|
"src/account/key.ts"() {
|
|
23371
24453
|
"use strict";
|
|
24454
|
+
init_credentials();
|
|
23372
24455
|
}
|
|
23373
24456
|
});
|
|
23374
24457
|
|
|
@@ -23562,8 +24645,8 @@ var init_transcript_signals = __esm({
|
|
|
23562
24645
|
});
|
|
23563
24646
|
|
|
23564
24647
|
// src/scan/report.ts
|
|
23565
|
-
import { mkdir as
|
|
23566
|
-
import { dirname as
|
|
24648
|
+
import { mkdir as mkdir15, writeFile as writeFile15 } from "node:fs/promises";
|
|
24649
|
+
import { dirname as dirname17 } from "node:path";
|
|
23567
24650
|
async function buildReport(input) {
|
|
23568
24651
|
return {
|
|
23569
24652
|
schema_version: REPORT_SCHEMA_VERSION,
|
|
@@ -23597,10 +24680,10 @@ async function buildReport(input) {
|
|
|
23597
24680
|
};
|
|
23598
24681
|
}
|
|
23599
24682
|
async function writeReport(reportPath, orphansTxtPath, report, orphansFull) {
|
|
23600
|
-
await
|
|
23601
|
-
await
|
|
23602
|
-
await
|
|
23603
|
-
await
|
|
24683
|
+
await mkdir15(dirname17(reportPath), { recursive: true });
|
|
24684
|
+
await writeFile15(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
24685
|
+
await mkdir15(dirname17(orphansTxtPath), { recursive: true });
|
|
24686
|
+
await writeFile15(
|
|
23604
24687
|
orphansTxtPath,
|
|
23605
24688
|
orphansFull.join("\n") + (orphansFull.length ? "\n" : ""),
|
|
23606
24689
|
"utf8"
|
|
@@ -23644,8 +24727,8 @@ var init_claimDefaultProject = __esm({
|
|
|
23644
24727
|
// src/scan/run-scan.ts
|
|
23645
24728
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
23646
24729
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
23647
|
-
import { mkdir as
|
|
23648
|
-
import { basename as basename2, dirname as
|
|
24730
|
+
import { mkdir as mkdir16, readFile as readFile17, writeFile as writeFile16 } from "node:fs/promises";
|
|
24731
|
+
import { basename as basename2, dirname as dirname18, join as join27 } from "node:path";
|
|
23649
24732
|
function modeInstruction(opts) {
|
|
23650
24733
|
const line = modeLine(opts);
|
|
23651
24734
|
if (opts.mode === "orchestrator" && opts.plan) {
|
|
@@ -23701,13 +24784,13 @@ Use scan-prompt variant \`${promptVariant}\` \u2014 pass \`variant: "${promptVar
|
|
|
23701
24784
|
return base;
|
|
23702
24785
|
}
|
|
23703
24786
|
async function writeScanSession(repoPath, session) {
|
|
23704
|
-
const path7 =
|
|
23705
|
-
await
|
|
23706
|
-
await
|
|
24787
|
+
const path7 = join27(repoPath, SCAN_SESSION_FILE);
|
|
24788
|
+
await mkdir16(dirname18(path7), { recursive: true });
|
|
24789
|
+
await writeFile16(path7, JSON.stringify(session, null, 2) + "\n");
|
|
23707
24790
|
}
|
|
23708
24791
|
async function readScanSession(repoPath) {
|
|
23709
|
-
const path7 =
|
|
23710
|
-
const raw = await
|
|
24792
|
+
const path7 = join27(repoPath, SCAN_SESSION_FILE);
|
|
24793
|
+
const raw = await readFile17(path7, "utf8").catch(() => null);
|
|
23711
24794
|
if (raw === null) throw new Error(`no scan session to resume at ${path7} \u2014 launch it instead`);
|
|
23712
24795
|
let parsed;
|
|
23713
24796
|
try {
|
|
@@ -24098,9 +25181,9 @@ async function runScan(opts, deps = {}) {
|
|
|
24098
25181
|
// Incompatible with an interactive TTY session.
|
|
24099
25182
|
streamJson: !interactive
|
|
24100
25183
|
});
|
|
24101
|
-
const transcriptDir =
|
|
24102
|
-
const transcriptPath =
|
|
24103
|
-
await
|
|
25184
|
+
const transcriptDir = join27(opts.repoPath, ".archme", "scan");
|
|
25185
|
+
const transcriptPath = join27(transcriptDir, "transcript.log");
|
|
25186
|
+
await mkdir16(transcriptDir, { recursive: true });
|
|
24104
25187
|
const transcriptStream = createWriteStream2(transcriptPath, { flags: opts.resume ? "a" : "w" });
|
|
24105
25188
|
let finalResultText = "";
|
|
24106
25189
|
const echo = opts.echo ?? ((line) => process.stdout.write(line + "\n"));
|
|
@@ -24143,9 +25226,9 @@ async function runScan(opts, deps = {}) {
|
|
|
24143
25226
|
await new Promise((resolve7, reject) => {
|
|
24144
25227
|
transcriptStream.end((err4) => err4 ? reject(err4) : resolve7());
|
|
24145
25228
|
});
|
|
24146
|
-
const reportPath =
|
|
24147
|
-
const orphansTxtPath =
|
|
24148
|
-
const transcript = await
|
|
25229
|
+
const reportPath = join27(transcriptDir, "report.json");
|
|
25230
|
+
const orphansTxtPath = join27(transcriptDir, "orphans.txt");
|
|
25231
|
+
const transcript = await readFile17(transcriptPath, "utf8").catch(() => "");
|
|
24149
25232
|
signals2 = extractTranscriptSignals(transcript);
|
|
24150
25233
|
const architecture = await fetchArchitectureDetail(supabase, resolved2.projectId, branchName2);
|
|
24151
25234
|
const orphans = await classifyOrphans({
|
|
@@ -24299,7 +25382,7 @@ var init_scanOutcome = __esm({
|
|
|
24299
25382
|
|
|
24300
25383
|
// src/init/repoSize.ts
|
|
24301
25384
|
import { stat as stat9 } from "node:fs/promises";
|
|
24302
|
-
import { extname, join as
|
|
25385
|
+
import { extname, join as join28 } from "node:path";
|
|
24303
25386
|
async function measureRepo(repoPath) {
|
|
24304
25387
|
const files = await gitListFiles(repoPath);
|
|
24305
25388
|
if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
|
|
@@ -24308,7 +25391,7 @@ async function measureRepo(repoPath) {
|
|
|
24308
25391
|
for (const rel of files) {
|
|
24309
25392
|
if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
|
|
24310
25393
|
try {
|
|
24311
|
-
const s = await stat9(
|
|
25394
|
+
const s = await stat9(join28(repoPath, rel));
|
|
24312
25395
|
if (!s.isFile()) continue;
|
|
24313
25396
|
sourceFiles++;
|
|
24314
25397
|
sourceBytes += s.size;
|
|
@@ -24944,10 +26027,10 @@ __export(localWatch_exports, {
|
|
|
24944
26027
|
watchLocalRepoRow: () => watchLocalRepoRow
|
|
24945
26028
|
});
|
|
24946
26029
|
import { homedir as homedir9 } from "node:os";
|
|
24947
|
-
import { join as
|
|
26030
|
+
import { join as join29, resolve as resolve2 } from "node:path";
|
|
24948
26031
|
import { stat as stat10 } from "node:fs/promises";
|
|
24949
26032
|
function mirrorPathFor(cloneBaseDir, owner, name) {
|
|
24950
|
-
return
|
|
26033
|
+
return join29(cloneBaseDir, owner, name);
|
|
24951
26034
|
}
|
|
24952
26035
|
function resolveInitTarget(input) {
|
|
24953
26036
|
if (!input.originUrl) {
|
|
@@ -25119,7 +26202,7 @@ async function registerLocalWatch(args) {
|
|
|
25119
26202
|
if (!watched.ok) return { ok: false, reason: watched.reason, exitCode: 3 };
|
|
25120
26203
|
const { watchedRepoId } = watched;
|
|
25121
26204
|
const mirrorPath = mirrorPathFor(cloneBaseDir, target.repoOwner, target.repoName);
|
|
25122
|
-
const mirrorExists = await stat10(
|
|
26205
|
+
const mirrorExists = await stat10(join29(mirrorPath, ".git")).then(() => true).catch(() => false);
|
|
25123
26206
|
if (mirrorExists) {
|
|
25124
26207
|
log(` mirror: ${mirrorPath} (reusing)`);
|
|
25125
26208
|
await setOriginUrl(mirrorPath, originUrl);
|
|
@@ -25266,7 +26349,7 @@ var init_localWatch = __esm({
|
|
|
25266
26349
|
init_localWatchResolve();
|
|
25267
26350
|
init_prompt();
|
|
25268
26351
|
init_localPrSetup();
|
|
25269
|
-
DEFAULT_CLONE_BASE_DIR2 =
|
|
26352
|
+
DEFAULT_CLONE_BASE_DIR2 = join29(homedir9(), ".archme-job-runner", "clones");
|
|
25270
26353
|
}
|
|
25271
26354
|
});
|
|
25272
26355
|
|
|
@@ -25299,9 +26382,9 @@ var init_plan = __esm({
|
|
|
25299
26382
|
});
|
|
25300
26383
|
|
|
25301
26384
|
// src/init/run-init.ts
|
|
25302
|
-
import { mkdtemp as
|
|
25303
|
-
import { tmpdir as
|
|
25304
|
-
import { join as
|
|
26385
|
+
import { mkdtemp as mkdtemp6, rm as rm10, writeFile as writeFile17 } from "node:fs/promises";
|
|
26386
|
+
import { tmpdir as tmpdir6 } from "node:os";
|
|
26387
|
+
import { join as join30 } from "node:path";
|
|
25305
26388
|
async function defaultStartDaemon() {
|
|
25306
26389
|
const { runStartFlow: runStartFlow2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
25307
26390
|
const out = await runStartFlow2({ noLogin: true });
|
|
@@ -25346,10 +26429,10 @@ async function defaultFetchInstallScript(url) {
|
|
|
25346
26429
|
return await resp.text();
|
|
25347
26430
|
}
|
|
25348
26431
|
async function defaultRunInstallScript(script, cwd) {
|
|
25349
|
-
const dir = await
|
|
25350
|
-
const scriptPath =
|
|
26432
|
+
const dir = await mkdtemp6(join30(tmpdir6(), "archme-init-"));
|
|
26433
|
+
const scriptPath = join30(dir, "install-agent.sh");
|
|
25351
26434
|
try {
|
|
25352
|
-
await
|
|
26435
|
+
await writeFile17(scriptPath, script, { mode: 448 });
|
|
25353
26436
|
await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
|
|
25354
26437
|
} finally {
|
|
25355
26438
|
await rm10(dir, { recursive: true, force: true });
|
|
@@ -26895,11 +27978,11 @@ var init_dispatch3 = __esm({
|
|
|
26895
27978
|
// src/init/prReviews.ts
|
|
26896
27979
|
async function offerPrReviewsForInit(ctx, flag, deps = {}) {
|
|
26897
27980
|
const readMode = deps.readMode ?? readPrReviewMode;
|
|
26898
|
-
const
|
|
27981
|
+
const ask2 = deps.ask ?? askForPrReviews;
|
|
26899
27982
|
const setMode = deps.setMode ?? setPrReviewMode;
|
|
26900
27983
|
const ghLogin = deps.ghLogin ?? ghLoginPrompt;
|
|
26901
27984
|
const current = await readMode(ctx.supabase, ctx.watchedRepoId);
|
|
26902
|
-
const want = await
|
|
27985
|
+
const want = await ask2({ flag, interactive: ctx.interactive, current });
|
|
26903
27986
|
if (want === null) {
|
|
26904
27987
|
if (current === "auto") ctx.log(`\u2713 ${PR_REVIEWS_ALREADY_ON}`);
|
|
26905
27988
|
return;
|
|
@@ -27325,7 +28408,7 @@ __export(run_uninstall_exports, {
|
|
|
27325
28408
|
runUninstall: () => runUninstall
|
|
27326
28409
|
});
|
|
27327
28410
|
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
27328
|
-
import { join as
|
|
28411
|
+
import { join as join31 } from "node:path";
|
|
27329
28412
|
function readIfExists(path7) {
|
|
27330
28413
|
try {
|
|
27331
28414
|
return existsSync3(path7) ? readFileSync6(path7, "utf8") : null;
|
|
@@ -27351,7 +28434,7 @@ async function runUninstall(opts) {
|
|
|
27351
28434
|
const backend = opts.backend ?? await defaultBackend();
|
|
27352
28435
|
const repoPath = await backend.gitRoot(opts.repoPath).catch(() => null) ?? opts.repoPath;
|
|
27353
28436
|
if (repoPath !== opts.repoPath) log(`Using the repo root: ${repoPath}`);
|
|
27354
|
-
const abs = (rel) =>
|
|
28437
|
+
const abs = (rel) => join31(repoPath, rel);
|
|
27355
28438
|
const actions = [];
|
|
27356
28439
|
const skipped = [];
|
|
27357
28440
|
const mcpRaw = readIfExists(abs(".mcp.json"));
|
|
@@ -27931,7 +29014,7 @@ __export(run_exports3, {
|
|
|
27931
29014
|
watchedElsewhere: () => watchedElsewhere
|
|
27932
29015
|
});
|
|
27933
29016
|
import { readdir as readdir3, stat as stat11 } from "node:fs/promises";
|
|
27934
|
-
import { dirname as
|
|
29017
|
+
import { dirname as dirname19, join as join32, resolve as resolve6, sep as sep2 } from "node:path";
|
|
27935
29018
|
function isThisRunsWrite(metadata) {
|
|
27936
29019
|
return metadata?.kind === "scan" || !!metadata?.repo_fold;
|
|
27937
29020
|
}
|
|
@@ -27961,12 +29044,12 @@ async function listClones(dir) {
|
|
|
27961
29044
|
const out = [];
|
|
27962
29045
|
for (const e of entries) {
|
|
27963
29046
|
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
27964
|
-
if (await stat11(
|
|
29047
|
+
if (await stat11(join32(dir, e.name, ".git")).then(() => true, () => false)) out.push(join32(dir, e.name));
|
|
27965
29048
|
}
|
|
27966
29049
|
return out.sort();
|
|
27967
29050
|
}
|
|
27968
29051
|
function commonDir(paths) {
|
|
27969
|
-
const split = paths.map((p) =>
|
|
29052
|
+
const split = paths.map((p) => dirname19(p).split(sep2));
|
|
27970
29053
|
const common2 = [];
|
|
27971
29054
|
for (let i2 = 0; split.every((s) => s[i2] !== void 0 && s[i2] === split[0][i2]); i2++) common2.push(split[0][i2]);
|
|
27972
29055
|
return common2.join(sep2) || sep2;
|
|
@@ -28001,7 +29084,7 @@ function buildRunSheet(opts) {
|
|
|
28001
29084
|
`- Clone: \`${r.root}\``,
|
|
28002
29085
|
`- Launch: \`${launch(r)}\``,
|
|
28003
29086
|
`- Resume: \`${launch(r)} --resume --message "<answer or correction>"\``,
|
|
28004
|
-
`- Session file: \`${
|
|
29087
|
+
`- Session file: \`${join32(r.root, SCAN_SESSION_FILE)}\``
|
|
28005
29088
|
])
|
|
28006
29089
|
];
|
|
28007
29090
|
if (opts.context.length > 0) {
|
|
@@ -28050,7 +29133,7 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
28050
29133
|
roots.push(found.root);
|
|
28051
29134
|
continue;
|
|
28052
29135
|
}
|
|
28053
|
-
const dest =
|
|
29136
|
+
const dest = join32(cwd, arg.split("/")[1]);
|
|
28054
29137
|
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
29138
|
log(`cloning ${arg} into ${dest}`);
|
|
28056
29139
|
const cloned = await (deps.cloneRepo ?? cloneFromGitHub)(arg, dest);
|
|
@@ -28422,8 +29505,8 @@ var init_dispatch7 = __esm({
|
|
|
28422
29505
|
});
|
|
28423
29506
|
|
|
28424
29507
|
// src/doctor/collect.ts
|
|
28425
|
-
import { readFile as
|
|
28426
|
-
import { join as
|
|
29508
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
29509
|
+
import { join as join33 } from "node:path";
|
|
28427
29510
|
function binaryPresent(name) {
|
|
28428
29511
|
try {
|
|
28429
29512
|
requireBinary(name);
|
|
@@ -28439,7 +29522,7 @@ async function git3(cwd, args) {
|
|
|
28439
29522
|
async function readMcpPin(repoRoot) {
|
|
28440
29523
|
const empty = { installed: false, pin: { project: null, branch: null } };
|
|
28441
29524
|
try {
|
|
28442
|
-
const raw = await
|
|
29525
|
+
const raw = await readFile18(join33(repoRoot, ".mcp.json"), "utf8");
|
|
28443
29526
|
const parsed = JSON.parse(raw);
|
|
28444
29527
|
const servers = parsed.mcpServers ?? {};
|
|
28445
29528
|
const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
|
|
@@ -28813,9 +29896,9 @@ __export(dispatch_exports8, {
|
|
|
28813
29896
|
parseDoctorArgs: () => parseDoctorArgs,
|
|
28814
29897
|
printDoctorHelp: () => printDoctorHelp
|
|
28815
29898
|
});
|
|
28816
|
-
import { mkdtemp as
|
|
28817
|
-
import { tmpdir as
|
|
28818
|
-
import { join as
|
|
29899
|
+
import { mkdtemp as mkdtemp7, rm as rm11, writeFile as writeFile18 } from "node:fs/promises";
|
|
29900
|
+
import { tmpdir as tmpdir7 } from "node:os";
|
|
29901
|
+
import { join as join34 } from "node:path";
|
|
28819
29902
|
function parseDoctorArgs(argv) {
|
|
28820
29903
|
const args = { plain: false, help: false };
|
|
28821
29904
|
for (const a2 of argv) {
|
|
@@ -28876,9 +29959,9 @@ async function dispatchDoctor(argv) {
|
|
|
28876
29959
|
);
|
|
28877
29960
|
return EX_OK6;
|
|
28878
29961
|
}
|
|
28879
|
-
const dir = await
|
|
28880
|
-
const mcpConfigPath =
|
|
28881
|
-
await
|
|
29962
|
+
const dir = await mkdtemp7(join34(tmpdir7(), "archme-doctor-"));
|
|
29963
|
+
const mcpConfigPath = join34(dir, "mcp.json");
|
|
29964
|
+
await writeFile18(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
|
|
28882
29965
|
try {
|
|
28883
29966
|
const result = await spawnClaude({
|
|
28884
29967
|
prompt: buildDoctorPrompt(facts, issues),
|
|
@@ -29045,7 +30128,7 @@ var init_conciergeToken = __esm({
|
|
|
29045
30128
|
});
|
|
29046
30129
|
|
|
29047
30130
|
// src/daemon/sandboxJob/input.ts
|
|
29048
|
-
import { readFile as
|
|
30131
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
29049
30132
|
function isObj2(v) {
|
|
29050
30133
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
29051
30134
|
}
|
|
@@ -29154,6 +30237,31 @@ function parseRepoScope(raw) {
|
|
|
29154
30237
|
}
|
|
29155
30238
|
fail("repoScope must be { kind: all | container | excluding }");
|
|
29156
30239
|
}
|
|
30240
|
+
function parseProbe(data) {
|
|
30241
|
+
const timeoutMs = data.timeoutMs;
|
|
30242
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) fail("timeoutMs must be a positive integer");
|
|
30243
|
+
if (!Array.isArray(data.targets) || data.targets.length > 64) fail("targets must be an array of at most 64");
|
|
30244
|
+
const targets = data.targets.map((raw, i2) => {
|
|
30245
|
+
const where = `targets[${i2}]`;
|
|
30246
|
+
if (!isObj2(raw)) fail(`${where} must be an object`);
|
|
30247
|
+
const id = str3(raw, "id", where);
|
|
30248
|
+
if (!PROBE_ID_RE.test(id)) fail(`${where}.id must be [a-z0-9_]`);
|
|
30249
|
+
const kind = raw.kind;
|
|
30250
|
+
if (!PROBE_KINDS.has(kind)) fail(`${where}.kind is not a probe kind`);
|
|
30251
|
+
const host = str3(raw, "host", where);
|
|
30252
|
+
if (kind === "ipv6_socket" ? host !== "::" : !IPV4_RE2.test(host)) fail(`${where}.host must be an IPv4 address`);
|
|
30253
|
+
const port = raw.port;
|
|
30254
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) fail(`${where}.port is not a port`);
|
|
30255
|
+
const target = { id, kind, host, port };
|
|
30256
|
+
if (raw.authority !== void 0) {
|
|
30257
|
+
const a2 = raw.authority;
|
|
30258
|
+
if (kind !== "connect" || typeof a2 !== "string" || !/^[A-Za-z0-9.\-[\]:]{1,300}$/.test(a2)) fail(`${where}.authority is not host:port`);
|
|
30259
|
+
target.authority = a2;
|
|
30260
|
+
}
|
|
30261
|
+
return target;
|
|
30262
|
+
});
|
|
30263
|
+
return { kind: "probe", targets, timeoutMs };
|
|
30264
|
+
}
|
|
29157
30265
|
function parseInput(raw) {
|
|
29158
30266
|
let data;
|
|
29159
30267
|
try {
|
|
@@ -29162,6 +30270,7 @@ function parseInput(raw) {
|
|
|
29162
30270
|
fail("not valid JSON");
|
|
29163
30271
|
}
|
|
29164
30272
|
if (!isObj2(data)) fail("must be a JSON object");
|
|
30273
|
+
if (data.kind === "probe") return parseProbe(data);
|
|
29165
30274
|
const job = parseJob(data.job);
|
|
29166
30275
|
if (data.kind === "clone") {
|
|
29167
30276
|
const input = {
|
|
@@ -29192,9 +30301,9 @@ function parseInput(raw) {
|
|
|
29192
30301
|
};
|
|
29193
30302
|
return input;
|
|
29194
30303
|
}
|
|
29195
|
-
fail("kind must be clone or
|
|
30304
|
+
fail("kind must be clone, agent or probe");
|
|
29196
30305
|
}
|
|
29197
|
-
async function readInput(path7 = INPUT_PATH, read = (p) =>
|
|
30306
|
+
async function readInput(path7 = INPUT_PATH, read = (p) => readFile19(p, "utf8")) {
|
|
29198
30307
|
let raw;
|
|
29199
30308
|
try {
|
|
29200
30309
|
raw = await read(path7);
|
|
@@ -29203,7 +30312,7 @@ async function readInput(path7 = INPUT_PATH, read = (p) => readFile18(p, "utf8")
|
|
|
29203
30312
|
}
|
|
29204
30313
|
return parseInput(raw);
|
|
29205
30314
|
}
|
|
29206
|
-
var InputSchemaError, SHA_RE3, REPO_PART_RE, IPV4_RE2, SOURCE_MODES;
|
|
30315
|
+
var InputSchemaError, SHA_RE3, REPO_PART_RE, IPV4_RE2, SOURCE_MODES, PROBE_KINDS, PROBE_ID_RE;
|
|
29207
30316
|
var init_input = __esm({
|
|
29208
30317
|
"src/daemon/sandboxJob/input.ts"() {
|
|
29209
30318
|
"use strict";
|
|
@@ -29218,6 +30327,135 @@ var init_input = __esm({
|
|
|
29218
30327
|
REPO_PART_RE = /^[A-Za-z0-9_.-]+$/;
|
|
29219
30328
|
IPV4_RE2 = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
|
|
29220
30329
|
SOURCE_MODES = /* @__PURE__ */ new Set(["diff", "reconcile", "pr_update"]);
|
|
30330
|
+
PROBE_KINDS = /* @__PURE__ */ new Set(["tcp", "udp_dns", "http", "connect", "ipv6_socket"]);
|
|
30331
|
+
PROBE_ID_RE = /^[a-z0-9_]{1,64}$/;
|
|
30332
|
+
}
|
|
30333
|
+
});
|
|
30334
|
+
|
|
30335
|
+
// src/daemon/sandboxJob/probe.ts
|
|
30336
|
+
import dgram from "node:dgram";
|
|
30337
|
+
import http2 from "node:http";
|
|
30338
|
+
import net2 from "node:net";
|
|
30339
|
+
function tcp(t, timeoutMs) {
|
|
30340
|
+
return new Promise((resolve7) => {
|
|
30341
|
+
const s = net2.connect({ host: t.host, port: t.port });
|
|
30342
|
+
const finish = (outcome) => {
|
|
30343
|
+
s.destroy();
|
|
30344
|
+
resolve7(outcome);
|
|
30345
|
+
};
|
|
30346
|
+
s.setTimeout(timeoutMs, () => finish("timeout"));
|
|
30347
|
+
s.once("connect", () => finish("connected"));
|
|
30348
|
+
s.once("error", (e) => {
|
|
30349
|
+
finish(e.code === "ECONNREFUSED" ? "refused" : e.code === "ETIMEDOUT" ? "timeout" : "unreachable");
|
|
30350
|
+
});
|
|
30351
|
+
});
|
|
30352
|
+
}
|
|
30353
|
+
function udpDns(t, timeoutMs) {
|
|
30354
|
+
return new Promise((resolve7) => {
|
|
30355
|
+
const s = dgram.createSocket("udp4");
|
|
30356
|
+
let done = false;
|
|
30357
|
+
const finish = (outcome) => {
|
|
30358
|
+
if (done) return;
|
|
30359
|
+
done = true;
|
|
30360
|
+
clearTimeout(timer);
|
|
30361
|
+
s.close();
|
|
30362
|
+
resolve7(outcome);
|
|
30363
|
+
};
|
|
30364
|
+
const timer = setTimeout(() => finish("silent"), timeoutMs);
|
|
30365
|
+
s.on("message", () => finish("answered"));
|
|
30366
|
+
s.on("error", () => finish("silent"));
|
|
30367
|
+
s.send(DNS_QUERY, t.port, t.host, (err4) => {
|
|
30368
|
+
if (err4) finish("silent");
|
|
30369
|
+
});
|
|
30370
|
+
});
|
|
30371
|
+
}
|
|
30372
|
+
function httpStatus(t, timeoutMs, method) {
|
|
30373
|
+
return new Promise((resolve7) => {
|
|
30374
|
+
let done = false;
|
|
30375
|
+
const finish = (outcome) => {
|
|
30376
|
+
if (done) return;
|
|
30377
|
+
done = true;
|
|
30378
|
+
req.destroy();
|
|
30379
|
+
resolve7(outcome);
|
|
30380
|
+
};
|
|
30381
|
+
const req = http2.request({
|
|
30382
|
+
host: t.host,
|
|
30383
|
+
port: t.port,
|
|
30384
|
+
method,
|
|
30385
|
+
agent: false,
|
|
30386
|
+
timeout: timeoutMs,
|
|
30387
|
+
path: method === "CONNECT" ? t.authority ?? "github.com:443" : "/"
|
|
30388
|
+
});
|
|
30389
|
+
req.on("response", (res) => {
|
|
30390
|
+
res.resume();
|
|
30391
|
+
finish(String(res.statusCode ?? "none"));
|
|
30392
|
+
});
|
|
30393
|
+
req.on("connect", (res, socket) => {
|
|
30394
|
+
socket.destroy();
|
|
30395
|
+
finish(String(res.statusCode ?? "none"));
|
|
30396
|
+
});
|
|
30397
|
+
req.on("timeout", () => finish("none"));
|
|
30398
|
+
req.on("error", () => finish("none"));
|
|
30399
|
+
req.end();
|
|
30400
|
+
});
|
|
30401
|
+
}
|
|
30402
|
+
function ipv6Socket() {
|
|
30403
|
+
return new Promise((resolve7) => {
|
|
30404
|
+
const s = dgram.createSocket("udp6");
|
|
30405
|
+
s.once("error", () => {
|
|
30406
|
+
s.close();
|
|
30407
|
+
resolve7("unsupported");
|
|
30408
|
+
});
|
|
30409
|
+
s.bind(0, "::", () => {
|
|
30410
|
+
s.close();
|
|
30411
|
+
resolve7("created");
|
|
30412
|
+
});
|
|
30413
|
+
});
|
|
30414
|
+
}
|
|
30415
|
+
async function runProbe(input) {
|
|
30416
|
+
const results = await Promise.all(input.targets.map(async (t) => {
|
|
30417
|
+
switch (t.kind) {
|
|
30418
|
+
case "tcp":
|
|
30419
|
+
return [t.id, await tcp(t, input.timeoutMs)];
|
|
30420
|
+
case "udp_dns":
|
|
30421
|
+
return [t.id, await udpDns(t, input.timeoutMs)];
|
|
30422
|
+
case "http":
|
|
30423
|
+
return [t.id, await httpStatus(t, input.timeoutMs, "GET")];
|
|
30424
|
+
case "connect":
|
|
30425
|
+
return [t.id, await httpStatus(t, input.timeoutMs, "CONNECT")];
|
|
30426
|
+
case "ipv6_socket":
|
|
30427
|
+
return [t.id, await ipv6Socket()];
|
|
30428
|
+
}
|
|
30429
|
+
}));
|
|
30430
|
+
return Object.fromEntries(results);
|
|
30431
|
+
}
|
|
30432
|
+
var DNS_QUERY;
|
|
30433
|
+
var init_probe = __esm({
|
|
30434
|
+
"src/daemon/sandboxJob/probe.ts"() {
|
|
30435
|
+
"use strict";
|
|
30436
|
+
DNS_QUERY = Buffer.from([
|
|
30437
|
+
18,
|
|
30438
|
+
52,
|
|
30439
|
+
1,
|
|
30440
|
+
0,
|
|
30441
|
+
0,
|
|
30442
|
+
1,
|
|
30443
|
+
0,
|
|
30444
|
+
0,
|
|
30445
|
+
0,
|
|
30446
|
+
0,
|
|
30447
|
+
0,
|
|
30448
|
+
0,
|
|
30449
|
+
7,
|
|
30450
|
+
...Buffer.from("example"),
|
|
30451
|
+
3,
|
|
30452
|
+
...Buffer.from("com"),
|
|
30453
|
+
0,
|
|
30454
|
+
0,
|
|
30455
|
+
1,
|
|
30456
|
+
0,
|
|
30457
|
+
1
|
|
30458
|
+
]);
|
|
29221
30459
|
}
|
|
29222
30460
|
});
|
|
29223
30461
|
|
|
@@ -29227,7 +30465,7 @@ __export(main_exports, {
|
|
|
29227
30465
|
readNonceLine: () => readNonceLine,
|
|
29228
30466
|
runSandboxJob: () => runSandboxJob
|
|
29229
30467
|
});
|
|
29230
|
-
import { mkdir as
|
|
30468
|
+
import { mkdir as mkdir17 } from "node:fs/promises";
|
|
29231
30469
|
function readNonceLine(stream = process.stdin) {
|
|
29232
30470
|
return new Promise((resolve7) => {
|
|
29233
30471
|
let buf = "";
|
|
@@ -29252,6 +30490,10 @@ function readNonceLine(stream = process.stdin) {
|
|
|
29252
30490
|
});
|
|
29253
30491
|
}
|
|
29254
30492
|
function emptyResult(step, error) {
|
|
30493
|
+
if (step === "probe") {
|
|
30494
|
+
const r2 = { kind: "probe", outcomes: {}, memoryPeakBytes: null, durations_ms: {}, error };
|
|
30495
|
+
return r2;
|
|
30496
|
+
}
|
|
29255
30497
|
if (step === "clone") {
|
|
29256
30498
|
const r2 = { kind: "clone", outcome: "diff", memoryPeakBytes: null, durations_ms: {}, error };
|
|
29257
30499
|
return r2;
|
|
@@ -29299,8 +30541,8 @@ async function runSandboxJob(argv, deps = {}) {
|
|
|
29299
30541
|
return code;
|
|
29300
30542
|
};
|
|
29301
30543
|
const step = argv[0];
|
|
29302
|
-
if (step !== "clone" && step !== "agent") {
|
|
29303
|
-
const msg = `unknown step ${JSON.stringify(step ?? "")}; expected clone or
|
|
30544
|
+
if (step !== "clone" && step !== "agent" && step !== "probe") {
|
|
30545
|
+
const msg = `unknown step ${JSON.stringify(step ?? "")}; expected clone, agent or probe`;
|
|
29304
30546
|
log(msg);
|
|
29305
30547
|
return emit2(emptyResult("clone", msg), EXIT_INPUT_SCHEMA);
|
|
29306
30548
|
}
|
|
@@ -29314,9 +30556,19 @@ async function runSandboxJob(argv, deps = {}) {
|
|
|
29314
30556
|
return emit2(emptyResult(step, msg), err4 instanceof InputSchemaError ? EXIT_INPUT_SCHEMA : 1);
|
|
29315
30557
|
}
|
|
29316
30558
|
mark("input");
|
|
30559
|
+
if (input.kind === "probe") {
|
|
30560
|
+
try {
|
|
30561
|
+
const outcomes = await (deps.probe ?? runProbe)(input);
|
|
30562
|
+
return emit2({ kind: "probe", outcomes, memoryPeakBytes: null, durations_ms: {} }, 0);
|
|
30563
|
+
} catch (err4) {
|
|
30564
|
+
const msg = err4 instanceof Error ? err4.message : String(err4);
|
|
30565
|
+
log(`failed: ${msg}`);
|
|
30566
|
+
return emit2(emptyResult("probe", msg), 1);
|
|
30567
|
+
}
|
|
30568
|
+
}
|
|
29317
30569
|
log(`${step} job=${input.job.id} repo=${input.job.repo_owner}/${input.job.repo_name} after=${input.job.after_sha.slice(0, 8)}`);
|
|
29318
30570
|
try {
|
|
29319
|
-
await
|
|
30571
|
+
await mkdir17(deps.homeDir ?? process.env.HOME ?? BOX_HOME, { recursive: true, mode: 448 });
|
|
29320
30572
|
if (input.kind === "clone") {
|
|
29321
30573
|
const { result: result2, exitCode: exitCode2 } = await runCloneStep(input, { log, mark, ...deps.clone });
|
|
29322
30574
|
return emit2({ ...result2, memoryPeakBytes: null, durations_ms: {} }, exitCode2);
|
|
@@ -29338,6 +30590,7 @@ var init_main2 = __esm({
|
|
|
29338
30590
|
init_result2();
|
|
29339
30591
|
init_clone();
|
|
29340
30592
|
init_agent();
|
|
30593
|
+
init_probe();
|
|
29341
30594
|
NONCE_LINE_MAX_BYTES = 256;
|
|
29342
30595
|
}
|
|
29343
30596
|
});
|
|
@@ -29416,7 +30669,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
29416
30669
|
"doctor",
|
|
29417
30670
|
// Hidden: operator-only (platform admins), deliberately absent from printHelp.
|
|
29418
30671
|
"concierge-token",
|
|
29419
|
-
// Hidden: the per-job sandbox image's entrypoint (`sandbox-job clone|agent`).
|
|
30672
|
+
// Hidden: the per-job sandbox image's entrypoint (`sandbox-job clone|agent|probe`).
|
|
29420
30673
|
"sandbox-job"
|
|
29421
30674
|
]);
|
|
29422
30675
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v"]);
|
|
@@ -29439,7 +30692,8 @@ var DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
29439
30692
|
"disable",
|
|
29440
30693
|
"install",
|
|
29441
30694
|
"uninstall",
|
|
29442
|
-
"run"
|
|
30695
|
+
"run",
|
|
30696
|
+
"sandbox"
|
|
29443
30697
|
]);
|
|
29444
30698
|
function routeDaemon(args) {
|
|
29445
30699
|
const sub = args[0];
|
|
@@ -29473,6 +30727,11 @@ function routeDaemon(args) {
|
|
|
29473
30727
|
return { action: "uninstall", purge: hasFlag("--purge") };
|
|
29474
30728
|
case "run":
|
|
29475
30729
|
return { action: "run" };
|
|
30730
|
+
case "sandbox": {
|
|
30731
|
+
if (rest[0] === "setup") return { action: "sandbox-setup", yes: rest.includes("--yes") || rest.includes("-y") };
|
|
30732
|
+
if (rest[0] === "status") return { action: "sandbox-status" };
|
|
30733
|
+
return { action: "unknown", arg: ["sandbox", ...rest.slice(0, 1)].join(" ") };
|
|
30734
|
+
}
|
|
29476
30735
|
}
|
|
29477
30736
|
return { action: "unknown", arg: sub };
|
|
29478
30737
|
}
|
|
@@ -29613,4 +30872,10 @@ Run 'archme init --help', 'archme daemon --help', 'archme analyze --help', or
|
|
|
29613
30872
|
}
|
|
29614
30873
|
|
|
29615
30874
|
// src/bin.ts
|
|
29616
|
-
main(process.argv.slice(2)).then(
|
|
30875
|
+
main(process.argv.slice(2)).then(
|
|
30876
|
+
(code) => process.exit(code),
|
|
30877
|
+
(err4) => {
|
|
30878
|
+
console.error(err4 instanceof Error ? err4.message : String(err4));
|
|
30879
|
+
process.exit(1);
|
|
30880
|
+
}
|
|
30881
|
+
);
|