@securecoms/agent-bridge 0.30.0 → 0.30.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/README.md +37 -0
- package/dist/bin.d.ts +82 -0
- package/dist/bin.js +942 -150
- package/dist/bridge-agent.d.ts +60 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.js +295 -21
- package/dist/service/install-prep.d.ts +21 -1
- package/dist/service/instance-lock.d.ts +88 -0
- package/dist/service/launchd.d.ts +1 -1
- package/dist/service/status.d.ts +3 -5
- package/dist/service/systemd.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +4 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -7556,7 +7556,7 @@ __export(import_bundle_exports, {
|
|
|
7556
7556
|
importModelBundle: () => importModelBundle
|
|
7557
7557
|
});
|
|
7558
7558
|
import { createHash as createHash6, randomBytes as randomBytes12 } from "node:crypto";
|
|
7559
|
-
import { readFileSync as
|
|
7559
|
+
import { readFileSync as readFileSync15, statSync as statSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7560
7560
|
import { join as join24 } from "node:path";
|
|
7561
7561
|
async function importModelBundle(srcDir, root3, m) {
|
|
7562
7562
|
const verified = [];
|
|
@@ -7564,7 +7564,7 @@ async function importModelBundle(srcDir, root3, m) {
|
|
|
7564
7564
|
const src = join24(srcDir, f.name);
|
|
7565
7565
|
let size;
|
|
7566
7566
|
try {
|
|
7567
|
-
const s =
|
|
7567
|
+
const s = statSync7(src);
|
|
7568
7568
|
if (!s.isFile()) throw new Error("not a file");
|
|
7569
7569
|
size = s.size;
|
|
7570
7570
|
} catch {
|
|
@@ -7574,7 +7574,7 @@ async function importModelBundle(srcDir, root3, m) {
|
|
|
7574
7574
|
if (size !== f.size) throw mismatch;
|
|
7575
7575
|
let bytes;
|
|
7576
7576
|
try {
|
|
7577
|
-
bytes =
|
|
7577
|
+
bytes = readFileSync15(src);
|
|
7578
7578
|
} catch {
|
|
7579
7579
|
throw new ModelImportError(`${f.name} is missing from the import directory`);
|
|
7580
7580
|
}
|
|
@@ -24803,6 +24803,7 @@ function base64UrlNoPad(s) {
|
|
|
24803
24803
|
}
|
|
24804
24804
|
var BEARER_SUBPROTOCOL = "securecoms.bearer.v1";
|
|
24805
24805
|
var WS_CLOSE_IDENTITY_REVOKED = 4006;
|
|
24806
|
+
var WS_CLOSE_SESSION_SUPERSEDED = 4409;
|
|
24806
24807
|
function isIdentityRevokedClose(code, reason) {
|
|
24807
24808
|
return code === WS_CLOSE_IDENTITY_REVOKED || reason === "identity_revoked";
|
|
24808
24809
|
}
|
|
@@ -25018,7 +25019,7 @@ function openDeliveryPings(o) {
|
|
|
25018
25019
|
end();
|
|
25019
25020
|
};
|
|
25020
25021
|
if (!hardened) {
|
|
25021
|
-
if (code === 4001 || isIdentityRevokedClose(code, reason)) {
|
|
25022
|
+
if (code === 4001 || code === WS_CLOSE_SESSION_SUPERSEDED || isIdentityRevokedClose(code, reason)) {
|
|
25022
25023
|
terminalClose();
|
|
25023
25024
|
return;
|
|
25024
25025
|
}
|
|
@@ -25027,7 +25028,7 @@ function openDeliveryPings(o) {
|
|
|
25027
25028
|
}
|
|
25028
25029
|
const tr = terminalReason;
|
|
25029
25030
|
terminalReason = void 0;
|
|
25030
|
-
if (code === 4006 || tr && code === 4001) {
|
|
25031
|
+
if (code === 4006 || code === WS_CLOSE_SESSION_SUPERSEDED || tr && code === 4001) {
|
|
25031
25032
|
lifecycle("terminal", tr ?? `code ${code}`);
|
|
25032
25033
|
terminalClose();
|
|
25033
25034
|
return;
|
|
@@ -25184,10 +25185,14 @@ var RestTransport = class {
|
|
|
25184
25185
|
f;
|
|
25185
25186
|
ws;
|
|
25186
25187
|
tokenProvider;
|
|
25187
|
-
/** The
|
|
25188
|
-
* cleared
|
|
25189
|
-
* a no-op when no subscription is active. */
|
|
25188
|
+
/** The shared delivery-ping socket's send, set while that socket is open and
|
|
25189
|
+
* cleared when it closes (last subscriber gone, or terminal). Control frames
|
|
25190
|
+
* (status keepalive) route through it; a no-op when no subscription is active. */
|
|
25190
25191
|
_controlSend = null;
|
|
25192
|
+
/** P-5: the one delivery-ping stream this transport holds, shared by every subscriber. */
|
|
25193
|
+
pingHub = null;
|
|
25194
|
+
/** Set once the shared stream ends on a terminal close; no socket is ever reopened. */
|
|
25195
|
+
pingsTerminal = false;
|
|
25191
25196
|
constructor(opts) {
|
|
25192
25197
|
this.opts = opts;
|
|
25193
25198
|
this.f = opts.fetchImpl ?? fetch;
|
|
@@ -25289,25 +25294,52 @@ var RestTransport = class {
|
|
|
25289
25294
|
idempotentReplay: j.idempotent_replay === true
|
|
25290
25295
|
};
|
|
25291
25296
|
}
|
|
25297
|
+
/** One device-wide delivery-ping socket per transport (P-5): the backend
|
|
25298
|
+
* supersedes a second socket for the same device, so every subscriber shares
|
|
25299
|
+
* one `openDeliveryPings` handle. Each gets its own bounded queue; its close()
|
|
25300
|
+
* detaches only itself; the socket closes with the last subscriber and a later
|
|
25301
|
+
* subscribe opens a fresh one. After a terminal close the transport is
|
|
25302
|
+
* terminal: later calls open no socket and get an already-ended iterator. */
|
|
25292
25303
|
subscribeDeliveryPings(_channelId) {
|
|
25304
|
+
if (this.pingsTerminal) {
|
|
25305
|
+
const ended = new PingSubscriber();
|
|
25306
|
+
ended.end();
|
|
25307
|
+
return { pings: ended.pings, close: () => {
|
|
25308
|
+
}, send: () => {
|
|
25309
|
+
} };
|
|
25310
|
+
}
|
|
25311
|
+
const hub = this.pingHub ??= this.openPingHub();
|
|
25312
|
+
const sub = hub.attach();
|
|
25313
|
+
return { pings: sub.pings, close: () => hub.detach(sub), send: hub.send };
|
|
25314
|
+
}
|
|
25315
|
+
openPingHub() {
|
|
25293
25316
|
let wsUrl = this.opts.baseUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/api/v1/ws";
|
|
25294
25317
|
if (this.opts.connectIdentityId) {
|
|
25295
25318
|
wsUrl += `?connect_identity_id=${encodeURIComponent(this.opts.connectIdentityId)}`;
|
|
25296
25319
|
}
|
|
25297
|
-
const
|
|
25320
|
+
const hub = new PingHub(openDeliveryPings({
|
|
25298
25321
|
url: wsUrl,
|
|
25299
25322
|
tokenProvider: this.tokenProvider,
|
|
25300
25323
|
wsImpl: this.ws,
|
|
25301
25324
|
hardened: this.opts.hardenedReconnect ?? false,
|
|
25302
|
-
onLifecycle: this.opts.onWsLifecycle
|
|
25303
|
-
|
|
25304
|
-
|
|
25305
|
-
|
|
25306
|
-
|
|
25307
|
-
this.
|
|
25308
|
-
|
|
25309
|
-
|
|
25310
|
-
|
|
25325
|
+
onLifecycle: this.opts.onWsLifecycle,
|
|
25326
|
+
// Any terminal close (4001, 4006, 4409, hardened war-cap) of the shared
|
|
25327
|
+
// socket makes this transport terminal. The shared iterator ends right
|
|
25328
|
+
// after this call, so the pump exit tears down every subscriber.
|
|
25329
|
+
onTerminalClose: (close) => {
|
|
25330
|
+
this.pingsTerminal = true;
|
|
25331
|
+
this.opts.onTerminalClose?.(close);
|
|
25332
|
+
}
|
|
25333
|
+
}), () => this.dropPingHub(hub));
|
|
25334
|
+
this._controlSend = hub.send;
|
|
25335
|
+
return hub;
|
|
25336
|
+
}
|
|
25337
|
+
dropPingHub(hub) {
|
|
25338
|
+
if (this.pingHub === hub) {
|
|
25339
|
+
this.pingHub = null;
|
|
25340
|
+
this._controlSend = null;
|
|
25341
|
+
}
|
|
25342
|
+
hub.shutdown();
|
|
25311
25343
|
}
|
|
25312
25344
|
sendControlFrame(_channelId, frame) {
|
|
25313
25345
|
try {
|
|
@@ -25335,6 +25367,90 @@ var RestTransport = class {
|
|
|
25335
25367
|
}));
|
|
25336
25368
|
}
|
|
25337
25369
|
};
|
|
25370
|
+
var SUBSCRIBER_QUEUE_MAX = 8;
|
|
25371
|
+
var PingSubscriber = class {
|
|
25372
|
+
queue = [];
|
|
25373
|
+
waiter;
|
|
25374
|
+
ended = false;
|
|
25375
|
+
started = false;
|
|
25376
|
+
pings = {
|
|
25377
|
+
[Symbol.asyncIterator]: () => {
|
|
25378
|
+
if (this.started)
|
|
25379
|
+
throw new Error("subscribeDeliveryPings: single-use async iterable");
|
|
25380
|
+
this.started = true;
|
|
25381
|
+
return {
|
|
25382
|
+
next: () => {
|
|
25383
|
+
if (this.ended)
|
|
25384
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
25385
|
+
if (this.queue.length)
|
|
25386
|
+
return Promise.resolve({ value: this.queue.shift(), done: false });
|
|
25387
|
+
return new Promise((resolve4) => {
|
|
25388
|
+
this.waiter = resolve4;
|
|
25389
|
+
});
|
|
25390
|
+
}
|
|
25391
|
+
};
|
|
25392
|
+
}
|
|
25393
|
+
};
|
|
25394
|
+
push(p) {
|
|
25395
|
+
if (this.ended)
|
|
25396
|
+
return;
|
|
25397
|
+
if (this.waiter) {
|
|
25398
|
+
const w = this.waiter;
|
|
25399
|
+
this.waiter = void 0;
|
|
25400
|
+
w({ value: p, done: false });
|
|
25401
|
+
} else if (this.queue.length < SUBSCRIBER_QUEUE_MAX) {
|
|
25402
|
+
this.queue.push(p);
|
|
25403
|
+
}
|
|
25404
|
+
}
|
|
25405
|
+
end() {
|
|
25406
|
+
this.ended = true;
|
|
25407
|
+
this.queue.length = 0;
|
|
25408
|
+
if (this.waiter) {
|
|
25409
|
+
const w = this.waiter;
|
|
25410
|
+
this.waiter = void 0;
|
|
25411
|
+
w({ value: void 0, done: true });
|
|
25412
|
+
}
|
|
25413
|
+
}
|
|
25414
|
+
};
|
|
25415
|
+
var PingHub = class {
|
|
25416
|
+
handle;
|
|
25417
|
+
onGone;
|
|
25418
|
+
subs = /* @__PURE__ */ new Set();
|
|
25419
|
+
send;
|
|
25420
|
+
constructor(handle, onGone) {
|
|
25421
|
+
this.handle = handle;
|
|
25422
|
+
this.onGone = onGone;
|
|
25423
|
+
this.send = handle.send;
|
|
25424
|
+
void this.pump();
|
|
25425
|
+
}
|
|
25426
|
+
/** `openDeliveryPings` iterables never reject and push() never throws, so
|
|
25427
|
+
* this loop only ends when the shared stream ends (our close, or terminal). */
|
|
25428
|
+
async pump() {
|
|
25429
|
+
for await (const p of this.handle.pings)
|
|
25430
|
+
for (const s of this.subs)
|
|
25431
|
+
s.push(p);
|
|
25432
|
+
this.onGone();
|
|
25433
|
+
}
|
|
25434
|
+
attach() {
|
|
25435
|
+
const s = new PingSubscriber();
|
|
25436
|
+
this.subs.add(s);
|
|
25437
|
+
return s;
|
|
25438
|
+
}
|
|
25439
|
+
/** Idempotent: detaches and ends only `s`; the last one out closes the socket. */
|
|
25440
|
+
detach(s) {
|
|
25441
|
+
this.subs.delete(s);
|
|
25442
|
+
s.end();
|
|
25443
|
+
if (this.subs.size === 0)
|
|
25444
|
+
this.onGone();
|
|
25445
|
+
}
|
|
25446
|
+
/** Idempotent: `openDeliveryPings().close()` is, and the set is empty after. */
|
|
25447
|
+
shutdown() {
|
|
25448
|
+
for (const s of this.subs)
|
|
25449
|
+
s.end();
|
|
25450
|
+
this.subs.clear();
|
|
25451
|
+
this.handle.close();
|
|
25452
|
+
}
|
|
25453
|
+
};
|
|
25338
25454
|
|
|
25339
25455
|
// ../connect-sdk-ts/dist/mls/rest-backend.js
|
|
25340
25456
|
function describeType(v) {
|
|
@@ -35208,6 +35324,7 @@ var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35208
35324
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35209
35325
|
]);
|
|
35210
35326
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35327
|
+
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35211
35328
|
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
35212
35329
|
var OWNER_TURN_UNCHECKED = Object.freeze({ kind: "unchecked" });
|
|
35213
35330
|
function ownerDmDefaultGrantsEnabled(env = process.env) {
|
|
@@ -35482,6 +35599,12 @@ function rosterFreshAt(entry, heldEpoch, heldGroupId) {
|
|
|
35482
35599
|
var RoomGrantEpochMovedError = class extends Error {
|
|
35483
35600
|
};
|
|
35484
35601
|
var ROOM_GRANT_TRACK_MAX = 4096;
|
|
35602
|
+
var SessionSupersededError = class extends Error {
|
|
35603
|
+
constructor() {
|
|
35604
|
+
super("session_superseded: another copy of this agent took its session during start()");
|
|
35605
|
+
this.name = "SessionSupersededError";
|
|
35606
|
+
}
|
|
35607
|
+
};
|
|
35485
35608
|
var GroupSupersededSendError = class extends Error {
|
|
35486
35609
|
constructor(channelId, activeGroupId) {
|
|
35487
35610
|
super(`send refused for channel ${channelId}: the channel moved to a new secure group; not sent`);
|
|
@@ -35521,9 +35644,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35521
35644
|
const seed = hexToBytes4(cfg.signingPrivateKey);
|
|
35522
35645
|
this.custody = SoftwareKeyCustody.fromPrivateKey("ed25519", seed);
|
|
35523
35646
|
this.replyIdKey = deriveReplyIdKey(seed);
|
|
35647
|
+
const base = deps.fetchImpl ?? fetch;
|
|
35648
|
+
this.fetchImpl = (input, init) => this.sessionSuperseded ? Promise.reject(new Error("session_superseded")) : base(input, init);
|
|
35524
35649
|
this.transport = deps.transport ?? new RestTransport({
|
|
35525
35650
|
baseUrl: cfg.apiBase,
|
|
35526
35651
|
bearerToken: cfg.apiKey,
|
|
35652
|
+
fetchImpl: this.fetchImpl,
|
|
35653
|
+
wsImpl: deps.wsImpl,
|
|
35654
|
+
// A 4409 close means another copy of this agent took the session.
|
|
35655
|
+
// Every other terminal close keeps today's behaviour.
|
|
35656
|
+
onTerminalClose: (close) => {
|
|
35657
|
+
if (close.code === WS_CLOSE_SESSION_SUPERSEDED2) this.onSessionSuperseded();
|
|
35658
|
+
},
|
|
35527
35659
|
connectIdentityId: cfg.identityId,
|
|
35528
35660
|
// P1b: hardened delivery-ping reconnect (verify-before-terminal +
|
|
35529
35661
|
// half-open + wake + auth war-cap). On by default for the daemon;
|
|
@@ -35537,7 +35669,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35537
35669
|
else writeServiceLog("warn", "transport.delivery_ping_lifecycle_unknown");
|
|
35538
35670
|
}
|
|
35539
35671
|
});
|
|
35540
|
-
this.backend = deps.backend ?? new RestMlsBackend({ baseUrl: cfg.apiBase, bearerToken: cfg.apiKey });
|
|
35672
|
+
this.backend = deps.backend ?? new RestMlsBackend({ baseUrl: cfg.apiBase, bearerToken: cfg.apiKey, fetchImpl: this.fetchImpl });
|
|
35541
35673
|
this.store = new SqliteMlsStateStore({
|
|
35542
35674
|
dbPath: cfg.storePath,
|
|
35543
35675
|
identityId: cfg.identityId,
|
|
@@ -35608,7 +35740,6 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35608
35740
|
// throws (mirrors the divergence-heal signal path).
|
|
35609
35741
|
onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId, this.drainExemptionAtCallIn())
|
|
35610
35742
|
});
|
|
35611
|
-
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
35612
35743
|
this.home = deps.home ?? homedir4();
|
|
35613
35744
|
this.grantWriters = {
|
|
35614
35745
|
writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
|
|
@@ -35740,6 +35871,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35740
35871
|
// stored for peek-then-join in ingestOnce
|
|
35741
35872
|
custody;
|
|
35742
35873
|
fetchImpl;
|
|
35874
|
+
/** P-1: set once a 4409 close says another copy took this agent's session.
|
|
35875
|
+
* The gated fetch built in the constructor refuses every request after it. */
|
|
35876
|
+
sessionSuperseded = false;
|
|
35877
|
+
/** C9: who hears `session_superseded` (runMode's quiesce). */
|
|
35878
|
+
sessionSupersededListeners = /* @__PURE__ */ new Set();
|
|
35879
|
+
/** C9 (spec §4.1.6, low 3): the one close() run every caller shares. */
|
|
35880
|
+
closing = null;
|
|
35743
35881
|
/** Round 5: home for the managed runtime-config directory (test seam). */
|
|
35744
35882
|
home;
|
|
35745
35883
|
grantWriters;
|
|
@@ -36215,6 +36353,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36215
36353
|
action;
|
|
36216
36354
|
waiters = [];
|
|
36217
36355
|
pingHandle;
|
|
36356
|
+
/** C10 (P-15): the delivery-ping subscription start() opens at its top,
|
|
36357
|
+
* held unconsumed until startReceivePump() takes it. */
|
|
36358
|
+
earlyPings = null;
|
|
36359
|
+
/** C10 fix round 1: settles (never rejects) when start()'s body does; close()
|
|
36360
|
+
* awaits it, bounded, so the store never closes under a start() step. */
|
|
36361
|
+
startInFlight = null;
|
|
36218
36362
|
safetyTimer;
|
|
36219
36363
|
/** The ingest cycle now running, if any (see ingestOnce: single-flight). */
|
|
36220
36364
|
ingestInFlight = null;
|
|
@@ -36792,7 +36936,50 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36792
36936
|
this.droppedMessages += overflow;
|
|
36793
36937
|
}
|
|
36794
36938
|
}
|
|
36939
|
+
/**
|
|
36940
|
+
* C10 (spec P-15, OQ-8 (b)): subscribe early, consume late. The
|
|
36941
|
+
* delivery-ping socket opens here, before anything else, so this copy claims
|
|
36942
|
+
* the device's session (and another copy gets its 4409) BEFORE this copy's
|
|
36943
|
+
* boot drain runs. The pings it carries are NOT consumed until
|
|
36944
|
+
* startReceivePump(), the same place as before: starting the pump first is
|
|
36945
|
+
* still forbidden (DRAIN BEFORE ROTATE, startBody).
|
|
36946
|
+
*
|
|
36947
|
+
* A 4409 during start() (audit H-3): the gated fetch is already set (C9 step
|
|
36948
|
+
* 0). start() then arms nothing more (supersededDuringStart before each
|
|
36949
|
+
* timer and before the pump), runs the memoized close() quiesce, and rejects
|
|
36950
|
+
* with SessionSupersededError. A step that FAILED because of the supersede
|
|
36951
|
+
* (a refused fetch, a store the quiesce closed) is the same outcome.
|
|
36952
|
+
*/
|
|
36795
36953
|
async start() {
|
|
36954
|
+
const early = this.transport.subscribeDeliveryPings("");
|
|
36955
|
+
this.earlyPings = early;
|
|
36956
|
+
this.pingHandle = { close: early.close };
|
|
36957
|
+
const body = this.startBody();
|
|
36958
|
+
this.startInFlight = body.then(() => {
|
|
36959
|
+
}, () => {
|
|
36960
|
+
});
|
|
36961
|
+
try {
|
|
36962
|
+
await body;
|
|
36963
|
+
} catch (e) {
|
|
36964
|
+
if (!this.sessionSuperseded) {
|
|
36965
|
+
if (this.earlyPings) {
|
|
36966
|
+
this.earlyPings.close();
|
|
36967
|
+
this.earlyPings = null;
|
|
36968
|
+
this.pingHandle = void 0;
|
|
36969
|
+
}
|
|
36970
|
+
throw e;
|
|
36971
|
+
}
|
|
36972
|
+
if (!(e instanceof SessionSupersededError)) writeServiceLog("warn", "startup.superseded_start_error", {}, e);
|
|
36973
|
+
await this.close().catch(() => {
|
|
36974
|
+
});
|
|
36975
|
+
throw new SessionSupersededError();
|
|
36976
|
+
}
|
|
36977
|
+
}
|
|
36978
|
+
/** C10: true once a 4409 landed; checked before each arming step of start(). */
|
|
36979
|
+
supersededDuringStart() {
|
|
36980
|
+
return this.sessionSuperseded;
|
|
36981
|
+
}
|
|
36982
|
+
async startBody() {
|
|
36796
36983
|
this.startedAt = Date.now();
|
|
36797
36984
|
await this.restoreRuntimeInbox();
|
|
36798
36985
|
await this.loadToolGrantState();
|
|
@@ -36852,10 +37039,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36852
37039
|
await this.warmRestoredNotes();
|
|
36853
37040
|
await this.reindexRestoredTurns();
|
|
36854
37041
|
if (this.unifiedOn) await this.reindexUnifiedRestored();
|
|
37042
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36855
37043
|
this.runtimeConfigTimer = setInterval(() => {
|
|
36856
37044
|
void this.refetchRuntimeConfig();
|
|
36857
37045
|
}, 6 * 60 * 60 * 1e3);
|
|
36858
37046
|
this.runtimeConfigTimer.unref?.();
|
|
37047
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36859
37048
|
this.membershipSweepTimer = setInterval(() => {
|
|
36860
37049
|
if (this.closingDown || this.membershipSweepWork) return;
|
|
36861
37050
|
this.membershipSweepWork = this.runMembershipSweeps().finally(() => {
|
|
@@ -36863,6 +37052,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36863
37052
|
});
|
|
36864
37053
|
}, PURGE_SWEEP_INTERVAL_MS);
|
|
36865
37054
|
this.membershipSweepTimer.unref?.();
|
|
37055
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36866
37056
|
this.recallPruneTimer = setInterval(() => {
|
|
36867
37057
|
if (this.closingDown || this.recallPruneWork) return;
|
|
36868
37058
|
this.recallPruneWork = this.runRecallPruneTick().finally(() => {
|
|
@@ -36870,6 +37060,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36870
37060
|
});
|
|
36871
37061
|
}, 60 * 60 * 1e3);
|
|
36872
37062
|
this.recallPruneTimer.unref?.();
|
|
37063
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36873
37064
|
if (this.machinePaths) this.recallMaintenanceTimer = setInterval(() => {
|
|
36874
37065
|
if (this.closingDown) return;
|
|
36875
37066
|
const n = ++this.maintenanceTickN;
|
|
@@ -36880,6 +37071,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36880
37071
|
});
|
|
36881
37072
|
}, 1e3);
|
|
36882
37073
|
this.recallMaintenanceTimer?.unref?.();
|
|
37074
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36883
37075
|
this.startReceivePump();
|
|
36884
37076
|
if (drainedInTime) {
|
|
36885
37077
|
this.armSelfHealSweeps();
|
|
@@ -41143,7 +41335,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41143
41335
|
}
|
|
41144
41336
|
}
|
|
41145
41337
|
startReceivePump() {
|
|
41146
|
-
const sub = this.transport.subscribeDeliveryPings("");
|
|
41338
|
+
const sub = this.earlyPings ?? this.transport.subscribeDeliveryPings("");
|
|
41339
|
+
this.earlyPings = null;
|
|
41147
41340
|
this.pingHandle = { close: sub.close };
|
|
41148
41341
|
void (async () => {
|
|
41149
41342
|
try {
|
|
@@ -42728,7 +42921,42 @@ ${channelId}`;
|
|
|
42728
42921
|
build: BRIDGE_VERSION
|
|
42729
42922
|
};
|
|
42730
42923
|
}
|
|
42731
|
-
|
|
42924
|
+
/** C9: `session_superseded` once a 4409 said another copy took this
|
|
42925
|
+
* agent's session (the gated fetch already refuses every request). The
|
|
42926
|
+
* local HTTP routes read it: every route but `/healthz` answers 503. */
|
|
42927
|
+
get sessionState() {
|
|
42928
|
+
return this.sessionSuperseded ? "session_superseded" : "live";
|
|
42929
|
+
}
|
|
42930
|
+
/** C9: subscribe to `session_superseded`. runMode subscribes right after
|
|
42931
|
+
* construction, before start(), so a 4409 during start() is heard too. */
|
|
42932
|
+
on(event, listener) {
|
|
42933
|
+
this.sessionSupersededListeners.add(listener);
|
|
42934
|
+
return () => this.sessionSupersededListeners.delete(listener);
|
|
42935
|
+
}
|
|
42936
|
+
/** P-1 step 0, then the event (spec §4.1.6): another copy of this agent took
|
|
42937
|
+
* the session (4409). The gated-fetch switch is set FIRST, so no HTTP
|
|
42938
|
+
* request leaves the process after this, whatever a listener does. */
|
|
42939
|
+
onSessionSuperseded() {
|
|
42940
|
+
this.sessionSuperseded = true;
|
|
42941
|
+
for (const listener of [...this.sessionSupersededListeners]) {
|
|
42942
|
+
try {
|
|
42943
|
+
listener();
|
|
42944
|
+
} catch (e) {
|
|
42945
|
+
writeServiceLog("error", "transport.session_superseded_listener_failed", {}, e);
|
|
42946
|
+
}
|
|
42947
|
+
}
|
|
42948
|
+
}
|
|
42949
|
+
/**
|
|
42950
|
+
* Idempotent (C9, spec §4.1.6 low 3): every call returns the FIRST call's
|
|
42951
|
+
* promise, so a SIGTERM during or after the supersede quiesce never runs
|
|
42952
|
+
* store.close() twice. A rejected close stays rejected: a retry would close
|
|
42953
|
+
* the store a second time.
|
|
42954
|
+
*/
|
|
42955
|
+
close() {
|
|
42956
|
+
this.closing ??= this.closeOnce();
|
|
42957
|
+
return this.closing;
|
|
42958
|
+
}
|
|
42959
|
+
async closeOnce() {
|
|
42732
42960
|
this.closingDown = true;
|
|
42733
42961
|
this.statusGate.close();
|
|
42734
42962
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
@@ -42741,6 +42969,28 @@ ${channelId}`;
|
|
|
42741
42969
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
42742
42970
|
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
42743
42971
|
if (this.membershipSweepTimer) clearInterval(this.membershipSweepTimer);
|
|
42972
|
+
const ingest = this.ingestInFlight;
|
|
42973
|
+
if (ingest) {
|
|
42974
|
+
let timer;
|
|
42975
|
+
await Promise.race([
|
|
42976
|
+
ingest.released,
|
|
42977
|
+
new Promise((resolve4) => {
|
|
42978
|
+
timer = setTimeout(resolve4, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
42979
|
+
})
|
|
42980
|
+
]);
|
|
42981
|
+
if (timer) clearTimeout(timer);
|
|
42982
|
+
}
|
|
42983
|
+
const starting = this.startInFlight;
|
|
42984
|
+
if (starting) {
|
|
42985
|
+
let timer;
|
|
42986
|
+
await Promise.race([
|
|
42987
|
+
starting,
|
|
42988
|
+
new Promise((resolve4) => {
|
|
42989
|
+
timer = setTimeout(resolve4, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
42990
|
+
})
|
|
42991
|
+
]);
|
|
42992
|
+
if (timer) clearTimeout(timer);
|
|
42993
|
+
}
|
|
42744
42994
|
if (this.membershipSweepWork) {
|
|
42745
42995
|
let timer;
|
|
42746
42996
|
await Promise.race([
|
|
@@ -42899,6 +43149,9 @@ var BRIDGE_ERRORS = {
|
|
|
42899
43149
|
// longer holds the shared bytes or key. Terminal for that link: never fall
|
|
42900
43150
|
// back to the current version. `detail.pin` names the refused pin.
|
|
42901
43151
|
vault_ref_changed: { http: 422, message: "this file changed in Files after it was shared; not opened" },
|
|
43152
|
+
// C9 (spec §4.1.6 step 1): another copy of this agent took its SecureComs
|
|
43153
|
+
// session (WS 4409), so this copy stopped. Every local route but /healthz.
|
|
43154
|
+
session_superseded: { http: 503, message: "another copy of this agent took its session; this copy stopped" },
|
|
42902
43155
|
internal: { http: 500, message: "internal error" }
|
|
42903
43156
|
};
|
|
42904
43157
|
function errorBody(code, detail) {
|
|
@@ -43129,6 +43382,10 @@ function createHttpServer(agent, opts) {
|
|
|
43129
43382
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
43130
43383
|
const path = url.pathname;
|
|
43131
43384
|
const method = req.method ?? "GET";
|
|
43385
|
+
if (agent.sessionState === "session_superseded") {
|
|
43386
|
+
if (method === "GET" && path === "/healthz") return json(res, 200, { state: "session_superseded" });
|
|
43387
|
+
return jsonError(res, "session_superseded");
|
|
43388
|
+
}
|
|
43132
43389
|
if (method === "GET" && path === "/healthz") {
|
|
43133
43390
|
return json(res, 200, { ...agent.health(), capabilities: capabilities(opts) });
|
|
43134
43391
|
}
|
|
@@ -43340,6 +43597,7 @@ var PING_INTERVAL_MS = 3e4;
|
|
|
43340
43597
|
var INFLIGHT_STALE_SWEEP_MS = 6e4;
|
|
43341
43598
|
var INFLIGHT_STALE_AFTER_MS = 15 * 6e4;
|
|
43342
43599
|
var HELLO_WAIT_MS = 2e3;
|
|
43600
|
+
var WS_CLOSE_SESSION_SUPERSEDED3 = 4409;
|
|
43343
43601
|
var MEMORY_NOTES_FEATURE = "memory_notes";
|
|
43344
43602
|
var MEMORY_RECALL_FEATURE = "memory_recall";
|
|
43345
43603
|
var MEMORY_UNIFIED_FEATURE = "memory_unified";
|
|
@@ -43360,7 +43618,12 @@ function authedUpgrade(req, opts) {
|
|
|
43360
43618
|
return true;
|
|
43361
43619
|
}
|
|
43362
43620
|
function attachAgentWebSocket(server, agent, opts) {
|
|
43363
|
-
|
|
43621
|
+
let sessionSuperseded = false;
|
|
43622
|
+
const wss = new WebSocketServer({
|
|
43623
|
+
server,
|
|
43624
|
+
path: "/v1/ws",
|
|
43625
|
+
verifyClient: (_info, done) => sessionSuperseded ? done(false, 503, "session_superseded") : done(true)
|
|
43626
|
+
});
|
|
43364
43627
|
const keepalive = setInterval(() => {
|
|
43365
43628
|
for (const client of wss.clients) {
|
|
43366
43629
|
const sock = client;
|
|
@@ -43500,6 +43763,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43500
43763
|
if (helloTimer) clearTimeout(helloTimer);
|
|
43501
43764
|
agent.noteSessionClose();
|
|
43502
43765
|
unsubscribe();
|
|
43766
|
+
if (sessionSuperseded) inflight.clear();
|
|
43503
43767
|
if (inflight.size) {
|
|
43504
43768
|
agent.requeueFailedRuntimeDeliveries(
|
|
43505
43769
|
[...inflight.values()].filter((m) => !agent.isRuntimeMessageRetired(m.message_id))
|
|
@@ -43521,7 +43785,10 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43521
43785
|
sock.on("close", cleanup);
|
|
43522
43786
|
sock.on("error", cleanup);
|
|
43523
43787
|
sock.on("message", (data) => {
|
|
43524
|
-
|
|
43788
|
+
if (sessionSuperseded) return;
|
|
43789
|
+
handleInbound(sock, agent, data, inflight, flush, session).catch((e) => {
|
|
43790
|
+
writeServiceLog("error", "ws.inbound_failed", {}, e);
|
|
43791
|
+
});
|
|
43525
43792
|
});
|
|
43526
43793
|
flush();
|
|
43527
43794
|
});
|
|
@@ -43536,6 +43803,13 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43536
43803
|
}
|
|
43537
43804
|
}
|
|
43538
43805
|
wss.close();
|
|
43806
|
+
},
|
|
43807
|
+
/** C9 (spec §4.1.6 step 1, OQ-9): close every open socket with 4409 /
|
|
43808
|
+
* `session_superseded`, and answer every later upgrade with 503. The
|
|
43809
|
+
* runtime's reconnect backoff does the rest; idempotent. */
|
|
43810
|
+
enterSessionSuperseded() {
|
|
43811
|
+
sessionSuperseded = true;
|
|
43812
|
+
for (const client of wss.clients) client.close(WS_CLOSE_SESSION_SUPERSEDED3, "session_superseded");
|
|
43539
43813
|
}
|
|
43540
43814
|
};
|
|
43541
43815
|
}
|
|
@@ -43837,11 +44111,12 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43837
44111
|
var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
43838
44112
|
|
|
43839
44113
|
// src/bin.ts
|
|
43840
|
-
import { readFileSync as
|
|
43841
|
-
import { join as join25, dirname as
|
|
44114
|
+
import { readFileSync as readFileSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, rmSync as rmSync8, existsSync as existsSync7, realpathSync as realpathSync3, readdirSync as readdirSync4, statSync as statSync8, chmodSync as chmodSync6 } from "node:fs";
|
|
44115
|
+
import { join as join25, dirname as dirname13, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
|
|
43842
44116
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
43843
44117
|
import { homedir as homedir5, userInfo } from "node:os";
|
|
43844
44118
|
import { spawnSync } from "node:child_process";
|
|
44119
|
+
import { createServer as netCreateServer } from "node:net";
|
|
43845
44120
|
|
|
43846
44121
|
// src/service/install.ts
|
|
43847
44122
|
import { dirname as dirname8 } from "node:path";
|
|
@@ -43962,7 +44237,10 @@ ${programArgs}
|
|
|
43962
44237
|
<key>RunAtLoad</key>
|
|
43963
44238
|
<true/>
|
|
43964
44239
|
<key>KeepAlive</key>
|
|
43965
|
-
<
|
|
44240
|
+
<dict>
|
|
44241
|
+
<key>SuccessfulExit</key>
|
|
44242
|
+
<false/>
|
|
44243
|
+
</dict>
|
|
43966
44244
|
<key>StandardOutPath</key>
|
|
43967
44245
|
<string>${escapeXml(cfg.logPath)}</string>
|
|
43968
44246
|
<key>StandardErrorPath</key>
|
|
@@ -43989,7 +44267,7 @@ Wants=network-online.target
|
|
|
43989
44267
|
[Service]
|
|
43990
44268
|
Type=simple
|
|
43991
44269
|
ExecStart=${execStart}
|
|
43992
|
-
Restart=
|
|
44270
|
+
Restart=on-failure
|
|
43993
44271
|
RestartSec=5
|
|
43994
44272
|
|
|
43995
44273
|
[Install]
|
|
@@ -44462,6 +44740,10 @@ ${customFacts(input)}`, wired: false, error: false };
|
|
|
44462
44740
|
}
|
|
44463
44741
|
|
|
44464
44742
|
// src/service/install-prep.ts
|
|
44743
|
+
function isDaemonRouted(h) {
|
|
44744
|
+
return h != null && h.state !== "session_superseded";
|
|
44745
|
+
}
|
|
44746
|
+
var VERIFY_BUDGET_MS = BOOT_DRAIN_WAIT_MS_DEFAULT + 15e3;
|
|
44465
44747
|
function assertReadableCredential(absPath, deps) {
|
|
44466
44748
|
let raw;
|
|
44467
44749
|
try {
|
|
@@ -44489,6 +44771,44 @@ function durableDir(home, slug) {
|
|
|
44489
44771
|
function durableCredentialPath(home, slug) {
|
|
44490
44772
|
return join21(durableDir(home, slug), `${slug}.json`);
|
|
44491
44773
|
}
|
|
44774
|
+
var DURABLE_PREFIX = "securecoms-agent-";
|
|
44775
|
+
var InstallRefusedError = class extends Error {
|
|
44776
|
+
};
|
|
44777
|
+
function existingIdentityError(dir, label) {
|
|
44778
|
+
return new InstallRefusedError(
|
|
44779
|
+
`This agent is already installed in ${dir} with a different key. Uninstall it first: agent-bridge uninstall --label ${label}`
|
|
44780
|
+
);
|
|
44781
|
+
}
|
|
44782
|
+
function refuseExistingIdentity(args2, deps) {
|
|
44783
|
+
const destName = `${DURABLE_PREFIX}${args2.slug}`;
|
|
44784
|
+
let names;
|
|
44785
|
+
try {
|
|
44786
|
+
names = deps.listDir(args2.home);
|
|
44787
|
+
} catch {
|
|
44788
|
+
names = [];
|
|
44789
|
+
}
|
|
44790
|
+
const others = names.filter((n) => n.startsWith(DURABLE_PREFIX) && n !== destName).sort();
|
|
44791
|
+
for (const name of [destName, ...others]) {
|
|
44792
|
+
const slug = name.slice(DURABLE_PREFIX.length);
|
|
44793
|
+
const dir = join21(args2.home, name);
|
|
44794
|
+
const credPath = join21(dir, `${slug}.json`);
|
|
44795
|
+
if (!deps.fileExists(credPath)) continue;
|
|
44796
|
+
let existing = null;
|
|
44797
|
+
try {
|
|
44798
|
+
const j = JSON.parse(deps.readFile(credPath));
|
|
44799
|
+
if (j && typeof j === "object") existing = j;
|
|
44800
|
+
} catch {
|
|
44801
|
+
}
|
|
44802
|
+
if (!existing || typeof existing.identity_id !== "string" || !existing.identity_id) {
|
|
44803
|
+
if (deps.fileExists(join21(dir, "bridge-state.db"))) throw existingIdentityError(dir, slug);
|
|
44804
|
+
continue;
|
|
44805
|
+
}
|
|
44806
|
+
if (existing.identity_id !== args2.parsed.identityId) continue;
|
|
44807
|
+
if (name !== destName || existing.device_id !== args2.parsed.deviceId) {
|
|
44808
|
+
throw existingIdentityError(dir, slug);
|
|
44809
|
+
}
|
|
44810
|
+
}
|
|
44811
|
+
}
|
|
44492
44812
|
function takeCustody(args2, deps) {
|
|
44493
44813
|
const dir = durableDir(args2.home, args2.slug);
|
|
44494
44814
|
const dest = durableCredentialPath(args2.home, args2.slug);
|
|
@@ -44496,10 +44816,11 @@ function takeCustody(args2, deps) {
|
|
|
44496
44816
|
if (deps.fileExists(dest) && deps.realpath(args2.sourceAbsPath) === deps.realpath(dest)) return dest;
|
|
44497
44817
|
} catch {
|
|
44498
44818
|
}
|
|
44819
|
+
refuseExistingIdentity(args2, deps);
|
|
44499
44820
|
if (deps.fileExists(dest)) {
|
|
44500
44821
|
try {
|
|
44501
44822
|
const existing = JSON.parse(deps.readFile(dest));
|
|
44502
|
-
if (existing.
|
|
44823
|
+
if (existing.identity_id && existing.identity_id !== args2.parsed.identityId) {
|
|
44503
44824
|
throw new Error(
|
|
44504
44825
|
`install: ${dir} already belongs to a different agent (device ${existing.device_id}). Pass a distinct --label to install this agent in its own directory.`
|
|
44505
44826
|
);
|
|
@@ -44554,21 +44875,19 @@ function selectPort(args2, deps) {
|
|
|
44554
44875
|
throw new Error(`install: no free port in ${PORT_MIN}-${PORT_MAX} \u2014 stop an unused bridge or pass --port`);
|
|
44555
44876
|
}
|
|
44556
44877
|
async function verifyDaemonUp(args2, deps) {
|
|
44557
|
-
const attempts = args2.attempts ?? 20;
|
|
44558
44878
|
const delayMs = args2.delayMs ?? 250;
|
|
44559
|
-
|
|
44560
|
-
|
|
44561
|
-
|
|
44562
|
-
|
|
44563
|
-
|
|
44564
|
-
}
|
|
44879
|
+
const deadline = deps.now() + VERIFY_BUDGET_MS;
|
|
44880
|
+
const maxAttempts = args2.attempts ?? Number.POSITIVE_INFINITY;
|
|
44881
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
44882
|
+
if (isDaemonRouted(deps.getHealth(args2.port))) return true;
|
|
44883
|
+
if (deps.now() >= deadline) return false;
|
|
44565
44884
|
await deps.sleep(delayMs);
|
|
44566
44885
|
}
|
|
44567
44886
|
return false;
|
|
44568
44887
|
}
|
|
44569
44888
|
async function orchestrateInstall(ctx) {
|
|
44570
44889
|
const parsed = assertReadableCredential(ctx.credentialAbsPath, ctx.prepDeps);
|
|
44571
|
-
const stemForSlug = ctx.label ?? ctx.credentialAbsPath.replace(/^.*[\\/]/, "").replace(/\.[^.]*$/, "");
|
|
44890
|
+
const stemForSlug = ctx.label ?? ctx.credentialAbsPath.replace(/^.*[\\/]/, "").replace(/\.[^.]*$/, "").replace(/ \(\d+\)$/, "");
|
|
44572
44891
|
const slug = slugify(stemForSlug);
|
|
44573
44892
|
const durablePath = takeCustody(
|
|
44574
44893
|
{ home: ctx.home, slug, sourceAbsPath: ctx.credentialAbsPath, parsed },
|
|
@@ -44692,13 +45011,13 @@ function readFleet(deps) {
|
|
|
44692
45011
|
// src/service/status.ts
|
|
44693
45012
|
function readFleetStatus(deps) {
|
|
44694
45013
|
return deps.readFleet().map((e) => {
|
|
44695
|
-
const
|
|
45014
|
+
const h = e.port != null ? deps.getHealth(e.port) : null;
|
|
45015
|
+
const up = isDaemonRouted(h);
|
|
44696
45016
|
let harness = "-";
|
|
44697
45017
|
let evictions = 0;
|
|
44698
|
-
if (up
|
|
44699
|
-
|
|
44700
|
-
|
|
44701
|
-
evictions = h?.evictions ?? 0;
|
|
45018
|
+
if (up) {
|
|
45019
|
+
harness = (h.agent_sessions ?? 0) >= 1 ? "attached" : "no brain";
|
|
45020
|
+
evictions = h.evictions ?? 0;
|
|
44702
45021
|
}
|
|
44703
45022
|
return {
|
|
44704
45023
|
slug: e.slug,
|
|
@@ -44790,20 +45109,339 @@ function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_F
|
|
|
44790
45109
|
}
|
|
44791
45110
|
}
|
|
44792
45111
|
|
|
45112
|
+
// src/service/instance-lock.ts
|
|
45113
|
+
import { execFileSync } from "node:child_process";
|
|
45114
|
+
import { closeSync as closeSync4, fsyncSync, mkdirSync as mkdirSync7, openSync as openSync4, readFileSync as readFileSync13, statSync as statSync6, unlinkSync as unlinkSync3, utimesSync, writeSync as writeSync2 } from "node:fs";
|
|
45115
|
+
import { hostname, uptime as uptime2 } from "node:os";
|
|
45116
|
+
import { dirname as dirname11 } from "node:path";
|
|
45117
|
+
var LOCK_PARTIAL_WRITE_GRACE_MS = 1e4;
|
|
45118
|
+
var LOCK_TAKEOVER_GUARD_STALE_MS = 3e4;
|
|
45119
|
+
var BOOT_TOLERANCE_SEC = 120;
|
|
45120
|
+
var LOCK_HEARTBEAT_MS = 6e4;
|
|
45121
|
+
var LOCK_FOREIGN_STALE_MS = 10 * LOCK_HEARTBEAT_MS;
|
|
45122
|
+
function errno(error) {
|
|
45123
|
+
const code = error?.code;
|
|
45124
|
+
return typeof code === "string" ? code : void 0;
|
|
45125
|
+
}
|
|
45126
|
+
function readTrimmed(path) {
|
|
45127
|
+
try {
|
|
45128
|
+
const v = readFileSync13(path, "utf8").trim();
|
|
45129
|
+
return v === "" ? void 0 : v;
|
|
45130
|
+
} catch {
|
|
45131
|
+
return void 0;
|
|
45132
|
+
}
|
|
45133
|
+
}
|
|
45134
|
+
function procStartFromStat(stat) {
|
|
45135
|
+
const close = stat.lastIndexOf(")");
|
|
45136
|
+
if (close < 0) return void 0;
|
|
45137
|
+
const rest = stat.slice(close + 1).trim().split(/\s+/);
|
|
45138
|
+
const v = rest[19];
|
|
45139
|
+
return v !== void 0 && /^\d+$/.test(v) ? v : void 0;
|
|
45140
|
+
}
|
|
45141
|
+
function pidAliveByKill(pid) {
|
|
45142
|
+
try {
|
|
45143
|
+
process.kill(pid, 0);
|
|
45144
|
+
return true;
|
|
45145
|
+
} catch (error) {
|
|
45146
|
+
return errno(error) === "EPERM";
|
|
45147
|
+
}
|
|
45148
|
+
}
|
|
45149
|
+
var bootIdCache;
|
|
45150
|
+
function readBootId() {
|
|
45151
|
+
if (bootIdCache) return bootIdCache.v;
|
|
45152
|
+
let v = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
45153
|
+
if (v === void 0 && process.platform === "darwin") {
|
|
45154
|
+
try {
|
|
45155
|
+
const out = execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], {
|
|
45156
|
+
encoding: "utf8",
|
|
45157
|
+
timeout: 2e3,
|
|
45158
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
45159
|
+
}).trim();
|
|
45160
|
+
v = /^[0-9A-Fa-f-]{16,64}$/.test(out) ? out : void 0;
|
|
45161
|
+
} catch {
|
|
45162
|
+
v = void 0;
|
|
45163
|
+
}
|
|
45164
|
+
}
|
|
45165
|
+
bootIdCache = { v };
|
|
45166
|
+
return v;
|
|
45167
|
+
}
|
|
45168
|
+
var realLockEnv = {
|
|
45169
|
+
pid: process.pid,
|
|
45170
|
+
hostname: () => hostname(),
|
|
45171
|
+
bootId: readBootId,
|
|
45172
|
+
bootEpochSec: () => Math.round(Date.now() / 1e3 - uptime2()),
|
|
45173
|
+
procStart: (pid) => {
|
|
45174
|
+
const stat = readTrimmed(`/proc/${pid}/stat`);
|
|
45175
|
+
return stat ? procStartFromStat(stat) : void 0;
|
|
45176
|
+
},
|
|
45177
|
+
pidAlive: pidAliveByKill
|
|
45178
|
+
};
|
|
45179
|
+
function parseBody(raw) {
|
|
45180
|
+
try {
|
|
45181
|
+
const b = JSON.parse(raw);
|
|
45182
|
+
if (b && b.v === 2 && Number.isInteger(b.pid) && b.pid > 0 && typeof b.host === "string" && Number.isFinite(b.boot)) {
|
|
45183
|
+
return b;
|
|
45184
|
+
}
|
|
45185
|
+
} catch {
|
|
45186
|
+
}
|
|
45187
|
+
return void 0;
|
|
45188
|
+
}
|
|
45189
|
+
var InstanceLock = class {
|
|
45190
|
+
constructor(path, env = realLockEnv, log = () => {
|
|
45191
|
+
}, heartbeatMs = LOCK_HEARTBEAT_MS) {
|
|
45192
|
+
this.path = path;
|
|
45193
|
+
this.env = env;
|
|
45194
|
+
this.log = log;
|
|
45195
|
+
this.heartbeatMs = heartbeatMs;
|
|
45196
|
+
}
|
|
45197
|
+
held = false;
|
|
45198
|
+
/** The exact body this process wrote (#808 N1): release and the ownership
|
|
45199
|
+
* check compare the file against THIS, never against live machine facts
|
|
45200
|
+
* (a hostname can change under a running process). */
|
|
45201
|
+
written;
|
|
45202
|
+
heartbeat;
|
|
45203
|
+
/** The error behind the last `unreadable` / `failed`, for the caller's one
|
|
45204
|
+
* log line (the lock itself never logs a refusal: it is retried per call). */
|
|
45205
|
+
lastError;
|
|
45206
|
+
/** PID read from the lock body of the live holder behind the last `held`. */
|
|
45207
|
+
holderPid;
|
|
45208
|
+
body() {
|
|
45209
|
+
const bootId = this.env.bootId();
|
|
45210
|
+
const start = this.env.procStart(this.env.pid);
|
|
45211
|
+
const b = {
|
|
45212
|
+
v: 2,
|
|
45213
|
+
pid: this.env.pid,
|
|
45214
|
+
host: this.env.hostname(),
|
|
45215
|
+
...bootId !== void 0 ? { boot_id: bootId } : {},
|
|
45216
|
+
boot: this.env.bootEpochSec(),
|
|
45217
|
+
...start !== void 0 ? { start } : {}
|
|
45218
|
+
};
|
|
45219
|
+
return JSON.stringify(b);
|
|
45220
|
+
}
|
|
45221
|
+
/** Try to hold the lock. Never throws (#808 L-e: machine reads included). */
|
|
45222
|
+
acquire() {
|
|
45223
|
+
this.holderPid = void 0;
|
|
45224
|
+
let result;
|
|
45225
|
+
try {
|
|
45226
|
+
result = this.acquireUnguarded();
|
|
45227
|
+
} catch (error) {
|
|
45228
|
+
this.lastError = error;
|
|
45229
|
+
result = "failed";
|
|
45230
|
+
}
|
|
45231
|
+
return result === "held" && this.holderPid !== void 0 ? { result, holderPid: this.holderPid } : { result };
|
|
45232
|
+
}
|
|
45233
|
+
/**
|
|
45234
|
+
* True while the file is still exactly the body this process wrote. Read
|
|
45235
|
+
* before every reserve (one small read), so a holder whose lock was taken
|
|
45236
|
+
* while it was frozen stops before it spends (#808 N1). A lost lock is
|
|
45237
|
+
* dropped: the caller then refuses, and later calls try to re-acquire.
|
|
45238
|
+
*/
|
|
45239
|
+
stillHeld() {
|
|
45240
|
+
if (!this.held) return false;
|
|
45241
|
+
let current;
|
|
45242
|
+
try {
|
|
45243
|
+
current = readFileSync13(this.path, "utf8");
|
|
45244
|
+
} catch {
|
|
45245
|
+
current = void 0;
|
|
45246
|
+
}
|
|
45247
|
+
if (current !== void 0 && current === this.written) return true;
|
|
45248
|
+
this.drop();
|
|
45249
|
+
this.log("warn", "bridge.instance_lock_lost", {});
|
|
45250
|
+
return false;
|
|
45251
|
+
}
|
|
45252
|
+
acquireUnguarded() {
|
|
45253
|
+
if (this.held) return "acquired";
|
|
45254
|
+
const created = this.create();
|
|
45255
|
+
if (created === "created") return this.took();
|
|
45256
|
+
if (created === "failed") return "failed";
|
|
45257
|
+
let raw;
|
|
45258
|
+
let ageMs;
|
|
45259
|
+
this.env.beforeLockRead?.();
|
|
45260
|
+
try {
|
|
45261
|
+
raw = readFileSync13(this.path, "utf8");
|
|
45262
|
+
ageMs = Date.now() - statSync6(this.path).mtimeMs;
|
|
45263
|
+
} catch (error) {
|
|
45264
|
+
if (errno(error) === "ENOENT") {
|
|
45265
|
+
const again = this.create();
|
|
45266
|
+
return again === "created" ? this.took() : again === "failed" ? "failed" : "busy";
|
|
45267
|
+
}
|
|
45268
|
+
this.lastError = error;
|
|
45269
|
+
return "unreadable";
|
|
45270
|
+
}
|
|
45271
|
+
const b = parseBody(raw);
|
|
45272
|
+
if (!b) {
|
|
45273
|
+
if (ageMs < LOCK_PARTIAL_WRITE_GRACE_MS) return "busy";
|
|
45274
|
+
return this.takeOver(raw, void 0);
|
|
45275
|
+
}
|
|
45276
|
+
if (b.host !== this.env.hostname()) {
|
|
45277
|
+
const bootId = this.env.bootId();
|
|
45278
|
+
const sameKernel = b.boot_id !== void 0 && bootId !== void 0 && b.boot_id === bootId;
|
|
45279
|
+
if (sameKernel && ageMs > LOCK_FOREIGN_STALE_MS) return this.takeOver(raw, b.pid);
|
|
45280
|
+
return "foreign_host";
|
|
45281
|
+
}
|
|
45282
|
+
if (!this.sameBoot(b)) return this.takeOver(raw, b.pid);
|
|
45283
|
+
const liveStart = this.env.procStart(b.pid);
|
|
45284
|
+
const reused = b.start !== void 0 && liveStart !== void 0 && liveStart !== b.start;
|
|
45285
|
+
if (b.pid === this.env.pid) {
|
|
45286
|
+
if (!reused) return this.took();
|
|
45287
|
+
return this.takeOver(raw, b.pid);
|
|
45288
|
+
}
|
|
45289
|
+
if (this.env.pidAlive(b.pid) && !reused) {
|
|
45290
|
+
this.holderPid = b.pid;
|
|
45291
|
+
return "held";
|
|
45292
|
+
}
|
|
45293
|
+
return this.takeOver(raw, b.pid);
|
|
45294
|
+
}
|
|
45295
|
+
/** Remove the lock only while it is still exactly the body this process
|
|
45296
|
+
* wrote (#808 N1: not the live hostname, which can change under us). */
|
|
45297
|
+
release() {
|
|
45298
|
+
if (!this.held) return;
|
|
45299
|
+
const written = this.written;
|
|
45300
|
+
this.drop();
|
|
45301
|
+
try {
|
|
45302
|
+
if (written !== void 0 && readFileSync13(this.path, "utf8") === written) unlinkSync3(this.path);
|
|
45303
|
+
} catch {
|
|
45304
|
+
}
|
|
45305
|
+
}
|
|
45306
|
+
drop() {
|
|
45307
|
+
this.held = false;
|
|
45308
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
45309
|
+
this.heartbeat = void 0;
|
|
45310
|
+
}
|
|
45311
|
+
/** We hold it: remember what we wrote (the file as it is now, ours) and
|
|
45312
|
+
* start the heartbeat. */
|
|
45313
|
+
took() {
|
|
45314
|
+
this.held = true;
|
|
45315
|
+
try {
|
|
45316
|
+
this.written = readFileSync13(this.path, "utf8");
|
|
45317
|
+
} catch {
|
|
45318
|
+
this.written = void 0;
|
|
45319
|
+
}
|
|
45320
|
+
if (!this.heartbeat && this.heartbeatMs > 0) {
|
|
45321
|
+
this.heartbeat = setInterval(() => this.beat(), this.heartbeatMs);
|
|
45322
|
+
this.heartbeat.unref?.();
|
|
45323
|
+
}
|
|
45324
|
+
return "acquired";
|
|
45325
|
+
}
|
|
45326
|
+
/** Refresh the mtime while the file is still ours; stop if it is not. */
|
|
45327
|
+
beat() {
|
|
45328
|
+
if (!this.stillHeld()) return;
|
|
45329
|
+
try {
|
|
45330
|
+
const now = /* @__PURE__ */ new Date();
|
|
45331
|
+
utimesSync(this.path, now, now);
|
|
45332
|
+
} catch {
|
|
45333
|
+
}
|
|
45334
|
+
}
|
|
45335
|
+
sameBoot(b) {
|
|
45336
|
+
const bootId = this.env.bootId();
|
|
45337
|
+
if (b.boot_id !== void 0 && bootId !== void 0) return b.boot_id === bootId;
|
|
45338
|
+
return Math.abs(b.boot - this.env.bootEpochSec()) <= BOOT_TOLERANCE_SEC;
|
|
45339
|
+
}
|
|
45340
|
+
/** O_EXCL create with our body. A write failure removes the half-written
|
|
45341
|
+
* file (it is ours: we created it) and reports `failed`, never throws. */
|
|
45342
|
+
create() {
|
|
45343
|
+
let fd;
|
|
45344
|
+
try {
|
|
45345
|
+
mkdirSync7(dirname11(this.path), { recursive: true, mode: 448 });
|
|
45346
|
+
fd = openSync4(this.path, "wx", 384);
|
|
45347
|
+
} catch (error) {
|
|
45348
|
+
if (errno(error) === "EEXIST") return "exists";
|
|
45349
|
+
this.lastError = error;
|
|
45350
|
+
return "failed";
|
|
45351
|
+
}
|
|
45352
|
+
try {
|
|
45353
|
+
writeSync2(fd, this.body());
|
|
45354
|
+
fsyncSync(fd);
|
|
45355
|
+
} catch (error) {
|
|
45356
|
+
try {
|
|
45357
|
+
closeSync4(fd);
|
|
45358
|
+
} catch {
|
|
45359
|
+
}
|
|
45360
|
+
try {
|
|
45361
|
+
unlinkSync3(this.path);
|
|
45362
|
+
} catch {
|
|
45363
|
+
}
|
|
45364
|
+
this.lastError = error;
|
|
45365
|
+
return "failed";
|
|
45366
|
+
}
|
|
45367
|
+
closeSync4(fd);
|
|
45368
|
+
return "created";
|
|
45369
|
+
}
|
|
45370
|
+
/**
|
|
45371
|
+
* Replace a stale lock. Only the holder of the O_EXCL takeover guard may
|
|
45372
|
+
* remove a lock, and only after re-reading it UNCHANGED, so two runtimes
|
|
45373
|
+
* that both saw the same stale lock cannot each delete the other's new one.
|
|
45374
|
+
* A fresh guard means a takeover is in progress: busy this time.
|
|
45375
|
+
*/
|
|
45376
|
+
takeOver(staleRaw, stalePid, retryGuard = true) {
|
|
45377
|
+
const guard = `${this.path}.takeover`;
|
|
45378
|
+
let fd;
|
|
45379
|
+
try {
|
|
45380
|
+
fd = openSync4(guard, "wx", 384);
|
|
45381
|
+
} catch (error) {
|
|
45382
|
+
if (errno(error) !== "EEXIST") return "failed";
|
|
45383
|
+
let crashed = false;
|
|
45384
|
+
try {
|
|
45385
|
+
crashed = Date.now() - statSync6(guard).mtimeMs > LOCK_TAKEOVER_GUARD_STALE_MS;
|
|
45386
|
+
if (crashed) unlinkSync3(guard);
|
|
45387
|
+
} catch {
|
|
45388
|
+
crashed = false;
|
|
45389
|
+
}
|
|
45390
|
+
if (crashed && retryGuard) return this.takeOver(staleRaw, stalePid, false);
|
|
45391
|
+
return "busy";
|
|
45392
|
+
}
|
|
45393
|
+
closeSync4(fd);
|
|
45394
|
+
try {
|
|
45395
|
+
this.env.beforeTakeoverReread?.();
|
|
45396
|
+
let current;
|
|
45397
|
+
try {
|
|
45398
|
+
current = readFileSync13(this.path, "utf8");
|
|
45399
|
+
} catch {
|
|
45400
|
+
current = void 0;
|
|
45401
|
+
}
|
|
45402
|
+
if (current !== void 0 && current !== staleRaw) {
|
|
45403
|
+
this.holderPid = parseBody(current)?.pid;
|
|
45404
|
+
return "held";
|
|
45405
|
+
}
|
|
45406
|
+
if (current !== void 0) unlinkSync3(this.path);
|
|
45407
|
+
this.env.beforeTakeoverCreate?.();
|
|
45408
|
+
const created = this.create();
|
|
45409
|
+
if (created !== "created") {
|
|
45410
|
+
if (created === "failed") return "failed";
|
|
45411
|
+
try {
|
|
45412
|
+
this.holderPid = parseBody(readFileSync13(this.path, "utf8"))?.pid;
|
|
45413
|
+
} catch {
|
|
45414
|
+
}
|
|
45415
|
+
return "held";
|
|
45416
|
+
}
|
|
45417
|
+
this.log("warn", "bridge.instance_lock_stale_taken_over", stalePid !== void 0 ? { stale_pid: stalePid } : {});
|
|
45418
|
+
return this.took();
|
|
45419
|
+
} catch (error) {
|
|
45420
|
+
this.lastError = error;
|
|
45421
|
+
return "failed";
|
|
45422
|
+
} finally {
|
|
45423
|
+
try {
|
|
45424
|
+
unlinkSync3(guard);
|
|
45425
|
+
} catch {
|
|
45426
|
+
}
|
|
45427
|
+
}
|
|
45428
|
+
}
|
|
45429
|
+
};
|
|
45430
|
+
|
|
44793
45431
|
// src/grants/pin-command.ts
|
|
44794
45432
|
import {
|
|
44795
|
-
closeSync as
|
|
45433
|
+
closeSync as closeSync5,
|
|
44796
45434
|
fchmodSync,
|
|
44797
|
-
fsyncSync,
|
|
45435
|
+
fsyncSync as fsyncSync2,
|
|
44798
45436
|
lstatSync as lstatSync8,
|
|
44799
|
-
openSync as
|
|
44800
|
-
readFileSync as
|
|
45437
|
+
openSync as openSync5,
|
|
45438
|
+
readFileSync as readFileSync14,
|
|
44801
45439
|
realpathSync as realpathSync2,
|
|
44802
45440
|
renameSync as renameSync5,
|
|
44803
|
-
unlinkSync as
|
|
44804
|
-
writeSync as
|
|
45441
|
+
unlinkSync as unlinkSync4,
|
|
45442
|
+
writeSync as writeSync3
|
|
44805
45443
|
} from "node:fs";
|
|
44806
|
-
import { dirname as
|
|
45444
|
+
import { dirname as dirname12, join as join23, resolve as resolve2 } from "node:path";
|
|
44807
45445
|
var PIN_COPY = Object.freeze({
|
|
44808
45446
|
saved: "pin saved. Restart the Connector to use it.",
|
|
44809
45447
|
// [COPY, approved by Eric 2026-10-12]
|
|
@@ -44834,27 +45472,27 @@ function isSymlink(path) {
|
|
|
44834
45472
|
}
|
|
44835
45473
|
}
|
|
44836
45474
|
function readJson(path) {
|
|
44837
|
-
return JSON.parse(
|
|
45475
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
44838
45476
|
}
|
|
44839
45477
|
function publicKeyOf(v) {
|
|
44840
45478
|
const r = parsePinObject(v);
|
|
44841
45479
|
return r.ok ? r.pin.publicKeyB64 : null;
|
|
44842
45480
|
}
|
|
44843
45481
|
function writeTemp(path, bytes) {
|
|
44844
|
-
const fd =
|
|
45482
|
+
const fd = openSync5(path, "wx", 384);
|
|
44845
45483
|
try {
|
|
44846
45484
|
fchmodSync(fd, 384);
|
|
44847
|
-
|
|
44848
|
-
|
|
45485
|
+
writeSync3(fd, bytes);
|
|
45486
|
+
fsyncSync2(fd);
|
|
44849
45487
|
} finally {
|
|
44850
|
-
|
|
45488
|
+
closeSync5(fd);
|
|
44851
45489
|
}
|
|
44852
45490
|
}
|
|
44853
45491
|
function runPinCommand(opts) {
|
|
44854
|
-
const credDir =
|
|
45492
|
+
const credDir = dirname12(resolve2(opts.credentialPath));
|
|
44855
45493
|
const target = join23(credDir, PIN_FILE_NAME);
|
|
44856
45494
|
try {
|
|
44857
|
-
if (realpathSync2(
|
|
45495
|
+
if (realpathSync2(dirname12(resolve2(opts.pinSourcePath))) !== realpathSync2(credDir)) return refuse("directory_mismatch");
|
|
44858
45496
|
} catch {
|
|
44859
45497
|
return refuse("io_error");
|
|
44860
45498
|
}
|
|
@@ -44901,7 +45539,7 @@ function runPinCommand(opts) {
|
|
|
44901
45539
|
const fail = () => {
|
|
44902
45540
|
for (const p of created) {
|
|
44903
45541
|
try {
|
|
44904
|
-
|
|
45542
|
+
unlinkSync4(p);
|
|
44905
45543
|
} catch {
|
|
44906
45544
|
}
|
|
44907
45545
|
}
|
|
@@ -44937,11 +45575,11 @@ function runPinCommand(opts) {
|
|
|
44937
45575
|
created.splice(created.indexOf(credTmp), 1);
|
|
44938
45576
|
step("cred_renamed");
|
|
44939
45577
|
try {
|
|
44940
|
-
const dfd =
|
|
45578
|
+
const dfd = openSync5(credDir, "r");
|
|
44941
45579
|
try {
|
|
44942
|
-
|
|
45580
|
+
fsyncSync2(dfd);
|
|
44943
45581
|
} finally {
|
|
44944
|
-
|
|
45582
|
+
closeSync5(dfd);
|
|
44945
45583
|
}
|
|
44946
45584
|
} catch {
|
|
44947
45585
|
}
|
|
@@ -44997,7 +45635,7 @@ function parseVerb(argv) {
|
|
|
44997
45635
|
return v === "install" || v === "uninstall" || v === "status" || v === "wire" || v === "unwire" || v === "models" || v === "pin" ? v : null;
|
|
44998
45636
|
}
|
|
44999
45637
|
function slugFromCredentialPath(credentialPath) {
|
|
45000
|
-
const dir =
|
|
45638
|
+
const dir = dirname13(credentialPath);
|
|
45001
45639
|
const base = dir.split(/[\\/]/).pop() ?? "";
|
|
45002
45640
|
const m = base.match(/^securecoms-agent-(.+)$/);
|
|
45003
45641
|
if (m) return m[1];
|
|
@@ -45013,7 +45651,7 @@ function realServiceDeps() {
|
|
|
45013
45651
|
},
|
|
45014
45652
|
writeFile: (p, data) => writeFileSync5(p, data, { mode: 420 }),
|
|
45015
45653
|
removeFile: (p) => rmSync8(p, { force: true }),
|
|
45016
|
-
mkdirp: (p) =>
|
|
45654
|
+
mkdirp: (p) => mkdirSync8(p, { recursive: true }),
|
|
45017
45655
|
fileExists: (p) => existsSync7(p),
|
|
45018
45656
|
user: userInfo().username
|
|
45019
45657
|
};
|
|
@@ -45057,10 +45695,10 @@ function realResolveDeps() {
|
|
|
45057
45695
|
}
|
|
45058
45696
|
function realPrepDeps() {
|
|
45059
45697
|
return {
|
|
45060
|
-
readFile: (p) =>
|
|
45698
|
+
readFile: (p) => readFileSync16(p, "utf8"),
|
|
45061
45699
|
writeFileMode: (p, data, mode) => writeFileSync5(p, data, { mode }),
|
|
45062
45700
|
mkdirp: (p, mode) => {
|
|
45063
|
-
|
|
45701
|
+
mkdirSync8(p, { recursive: true, mode: mode ?? 493 });
|
|
45064
45702
|
},
|
|
45065
45703
|
fileExists: (p) => existsSync7(p),
|
|
45066
45704
|
realpath: (p) => realpathSync3(p),
|
|
@@ -45080,25 +45718,22 @@ function realPrepDeps() {
|
|
|
45080
45718
|
]);
|
|
45081
45719
|
return r.status === 0;
|
|
45082
45720
|
},
|
|
45083
|
-
|
|
45084
|
-
const r = spawnSync(process.execPath, [
|
|
45085
|
-
"-e",
|
|
45086
|
-
`const s=require('net').connect(${port},'127.0.0.1');s.once('connect',()=>{s.destroy();process.exit(0)});s.once('error',()=>process.exit(1));setTimeout(()=>process.exit(1),500);`
|
|
45087
|
-
]);
|
|
45088
|
-
return r.status === 0;
|
|
45089
|
-
},
|
|
45721
|
+
getHealth: realGetHealth,
|
|
45090
45722
|
// No unref: an unref'd timer would let the process exit mid-verification.
|
|
45091
|
-
sleep: (ms) => new Promise((res) => setTimeout(res, ms))
|
|
45723
|
+
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
|
|
45724
|
+
now: () => Date.now()
|
|
45092
45725
|
};
|
|
45093
45726
|
}
|
|
45094
45727
|
function realGetHealth(port) {
|
|
45095
45728
|
const r = spawnSync(process.execPath, [
|
|
45096
45729
|
"-e",
|
|
45097
|
-
`const http=require('http');const q=http.get({host:'127.0.0.1',port:${port},path:'/healthz',timeout:800},res=>{let d='';res.on('data',c=>d+=c);res.on('end',()=>{process.stdout.write(d);process.exit(0)})});q.on('error',()=>process.exit(1));q.on('timeout',()=>process.exit(1));`
|
|
45098
|
-
], { encoding: "utf8" });
|
|
45730
|
+
`const http=require('http');const q=http.get({host:'127.0.0.1',port:${port},path:'/healthz',timeout:800},res=>{let d='';res.on('data',c=>d+=c);res.on('end',()=>{process.stdout.write(res.statusCode+'\\n'+d);process.exit(0)})});q.on('error',()=>process.exit(1));q.on('timeout',()=>process.exit(1));`
|
|
45731
|
+
], { encoding: "utf8", timeout: 2e3 });
|
|
45099
45732
|
if (r.status !== 0) return null;
|
|
45733
|
+
const nl = r.stdout.indexOf("\n");
|
|
45734
|
+
if (nl < 0 || r.stdout.slice(0, nl) !== "200") return null;
|
|
45100
45735
|
try {
|
|
45101
|
-
return JSON.parse(r.stdout);
|
|
45736
|
+
return JSON.parse(r.stdout.slice(nl + 1));
|
|
45102
45737
|
} catch {
|
|
45103
45738
|
return null;
|
|
45104
45739
|
}
|
|
@@ -45113,19 +45748,12 @@ function realReadFleet() {
|
|
|
45113
45748
|
return [];
|
|
45114
45749
|
}
|
|
45115
45750
|
},
|
|
45116
|
-
readFile: (p) =>
|
|
45751
|
+
readFile: (p) => readFileSync16(p, "utf8")
|
|
45117
45752
|
});
|
|
45118
45753
|
}
|
|
45119
45754
|
function runStatus() {
|
|
45120
45755
|
const deps = {
|
|
45121
45756
|
readFleet: realReadFleet,
|
|
45122
|
-
probeConnect: (port) => {
|
|
45123
|
-
const r = spawnSync(process.execPath, [
|
|
45124
|
-
"-e",
|
|
45125
|
-
`const s=require('net').connect(${port},'127.0.0.1');s.once('connect',()=>{s.destroy();process.exit(0)});s.once('error',()=>process.exit(1));setTimeout(()=>process.exit(1),500);`
|
|
45126
|
-
]);
|
|
45127
|
-
return r.status === 0;
|
|
45128
|
-
},
|
|
45129
45757
|
getHealth: realGetHealth
|
|
45130
45758
|
};
|
|
45131
45759
|
const rows = readFleetStatus(deps);
|
|
@@ -45135,26 +45763,26 @@ function runStatus() {
|
|
|
45135
45763
|
}
|
|
45136
45764
|
function realCopyConnector(srcDir, dstDir, harness) {
|
|
45137
45765
|
const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
|
|
45138
|
-
|
|
45766
|
+
mkdirSync8(dstDir, { recursive: true });
|
|
45139
45767
|
for (const name of readdirSync4(srcDir)) {
|
|
45140
45768
|
const srcPath = join25(srcDir, name);
|
|
45141
|
-
if (
|
|
45769
|
+
if (statSync8(srcPath).isDirectory()) {
|
|
45142
45770
|
if (CONNECTOR_SKIP_DIRS.has(name)) continue;
|
|
45143
45771
|
realCopyConnector(srcPath, join25(dstDir, name), harness);
|
|
45144
45772
|
continue;
|
|
45145
45773
|
}
|
|
45146
45774
|
if (name.includes(".test.")) continue;
|
|
45147
45775
|
if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
|
|
45148
|
-
writeFileSync5(join25(dstDir, name),
|
|
45776
|
+
writeFileSync5(join25(dstDir, name), readFileSync16(srcPath));
|
|
45149
45777
|
}
|
|
45150
45778
|
}
|
|
45151
45779
|
function realWriteBinding(dstDir, identityId) {
|
|
45152
|
-
|
|
45780
|
+
mkdirSync8(dstDir, { recursive: true });
|
|
45153
45781
|
writeFileSync5(join25(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
|
|
45154
45782
|
}
|
|
45155
45783
|
function realReadBinding(dstDir) {
|
|
45156
45784
|
try {
|
|
45157
|
-
const parsed = JSON.parse(
|
|
45785
|
+
const parsed = JSON.parse(readFileSync16(join25(dstDir, "securecoms.binding.json"), "utf8"));
|
|
45158
45786
|
return parsed.identity_id ?? null;
|
|
45159
45787
|
} catch {
|
|
45160
45788
|
return null;
|
|
@@ -45165,14 +45793,14 @@ function realRemoveBinding(dstDir) {
|
|
|
45165
45793
|
}
|
|
45166
45794
|
function realStatDir(p) {
|
|
45167
45795
|
try {
|
|
45168
|
-
return
|
|
45796
|
+
return statSync8(p).isDirectory();
|
|
45169
45797
|
} catch {
|
|
45170
45798
|
return false;
|
|
45171
45799
|
}
|
|
45172
45800
|
}
|
|
45173
45801
|
function realReadHermesConfig(p) {
|
|
45174
45802
|
try {
|
|
45175
|
-
return
|
|
45803
|
+
return readFileSync16(p, "utf8");
|
|
45176
45804
|
} catch {
|
|
45177
45805
|
return null;
|
|
45178
45806
|
}
|
|
@@ -45293,12 +45921,15 @@ async function runServiceVerb(verb) {
|
|
|
45293
45921
|
}
|
|
45294
45922
|
process.exit(0);
|
|
45295
45923
|
} catch (e) {
|
|
45296
|
-
|
|
45297
|
-
process.stderr.write(`${raw ? e.message : `service ${verb} failed: ${e.message ?? e}`}
|
|
45298
|
-
`);
|
|
45924
|
+
process.stderr.write(formatServiceError(verb, e));
|
|
45299
45925
|
process.exit(1);
|
|
45300
45926
|
}
|
|
45301
45927
|
}
|
|
45928
|
+
function formatServiceError(verb, e) {
|
|
45929
|
+
const raw = e instanceof ServiceUnsupportedError || e instanceof GlobalInstallPermissionError || e instanceof InstallRefusedError;
|
|
45930
|
+
return `${raw ? e.message : `service ${verb} failed: ${e.message ?? e}`}
|
|
45931
|
+
`;
|
|
45932
|
+
}
|
|
45302
45933
|
async function startThenServe(agent, opts) {
|
|
45303
45934
|
await agent.start();
|
|
45304
45935
|
const tls = opts.readTls();
|
|
@@ -45330,6 +45961,216 @@ async function runModelsVerb() {
|
|
|
45330
45961
|
function runPinVerb() {
|
|
45331
45962
|
process.exit(runPinCli(process.argv, { out: (l) => process.stdout.write(l + "\n"), err: (l) => process.stderr.write(l + "\n") }));
|
|
45332
45963
|
}
|
|
45964
|
+
var CANONICAL_UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
45965
|
+
var HELD_POLL_TOTAL_MS = 1e4;
|
|
45966
|
+
var HELD_POLL_INTERVAL_MS = 500;
|
|
45967
|
+
var HELD_NO_PID_REACQUIRES = 3;
|
|
45968
|
+
var SHUTDOWN_HARD_EXIT_MS = 8e3;
|
|
45969
|
+
function realRunModeDeps(credentialPath, storePath) {
|
|
45970
|
+
return {
|
|
45971
|
+
credentialPath,
|
|
45972
|
+
storePath,
|
|
45973
|
+
argv: process.argv,
|
|
45974
|
+
env: process.env,
|
|
45975
|
+
home: homedir5(),
|
|
45976
|
+
newLock: (path) => new InstanceLock(path, void 0, writeServiceLog),
|
|
45977
|
+
// 7b Task 19 fix M2: the production construction owns the machine paths (heartbeat, model marker and GC).
|
|
45978
|
+
newAgent: (cfg) => new BridgeAgent(cfg, { hostMachine: true }),
|
|
45979
|
+
net: { createServer: netCreateServer },
|
|
45980
|
+
serve: startThenServe,
|
|
45981
|
+
now: () => Date.now(),
|
|
45982
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
45983
|
+
setHardExitTimer: (cb, ms) => {
|
|
45984
|
+
const t = setTimeout(cb, ms);
|
|
45985
|
+
t.unref?.();
|
|
45986
|
+
return () => clearTimeout(t);
|
|
45987
|
+
},
|
|
45988
|
+
onSignal: (signal, handler) => {
|
|
45989
|
+
process.on(signal, handler);
|
|
45990
|
+
},
|
|
45991
|
+
onExit: (handler) => {
|
|
45992
|
+
process.once("exit", handler);
|
|
45993
|
+
},
|
|
45994
|
+
exit: (code) => process.exit(code),
|
|
45995
|
+
print: (line) => {
|
|
45996
|
+
process.stderr.write(`${line}
|
|
45997
|
+
`);
|
|
45998
|
+
},
|
|
45999
|
+
log: writeServiceLog
|
|
46000
|
+
};
|
|
46001
|
+
}
|
|
46002
|
+
async function runMode(deps) {
|
|
46003
|
+
const { credentialPath, storePath, log } = deps;
|
|
46004
|
+
let cfg, daemon;
|
|
46005
|
+
try {
|
|
46006
|
+
cfg = loadConfig({ credentialPath, storePath });
|
|
46007
|
+
daemon = parseDaemonArgs(deps.argv, deps.env);
|
|
46008
|
+
validateBind(daemon.bind, daemon.tls);
|
|
46009
|
+
} catch (e) {
|
|
46010
|
+
log("error", "startup.config_invalid", {}, e);
|
|
46011
|
+
return deps.exit(1);
|
|
46012
|
+
}
|
|
46013
|
+
if (!CANONICAL_UUID2.test(cfg.identityId)) {
|
|
46014
|
+
log("error", "startup.config_invalid", { field: ServiceLogEnum.of("identity_id", ["identity_id"]) });
|
|
46015
|
+
return deps.exit(1);
|
|
46016
|
+
}
|
|
46017
|
+
const lockDir = join25(deps.home, ".securecoms", "locks");
|
|
46018
|
+
const lockPath = join25(lockDir, `${cfg.identityId.toLowerCase()}.lock`);
|
|
46019
|
+
try {
|
|
46020
|
+
mkdirSync8(lockDir, { recursive: true, mode: 448 });
|
|
46021
|
+
chmodSync6(lockDir, 448);
|
|
46022
|
+
} catch (e) {
|
|
46023
|
+
log("error", "startup.instance_lock_failed", {}, e);
|
|
46024
|
+
return deps.exit(1);
|
|
46025
|
+
}
|
|
46026
|
+
const lock = deps.newLock(lockPath);
|
|
46027
|
+
let got = lock.acquire();
|
|
46028
|
+
const pollStart = deps.now();
|
|
46029
|
+
while (got.result === "held" && deps.now() - pollStart < HELD_POLL_TOTAL_MS) {
|
|
46030
|
+
await deps.sleep(HELD_POLL_INTERVAL_MS);
|
|
46031
|
+
got = lock.acquire();
|
|
46032
|
+
}
|
|
46033
|
+
for (let i = 0; i < HELD_NO_PID_REACQUIRES && got.result === "held" && got.holderPid === void 0; i++) {
|
|
46034
|
+
await deps.sleep(HELD_POLL_INTERVAL_MS);
|
|
46035
|
+
got = lock.acquire();
|
|
46036
|
+
}
|
|
46037
|
+
switch (got.result) {
|
|
46038
|
+
case "acquired":
|
|
46039
|
+
break;
|
|
46040
|
+
case "held":
|
|
46041
|
+
if (got.holderPid === void 0) {
|
|
46042
|
+
log("error", "startup.instance_lock_busy");
|
|
46043
|
+
return deps.exit(1);
|
|
46044
|
+
}
|
|
46045
|
+
log("info", "startup.already_running");
|
|
46046
|
+
deps.print(`This agent is already running on this computer (process ${got.holderPid}). Stop that copy first, or run: agent-bridge status`);
|
|
46047
|
+
return deps.exit(0);
|
|
46048
|
+
case "foreign_host":
|
|
46049
|
+
log("error", "startup.instance_lock_foreign_host", {
|
|
46050
|
+
lock_path: ServiceLogEnum.of("home_securecoms_locks_identity", ["home_securecoms_locks_identity"]),
|
|
46051
|
+
remedy: ServiceLogEnum.of("delete_lock_if_no_other_host", ["delete_lock_if_no_other_host"])
|
|
46052
|
+
});
|
|
46053
|
+
deps.print(`This agent's lock file was written under a different computer name: ${lockPath}. If no other computer runs this agent, delete that file and start the agent again.`);
|
|
46054
|
+
return deps.exit(1);
|
|
46055
|
+
case "unreadable":
|
|
46056
|
+
log("error", "startup.instance_lock_unreadable", {}, lock.lastError);
|
|
46057
|
+
return deps.exit(1);
|
|
46058
|
+
case "busy":
|
|
46059
|
+
log("error", "startup.instance_lock_busy");
|
|
46060
|
+
return deps.exit(1);
|
|
46061
|
+
case "failed":
|
|
46062
|
+
log("error", "startup.instance_lock_failed", {}, lock.lastError);
|
|
46063
|
+
return deps.exit(1);
|
|
46064
|
+
}
|
|
46065
|
+
deps.onExit(() => lock.release());
|
|
46066
|
+
const tokenFile = daemon.tokenFile ?? join25(dirname13(credentialPath), "bridge-token");
|
|
46067
|
+
const token = resolveToken({ tokenFile, token: daemon.token });
|
|
46068
|
+
const preBound = deps.net.createServer((sock) => sock.destroy());
|
|
46069
|
+
const bindError = await new Promise((resolveBind) => {
|
|
46070
|
+
const onError = (e) => resolveBind(e);
|
|
46071
|
+
preBound.once("error", onError);
|
|
46072
|
+
preBound.listen(daemon.port, daemon.bind, () => {
|
|
46073
|
+
preBound.removeListener("error", onError);
|
|
46074
|
+
resolveBind(void 0);
|
|
46075
|
+
});
|
|
46076
|
+
});
|
|
46077
|
+
if (bindError) {
|
|
46078
|
+
if (bindError.code === "EADDRINUSE") {
|
|
46079
|
+
log("error", "startup.port_in_use", { port: daemon.port });
|
|
46080
|
+
deps.print(`Port ${daemon.port} is already in use by another program on this computer, so the agent did not start.`);
|
|
46081
|
+
} else {
|
|
46082
|
+
log("error", "startup.listen_failed", {}, bindError);
|
|
46083
|
+
}
|
|
46084
|
+
lock.release();
|
|
46085
|
+
return deps.exit(1);
|
|
46086
|
+
}
|
|
46087
|
+
const agent = deps.newAgent(cfg);
|
|
46088
|
+
const instanceId = newInstanceId();
|
|
46089
|
+
const identityId = cfg.identityId;
|
|
46090
|
+
let served = null;
|
|
46091
|
+
let superseded = false;
|
|
46092
|
+
const stopIngress = (s) => {
|
|
46093
|
+
s.agentWs.enterSessionSuperseded();
|
|
46094
|
+
s.closeAllSseStreams();
|
|
46095
|
+
};
|
|
46096
|
+
agent.on("session_superseded", () => {
|
|
46097
|
+
if (superseded) return;
|
|
46098
|
+
superseded = true;
|
|
46099
|
+
if (served) stopIngress(served);
|
|
46100
|
+
log("warn", "transport.session_superseded");
|
|
46101
|
+
deps.print("This agent connected from another place, so this copy stopped. Stop the other copy, then restart this one.");
|
|
46102
|
+
void (async () => {
|
|
46103
|
+
try {
|
|
46104
|
+
await agent.close();
|
|
46105
|
+
} catch (e) {
|
|
46106
|
+
log("error", "transport.session_superseded_close_failed", {}, e);
|
|
46107
|
+
}
|
|
46108
|
+
removeConnectionFileIfOwned(deps.home, identityId, instanceId);
|
|
46109
|
+
})();
|
|
46110
|
+
});
|
|
46111
|
+
let exiting = false;
|
|
46112
|
+
const closeReleaseExit = () => {
|
|
46113
|
+
if (exiting) return;
|
|
46114
|
+
exiting = true;
|
|
46115
|
+
const cancelHard = deps.setHardExitTimer(() => {
|
|
46116
|
+
lock.release();
|
|
46117
|
+
deps.exit(0);
|
|
46118
|
+
}, SHUTDOWN_HARD_EXIT_MS);
|
|
46119
|
+
void agent.close().finally(() => {
|
|
46120
|
+
cancelHard();
|
|
46121
|
+
lock.release();
|
|
46122
|
+
deps.exit(0);
|
|
46123
|
+
});
|
|
46124
|
+
};
|
|
46125
|
+
try {
|
|
46126
|
+
served = await deps.serve(agent, {
|
|
46127
|
+
token,
|
|
46128
|
+
allowOrigins: daemon.allowOrigins,
|
|
46129
|
+
readTls: () => daemon.tls ? { cert: readFileSync16(daemon.tlsCert, "utf8"), key: readFileSync16(daemon.tlsKey, "utf8") } : void 0
|
|
46130
|
+
});
|
|
46131
|
+
} catch (e) {
|
|
46132
|
+
if (!(e instanceof SessionSupersededError)) throw e;
|
|
46133
|
+
deps.onSignal("SIGINT", closeReleaseExit);
|
|
46134
|
+
deps.onSignal("SIGTERM", closeReleaseExit);
|
|
46135
|
+
return;
|
|
46136
|
+
}
|
|
46137
|
+
if (superseded) stopIngress(served);
|
|
46138
|
+
const { server, closeAllSseStreams, agentWs } = served;
|
|
46139
|
+
let down = false;
|
|
46140
|
+
const shutdown = () => {
|
|
46141
|
+
if (down) return;
|
|
46142
|
+
down = true;
|
|
46143
|
+
agentWs.close();
|
|
46144
|
+
server.close();
|
|
46145
|
+
closeAllSseStreams();
|
|
46146
|
+
closeReleaseExit();
|
|
46147
|
+
};
|
|
46148
|
+
deps.onSignal("SIGINT", shutdown);
|
|
46149
|
+
deps.onSignal("SIGTERM", shutdown);
|
|
46150
|
+
const slug = slugFromCredentialPath(credentialPath);
|
|
46151
|
+
deps.onExit(() => removeConnectionFileIfOwned(deps.home, identityId, instanceId));
|
|
46152
|
+
server.listen(preBound, () => {
|
|
46153
|
+
log("info", "startup.listening", { port: daemon.port, tls: !!daemon.tls });
|
|
46154
|
+
log("info", "startup.token_loaded");
|
|
46155
|
+
const addr = server.address();
|
|
46156
|
+
const boundPort = typeof addr === "object" && addr ? addr.port : daemon.port;
|
|
46157
|
+
if (superseded) return;
|
|
46158
|
+
try {
|
|
46159
|
+
writeConnectionFile(deps.home, buildConnectionFile({
|
|
46160
|
+
identityId,
|
|
46161
|
+
port: boundPort,
|
|
46162
|
+
tokenFile,
|
|
46163
|
+
slug,
|
|
46164
|
+
label: slug,
|
|
46165
|
+
instanceId,
|
|
46166
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
46167
|
+
}));
|
|
46168
|
+
log("info", "startup.connection_file_written");
|
|
46169
|
+
} catch (e) {
|
|
46170
|
+
log("warn", "startup.connection_file_write_failed", {}, e);
|
|
46171
|
+
}
|
|
46172
|
+
});
|
|
46173
|
+
}
|
|
45333
46174
|
async function main() {
|
|
45334
46175
|
if (!nodeMajorOk(process.versions.node)) {
|
|
45335
46176
|
writeServiceLog("error", "startup.node_unsupported");
|
|
@@ -45362,63 +46203,7 @@ async function main() {
|
|
|
45362
46203
|
process.exit(2);
|
|
45363
46204
|
}
|
|
45364
46205
|
const storePath = argOrEnv("--store", "SECURECOMS_STORE");
|
|
45365
|
-
|
|
45366
|
-
try {
|
|
45367
|
-
cfg = loadConfig({ credentialPath, storePath });
|
|
45368
|
-
daemon = parseDaemonArgs(process.argv, process.env);
|
|
45369
|
-
validateBind(daemon.bind, daemon.tls);
|
|
45370
|
-
} catch (e) {
|
|
45371
|
-
writeServiceLog("error", "startup.config_invalid", {}, e);
|
|
45372
|
-
process.exit(1);
|
|
45373
|
-
}
|
|
45374
|
-
const tokenFile = daemon.tokenFile ?? join25(dirname12(credentialPath), "bridge-token");
|
|
45375
|
-
const token = resolveToken({ tokenFile, token: daemon.token });
|
|
45376
|
-
const agent = new BridgeAgent(cfg, { hostMachine: true });
|
|
45377
|
-
const { server, closeAllSseStreams, agentWs } = await startThenServe(agent, {
|
|
45378
|
-
token,
|
|
45379
|
-
allowOrigins: daemon.allowOrigins,
|
|
45380
|
-
readTls: () => daemon.tls ? { cert: readFileSync15(daemon.tlsCert, "utf8"), key: readFileSync15(daemon.tlsKey, "utf8") } : void 0
|
|
45381
|
-
});
|
|
45382
|
-
let down = false;
|
|
45383
|
-
const shutdown = () => {
|
|
45384
|
-
if (down) return;
|
|
45385
|
-
down = true;
|
|
45386
|
-
agentWs.close();
|
|
45387
|
-
server.close();
|
|
45388
|
-
closeAllSseStreams();
|
|
45389
|
-
const hard = setTimeout(() => process.exit(0), 8e3);
|
|
45390
|
-
hard.unref?.();
|
|
45391
|
-
void agent.close().finally(() => {
|
|
45392
|
-
clearTimeout(hard);
|
|
45393
|
-
process.exit(0);
|
|
45394
|
-
});
|
|
45395
|
-
};
|
|
45396
|
-
process.on("SIGINT", shutdown);
|
|
45397
|
-
process.on("SIGTERM", shutdown);
|
|
45398
|
-
const instanceId = newInstanceId();
|
|
45399
|
-
const identityId = agent.health().identity_id;
|
|
45400
|
-
const slug = slugFromCredentialPath(credentialPath);
|
|
45401
|
-
process.once("exit", () => removeConnectionFileIfOwned(homedir5(), identityId, instanceId));
|
|
45402
|
-
server.listen(daemon.port, daemon.bind, () => {
|
|
45403
|
-
writeServiceLog("info", "startup.listening", { port: daemon.port, tls: !!daemon.tls });
|
|
45404
|
-
writeServiceLog("info", "startup.token_loaded");
|
|
45405
|
-
const addr = server.address();
|
|
45406
|
-
const boundPort = typeof addr === "object" && addr ? addr.port : daemon.port;
|
|
45407
|
-
try {
|
|
45408
|
-
writeConnectionFile(homedir5(), buildConnectionFile({
|
|
45409
|
-
identityId,
|
|
45410
|
-
port: boundPort,
|
|
45411
|
-
tokenFile,
|
|
45412
|
-
slug,
|
|
45413
|
-
label: slug,
|
|
45414
|
-
instanceId,
|
|
45415
|
-
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
45416
|
-
}));
|
|
45417
|
-
writeServiceLog("info", "startup.connection_file_written");
|
|
45418
|
-
} catch (e) {
|
|
45419
|
-
writeServiceLog("warn", "startup.connection_file_write_failed", {}, e);
|
|
45420
|
-
}
|
|
45421
|
-
});
|
|
46206
|
+
await runMode(realRunModeDeps(credentialPath, storePath));
|
|
45422
46207
|
}
|
|
45423
46208
|
var here = fileURLToPath4(import.meta.url);
|
|
45424
46209
|
var isMain = !!process.argv[1] && refersToSameFile(here, process.argv[1], realpathSync3);
|
|
@@ -45429,10 +46214,17 @@ if (isMain) {
|
|
|
45429
46214
|
});
|
|
45430
46215
|
}
|
|
45431
46216
|
export {
|
|
46217
|
+
HELD_NO_PID_REACQUIRES,
|
|
46218
|
+
HELD_POLL_INTERVAL_MS,
|
|
46219
|
+
HELD_POLL_TOTAL_MS,
|
|
46220
|
+
SHUTDOWN_HARD_EXIT_MS,
|
|
45432
46221
|
chooseInstallExecPath,
|
|
46222
|
+
formatServiceError,
|
|
45433
46223
|
nodeMajorOk,
|
|
45434
46224
|
parseVerb,
|
|
45435
46225
|
realCopyConnector,
|
|
46226
|
+
realGetHealth,
|
|
46227
|
+
runMode,
|
|
45436
46228
|
slugFromCredentialPath,
|
|
45437
46229
|
startThenServe,
|
|
45438
46230
|
tokenFingerprint
|