@securecoms/agent-bridge 0.30.0 → 0.30.2
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 +77 -6
- package/dist/bin.d.ts +101 -0
- package/dist/bin.js +1102 -161
- package/dist/bridge-agent.d.ts +60 -0
- package/dist/config.d.ts +13 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.js +335 -27
- package/dist/preflight-upgrade.d.cts +0 -2
- package/dist/preflight.cjs +51 -49
- package/dist/preflight.d.cts +14 -0
- package/dist/service/credential-arg.d.ts +23 -0
- package/dist/service/install-prep.d.ts +21 -1
- package/dist/service/install.d.ts +7 -0
- package/dist/service/instance-lock.d.ts +88 -0
- package/dist/service/launchd.d.ts +1 -1
- package/dist/service/resolve-node.d.ts +1 -0
- 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 +2 -2
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
|
}
|
|
@@ -8001,8 +8001,30 @@ function ackOnlyGreeting(agentName, callerName) {
|
|
|
8001
8001
|
|
|
8002
8002
|
// src/config.ts
|
|
8003
8003
|
var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
8004
|
+
var CONFIG_ERROR_MESSAGE = {
|
|
8005
|
+
credential_not_found: "credential file not found",
|
|
8006
|
+
credential_unreadable: "credential file could not be read",
|
|
8007
|
+
credential_not_json: "credential file is not valid JSON",
|
|
8008
|
+
credential_missing_field: "credential file missing required field",
|
|
8009
|
+
credential_voice_invalid: "credential file voice config invalid"
|
|
8010
|
+
};
|
|
8011
|
+
var ConfigError = class extends Error {
|
|
8012
|
+
constructor(reason, field, source) {
|
|
8013
|
+
super(CONFIG_ERROR_MESSAGE[reason] + (field ? `: ${field}` : ""));
|
|
8014
|
+
this.reason = reason;
|
|
8015
|
+
this.field = field;
|
|
8016
|
+
this.source = source;
|
|
8017
|
+
this.name = "ConfigError";
|
|
8018
|
+
}
|
|
8019
|
+
};
|
|
8004
8020
|
function loadConfig(opts) {
|
|
8005
|
-
|
|
8021
|
+
let raw;
|
|
8022
|
+
try {
|
|
8023
|
+
raw = readFileSync2(opts.credentialPath, "utf8");
|
|
8024
|
+
} catch (e) {
|
|
8025
|
+
const code = e?.code;
|
|
8026
|
+
throw new ConfigError(code === "ENOENT" ? "credential_not_found" : "credential_unreadable");
|
|
8027
|
+
}
|
|
8006
8028
|
try {
|
|
8007
8029
|
const mode = statSync(opts.credentialPath).mode & 511;
|
|
8008
8030
|
if (mode & 63) writeServiceLog("warn", "config.credential_permissions_unsafe");
|
|
@@ -8012,9 +8034,9 @@ function loadConfig(opts) {
|
|
|
8012
8034
|
try {
|
|
8013
8035
|
json2 = JSON.parse(raw);
|
|
8014
8036
|
} catch {
|
|
8015
|
-
throw new
|
|
8037
|
+
throw new ConfigError("credential_not_json");
|
|
8016
8038
|
}
|
|
8017
|
-
for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new
|
|
8039
|
+
for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new ConfigError("credential_missing_field", k);
|
|
8018
8040
|
const storePath = opts.storePath ?? join2(dirname2(opts.credentialPath), "bridge-state.db");
|
|
8019
8041
|
const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
|
|
8020
8042
|
writeServiceLog(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
|
|
@@ -8052,8 +8074,14 @@ function loadConfig(opts) {
|
|
|
8052
8074
|
let voice;
|
|
8053
8075
|
try {
|
|
8054
8076
|
voice = parseVoiceConfig(json2.voice, process.env);
|
|
8055
|
-
} catch
|
|
8056
|
-
|
|
8077
|
+
} catch {
|
|
8078
|
+
let source = "env";
|
|
8079
|
+
try {
|
|
8080
|
+
parseVoiceConfig(json2.voice, {});
|
|
8081
|
+
} catch {
|
|
8082
|
+
source = "file";
|
|
8083
|
+
}
|
|
8084
|
+
throw new ConfigError("credential_voice_invalid", void 0, source);
|
|
8057
8085
|
}
|
|
8058
8086
|
const modelSource = json2.memory?.model_source === "import_only" ? "import_only" : "download";
|
|
8059
8087
|
const enabledRaw = json2.memory?.enabled;
|
|
@@ -18169,7 +18197,13 @@ var CODES = [
|
|
|
18169
18197
|
// lookup missed and before any write, so a retry can never land: the
|
|
18170
18198
|
// pending commit is dropped instead of being resent forever. The same pair
|
|
18171
18199
|
// answers an application send into a deleted channel (#325 F5).
|
|
18172
|
-
{ status: 409, code: "channel_unavailable" }
|
|
18200
|
+
{ status: 409, code: "channel_unavailable" },
|
|
18201
|
+
// ADR-0023 B.1: an Add or Welcome into a humans-only enclave's group naming
|
|
18202
|
+
// a device that is not provably a person's. Refused after the replay lookup
|
|
18203
|
+
// missed and before any write, so nothing landed. Definite: the committer's
|
|
18204
|
+
// work cannot name an agent in a humans-only enclave (admission refuses
|
|
18205
|
+
// one), so dropping the commit starts no resend loop.
|
|
18206
|
+
{ status: 403, code: "humans_only_enclave" }
|
|
18173
18207
|
];
|
|
18174
18208
|
var DISTRIBUTE_DEFINITE_CODES = Object.freeze(CODES.map((c) => Object.freeze({ ...c })));
|
|
18175
18209
|
var DEFINITE_KEYS = new Set(DISTRIBUTE_DEFINITE_CODES.map((c) => `${c.status} ${c.code}`));
|
|
@@ -24803,6 +24837,7 @@ function base64UrlNoPad(s) {
|
|
|
24803
24837
|
}
|
|
24804
24838
|
var BEARER_SUBPROTOCOL = "securecoms.bearer.v1";
|
|
24805
24839
|
var WS_CLOSE_IDENTITY_REVOKED = 4006;
|
|
24840
|
+
var WS_CLOSE_SESSION_SUPERSEDED = 4409;
|
|
24806
24841
|
function isIdentityRevokedClose(code, reason) {
|
|
24807
24842
|
return code === WS_CLOSE_IDENTITY_REVOKED || reason === "identity_revoked";
|
|
24808
24843
|
}
|
|
@@ -25018,7 +25053,7 @@ function openDeliveryPings(o) {
|
|
|
25018
25053
|
end();
|
|
25019
25054
|
};
|
|
25020
25055
|
if (!hardened) {
|
|
25021
|
-
if (code === 4001 || isIdentityRevokedClose(code, reason)) {
|
|
25056
|
+
if (code === 4001 || code === WS_CLOSE_SESSION_SUPERSEDED || isIdentityRevokedClose(code, reason)) {
|
|
25022
25057
|
terminalClose();
|
|
25023
25058
|
return;
|
|
25024
25059
|
}
|
|
@@ -25027,7 +25062,7 @@ function openDeliveryPings(o) {
|
|
|
25027
25062
|
}
|
|
25028
25063
|
const tr = terminalReason;
|
|
25029
25064
|
terminalReason = void 0;
|
|
25030
|
-
if (code === 4006 || tr && code === 4001) {
|
|
25065
|
+
if (code === 4006 || code === WS_CLOSE_SESSION_SUPERSEDED || tr && code === 4001) {
|
|
25031
25066
|
lifecycle("terminal", tr ?? `code ${code}`);
|
|
25032
25067
|
terminalClose();
|
|
25033
25068
|
return;
|
|
@@ -25184,10 +25219,14 @@ var RestTransport = class {
|
|
|
25184
25219
|
f;
|
|
25185
25220
|
ws;
|
|
25186
25221
|
tokenProvider;
|
|
25187
|
-
/** The
|
|
25188
|
-
* cleared
|
|
25189
|
-
* a no-op when no subscription is active. */
|
|
25222
|
+
/** The shared delivery-ping socket's send, set while that socket is open and
|
|
25223
|
+
* cleared when it closes (last subscriber gone, or terminal). Control frames
|
|
25224
|
+
* (status keepalive) route through it; a no-op when no subscription is active. */
|
|
25190
25225
|
_controlSend = null;
|
|
25226
|
+
/** P-5: the one delivery-ping stream this transport holds, shared by every subscriber. */
|
|
25227
|
+
pingHub = null;
|
|
25228
|
+
/** Set once the shared stream ends on a terminal close; no socket is ever reopened. */
|
|
25229
|
+
pingsTerminal = false;
|
|
25191
25230
|
constructor(opts) {
|
|
25192
25231
|
this.opts = opts;
|
|
25193
25232
|
this.f = opts.fetchImpl ?? fetch;
|
|
@@ -25289,25 +25328,52 @@ var RestTransport = class {
|
|
|
25289
25328
|
idempotentReplay: j.idempotent_replay === true
|
|
25290
25329
|
};
|
|
25291
25330
|
}
|
|
25331
|
+
/** One device-wide delivery-ping socket per transport (P-5): the backend
|
|
25332
|
+
* supersedes a second socket for the same device, so every subscriber shares
|
|
25333
|
+
* one `openDeliveryPings` handle. Each gets its own bounded queue; its close()
|
|
25334
|
+
* detaches only itself; the socket closes with the last subscriber and a later
|
|
25335
|
+
* subscribe opens a fresh one. After a terminal close the transport is
|
|
25336
|
+
* terminal: later calls open no socket and get an already-ended iterator. */
|
|
25292
25337
|
subscribeDeliveryPings(_channelId) {
|
|
25338
|
+
if (this.pingsTerminal) {
|
|
25339
|
+
const ended = new PingSubscriber();
|
|
25340
|
+
ended.end();
|
|
25341
|
+
return { pings: ended.pings, close: () => {
|
|
25342
|
+
}, send: () => {
|
|
25343
|
+
} };
|
|
25344
|
+
}
|
|
25345
|
+
const hub = this.pingHub ??= this.openPingHub();
|
|
25346
|
+
const sub = hub.attach();
|
|
25347
|
+
return { pings: sub.pings, close: () => hub.detach(sub), send: hub.send };
|
|
25348
|
+
}
|
|
25349
|
+
openPingHub() {
|
|
25293
25350
|
let wsUrl = this.opts.baseUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/api/v1/ws";
|
|
25294
25351
|
if (this.opts.connectIdentityId) {
|
|
25295
25352
|
wsUrl += `?connect_identity_id=${encodeURIComponent(this.opts.connectIdentityId)}`;
|
|
25296
25353
|
}
|
|
25297
|
-
const
|
|
25354
|
+
const hub = new PingHub(openDeliveryPings({
|
|
25298
25355
|
url: wsUrl,
|
|
25299
25356
|
tokenProvider: this.tokenProvider,
|
|
25300
25357
|
wsImpl: this.ws,
|
|
25301
25358
|
hardened: this.opts.hardenedReconnect ?? false,
|
|
25302
|
-
onLifecycle: this.opts.onWsLifecycle
|
|
25303
|
-
|
|
25304
|
-
|
|
25305
|
-
|
|
25306
|
-
|
|
25307
|
-
this.
|
|
25308
|
-
|
|
25309
|
-
|
|
25310
|
-
|
|
25359
|
+
onLifecycle: this.opts.onWsLifecycle,
|
|
25360
|
+
// Any terminal close (4001, 4006, 4409, hardened war-cap) of the shared
|
|
25361
|
+
// socket makes this transport terminal. The shared iterator ends right
|
|
25362
|
+
// after this call, so the pump exit tears down every subscriber.
|
|
25363
|
+
onTerminalClose: (close) => {
|
|
25364
|
+
this.pingsTerminal = true;
|
|
25365
|
+
this.opts.onTerminalClose?.(close);
|
|
25366
|
+
}
|
|
25367
|
+
}), () => this.dropPingHub(hub));
|
|
25368
|
+
this._controlSend = hub.send;
|
|
25369
|
+
return hub;
|
|
25370
|
+
}
|
|
25371
|
+
dropPingHub(hub) {
|
|
25372
|
+
if (this.pingHub === hub) {
|
|
25373
|
+
this.pingHub = null;
|
|
25374
|
+
this._controlSend = null;
|
|
25375
|
+
}
|
|
25376
|
+
hub.shutdown();
|
|
25311
25377
|
}
|
|
25312
25378
|
sendControlFrame(_channelId, frame) {
|
|
25313
25379
|
try {
|
|
@@ -25335,6 +25401,90 @@ var RestTransport = class {
|
|
|
25335
25401
|
}));
|
|
25336
25402
|
}
|
|
25337
25403
|
};
|
|
25404
|
+
var SUBSCRIBER_QUEUE_MAX = 8;
|
|
25405
|
+
var PingSubscriber = class {
|
|
25406
|
+
queue = [];
|
|
25407
|
+
waiter;
|
|
25408
|
+
ended = false;
|
|
25409
|
+
started = false;
|
|
25410
|
+
pings = {
|
|
25411
|
+
[Symbol.asyncIterator]: () => {
|
|
25412
|
+
if (this.started)
|
|
25413
|
+
throw new Error("subscribeDeliveryPings: single-use async iterable");
|
|
25414
|
+
this.started = true;
|
|
25415
|
+
return {
|
|
25416
|
+
next: () => {
|
|
25417
|
+
if (this.ended)
|
|
25418
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
25419
|
+
if (this.queue.length)
|
|
25420
|
+
return Promise.resolve({ value: this.queue.shift(), done: false });
|
|
25421
|
+
return new Promise((resolve4) => {
|
|
25422
|
+
this.waiter = resolve4;
|
|
25423
|
+
});
|
|
25424
|
+
}
|
|
25425
|
+
};
|
|
25426
|
+
}
|
|
25427
|
+
};
|
|
25428
|
+
push(p) {
|
|
25429
|
+
if (this.ended)
|
|
25430
|
+
return;
|
|
25431
|
+
if (this.waiter) {
|
|
25432
|
+
const w = this.waiter;
|
|
25433
|
+
this.waiter = void 0;
|
|
25434
|
+
w({ value: p, done: false });
|
|
25435
|
+
} else if (this.queue.length < SUBSCRIBER_QUEUE_MAX) {
|
|
25436
|
+
this.queue.push(p);
|
|
25437
|
+
}
|
|
25438
|
+
}
|
|
25439
|
+
end() {
|
|
25440
|
+
this.ended = true;
|
|
25441
|
+
this.queue.length = 0;
|
|
25442
|
+
if (this.waiter) {
|
|
25443
|
+
const w = this.waiter;
|
|
25444
|
+
this.waiter = void 0;
|
|
25445
|
+
w({ value: void 0, done: true });
|
|
25446
|
+
}
|
|
25447
|
+
}
|
|
25448
|
+
};
|
|
25449
|
+
var PingHub = class {
|
|
25450
|
+
handle;
|
|
25451
|
+
onGone;
|
|
25452
|
+
subs = /* @__PURE__ */ new Set();
|
|
25453
|
+
send;
|
|
25454
|
+
constructor(handle, onGone) {
|
|
25455
|
+
this.handle = handle;
|
|
25456
|
+
this.onGone = onGone;
|
|
25457
|
+
this.send = handle.send;
|
|
25458
|
+
void this.pump();
|
|
25459
|
+
}
|
|
25460
|
+
/** `openDeliveryPings` iterables never reject and push() never throws, so
|
|
25461
|
+
* this loop only ends when the shared stream ends (our close, or terminal). */
|
|
25462
|
+
async pump() {
|
|
25463
|
+
for await (const p of this.handle.pings)
|
|
25464
|
+
for (const s of this.subs)
|
|
25465
|
+
s.push(p);
|
|
25466
|
+
this.onGone();
|
|
25467
|
+
}
|
|
25468
|
+
attach() {
|
|
25469
|
+
const s = new PingSubscriber();
|
|
25470
|
+
this.subs.add(s);
|
|
25471
|
+
return s;
|
|
25472
|
+
}
|
|
25473
|
+
/** Idempotent: detaches and ends only `s`; the last one out closes the socket. */
|
|
25474
|
+
detach(s) {
|
|
25475
|
+
this.subs.delete(s);
|
|
25476
|
+
s.end();
|
|
25477
|
+
if (this.subs.size === 0)
|
|
25478
|
+
this.onGone();
|
|
25479
|
+
}
|
|
25480
|
+
/** Idempotent: `openDeliveryPings().close()` is, and the set is empty after. */
|
|
25481
|
+
shutdown() {
|
|
25482
|
+
for (const s of this.subs)
|
|
25483
|
+
s.end();
|
|
25484
|
+
this.subs.clear();
|
|
25485
|
+
this.handle.close();
|
|
25486
|
+
}
|
|
25487
|
+
};
|
|
25338
25488
|
|
|
25339
25489
|
// ../connect-sdk-ts/dist/mls/rest-backend.js
|
|
25340
25490
|
function describeType(v) {
|
|
@@ -35208,6 +35358,7 @@ var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35208
35358
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35209
35359
|
]);
|
|
35210
35360
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35361
|
+
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35211
35362
|
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
35212
35363
|
var OWNER_TURN_UNCHECKED = Object.freeze({ kind: "unchecked" });
|
|
35213
35364
|
function ownerDmDefaultGrantsEnabled(env = process.env) {
|
|
@@ -35482,6 +35633,12 @@ function rosterFreshAt(entry, heldEpoch, heldGroupId) {
|
|
|
35482
35633
|
var RoomGrantEpochMovedError = class extends Error {
|
|
35483
35634
|
};
|
|
35484
35635
|
var ROOM_GRANT_TRACK_MAX = 4096;
|
|
35636
|
+
var SessionSupersededError = class extends Error {
|
|
35637
|
+
constructor() {
|
|
35638
|
+
super("session_superseded: another copy of this agent took its session during start()");
|
|
35639
|
+
this.name = "SessionSupersededError";
|
|
35640
|
+
}
|
|
35641
|
+
};
|
|
35485
35642
|
var GroupSupersededSendError = class extends Error {
|
|
35486
35643
|
constructor(channelId, activeGroupId) {
|
|
35487
35644
|
super(`send refused for channel ${channelId}: the channel moved to a new secure group; not sent`);
|
|
@@ -35521,9 +35678,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35521
35678
|
const seed = hexToBytes4(cfg.signingPrivateKey);
|
|
35522
35679
|
this.custody = SoftwareKeyCustody.fromPrivateKey("ed25519", seed);
|
|
35523
35680
|
this.replyIdKey = deriveReplyIdKey(seed);
|
|
35681
|
+
const base = deps.fetchImpl ?? fetch;
|
|
35682
|
+
this.fetchImpl = (input, init) => this.sessionSuperseded ? Promise.reject(new Error("session_superseded")) : base(input, init);
|
|
35524
35683
|
this.transport = deps.transport ?? new RestTransport({
|
|
35525
35684
|
baseUrl: cfg.apiBase,
|
|
35526
35685
|
bearerToken: cfg.apiKey,
|
|
35686
|
+
fetchImpl: this.fetchImpl,
|
|
35687
|
+
wsImpl: deps.wsImpl,
|
|
35688
|
+
// A 4409 close means another copy of this agent took the session.
|
|
35689
|
+
// Every other terminal close keeps today's behaviour.
|
|
35690
|
+
onTerminalClose: (close) => {
|
|
35691
|
+
if (close.code === WS_CLOSE_SESSION_SUPERSEDED2) this.onSessionSuperseded();
|
|
35692
|
+
},
|
|
35527
35693
|
connectIdentityId: cfg.identityId,
|
|
35528
35694
|
// P1b: hardened delivery-ping reconnect (verify-before-terminal +
|
|
35529
35695
|
// half-open + wake + auth war-cap). On by default for the daemon;
|
|
@@ -35537,7 +35703,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35537
35703
|
else writeServiceLog("warn", "transport.delivery_ping_lifecycle_unknown");
|
|
35538
35704
|
}
|
|
35539
35705
|
});
|
|
35540
|
-
this.backend = deps.backend ?? new RestMlsBackend({ baseUrl: cfg.apiBase, bearerToken: cfg.apiKey });
|
|
35706
|
+
this.backend = deps.backend ?? new RestMlsBackend({ baseUrl: cfg.apiBase, bearerToken: cfg.apiKey, fetchImpl: this.fetchImpl });
|
|
35541
35707
|
this.store = new SqliteMlsStateStore({
|
|
35542
35708
|
dbPath: cfg.storePath,
|
|
35543
35709
|
identityId: cfg.identityId,
|
|
@@ -35608,7 +35774,6 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35608
35774
|
// throws (mirrors the divergence-heal signal path).
|
|
35609
35775
|
onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId, this.drainExemptionAtCallIn())
|
|
35610
35776
|
});
|
|
35611
|
-
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
35612
35777
|
this.home = deps.home ?? homedir4();
|
|
35613
35778
|
this.grantWriters = {
|
|
35614
35779
|
writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
|
|
@@ -35740,6 +35905,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35740
35905
|
// stored for peek-then-join in ingestOnce
|
|
35741
35906
|
custody;
|
|
35742
35907
|
fetchImpl;
|
|
35908
|
+
/** P-1: set once a 4409 close says another copy took this agent's session.
|
|
35909
|
+
* The gated fetch built in the constructor refuses every request after it. */
|
|
35910
|
+
sessionSuperseded = false;
|
|
35911
|
+
/** C9: who hears `session_superseded` (runMode's quiesce). */
|
|
35912
|
+
sessionSupersededListeners = /* @__PURE__ */ new Set();
|
|
35913
|
+
/** C9 (spec §4.1.6, low 3): the one close() run every caller shares. */
|
|
35914
|
+
closing = null;
|
|
35743
35915
|
/** Round 5: home for the managed runtime-config directory (test seam). */
|
|
35744
35916
|
home;
|
|
35745
35917
|
grantWriters;
|
|
@@ -36215,6 +36387,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36215
36387
|
action;
|
|
36216
36388
|
waiters = [];
|
|
36217
36389
|
pingHandle;
|
|
36390
|
+
/** C10 (P-15): the delivery-ping subscription start() opens at its top,
|
|
36391
|
+
* held unconsumed until startReceivePump() takes it. */
|
|
36392
|
+
earlyPings = null;
|
|
36393
|
+
/** C10 fix round 1: settles (never rejects) when start()'s body does; close()
|
|
36394
|
+
* awaits it, bounded, so the store never closes under a start() step. */
|
|
36395
|
+
startInFlight = null;
|
|
36218
36396
|
safetyTimer;
|
|
36219
36397
|
/** The ingest cycle now running, if any (see ingestOnce: single-flight). */
|
|
36220
36398
|
ingestInFlight = null;
|
|
@@ -36792,7 +36970,50 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36792
36970
|
this.droppedMessages += overflow;
|
|
36793
36971
|
}
|
|
36794
36972
|
}
|
|
36973
|
+
/**
|
|
36974
|
+
* C10 (spec P-15, OQ-8 (b)): subscribe early, consume late. The
|
|
36975
|
+
* delivery-ping socket opens here, before anything else, so this copy claims
|
|
36976
|
+
* the device's session (and another copy gets its 4409) BEFORE this copy's
|
|
36977
|
+
* boot drain runs. The pings it carries are NOT consumed until
|
|
36978
|
+
* startReceivePump(), the same place as before: starting the pump first is
|
|
36979
|
+
* still forbidden (DRAIN BEFORE ROTATE, startBody).
|
|
36980
|
+
*
|
|
36981
|
+
* A 4409 during start() (audit H-3): the gated fetch is already set (C9 step
|
|
36982
|
+
* 0). start() then arms nothing more (supersededDuringStart before each
|
|
36983
|
+
* timer and before the pump), runs the memoized close() quiesce, and rejects
|
|
36984
|
+
* with SessionSupersededError. A step that FAILED because of the supersede
|
|
36985
|
+
* (a refused fetch, a store the quiesce closed) is the same outcome.
|
|
36986
|
+
*/
|
|
36795
36987
|
async start() {
|
|
36988
|
+
const early = this.transport.subscribeDeliveryPings("");
|
|
36989
|
+
this.earlyPings = early;
|
|
36990
|
+
this.pingHandle = { close: early.close };
|
|
36991
|
+
const body = this.startBody();
|
|
36992
|
+
this.startInFlight = body.then(() => {
|
|
36993
|
+
}, () => {
|
|
36994
|
+
});
|
|
36995
|
+
try {
|
|
36996
|
+
await body;
|
|
36997
|
+
} catch (e) {
|
|
36998
|
+
if (!this.sessionSuperseded) {
|
|
36999
|
+
if (this.earlyPings) {
|
|
37000
|
+
this.earlyPings.close();
|
|
37001
|
+
this.earlyPings = null;
|
|
37002
|
+
this.pingHandle = void 0;
|
|
37003
|
+
}
|
|
37004
|
+
throw e;
|
|
37005
|
+
}
|
|
37006
|
+
if (!(e instanceof SessionSupersededError)) writeServiceLog("warn", "startup.superseded_start_error", {}, e);
|
|
37007
|
+
await this.close().catch(() => {
|
|
37008
|
+
});
|
|
37009
|
+
throw new SessionSupersededError();
|
|
37010
|
+
}
|
|
37011
|
+
}
|
|
37012
|
+
/** C10: true once a 4409 landed; checked before each arming step of start(). */
|
|
37013
|
+
supersededDuringStart() {
|
|
37014
|
+
return this.sessionSuperseded;
|
|
37015
|
+
}
|
|
37016
|
+
async startBody() {
|
|
36796
37017
|
this.startedAt = Date.now();
|
|
36797
37018
|
await this.restoreRuntimeInbox();
|
|
36798
37019
|
await this.loadToolGrantState();
|
|
@@ -36852,10 +37073,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36852
37073
|
await this.warmRestoredNotes();
|
|
36853
37074
|
await this.reindexRestoredTurns();
|
|
36854
37075
|
if (this.unifiedOn) await this.reindexUnifiedRestored();
|
|
37076
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36855
37077
|
this.runtimeConfigTimer = setInterval(() => {
|
|
36856
37078
|
void this.refetchRuntimeConfig();
|
|
36857
37079
|
}, 6 * 60 * 60 * 1e3);
|
|
36858
37080
|
this.runtimeConfigTimer.unref?.();
|
|
37081
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36859
37082
|
this.membershipSweepTimer = setInterval(() => {
|
|
36860
37083
|
if (this.closingDown || this.membershipSweepWork) return;
|
|
36861
37084
|
this.membershipSweepWork = this.runMembershipSweeps().finally(() => {
|
|
@@ -36863,6 +37086,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36863
37086
|
});
|
|
36864
37087
|
}, PURGE_SWEEP_INTERVAL_MS);
|
|
36865
37088
|
this.membershipSweepTimer.unref?.();
|
|
37089
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36866
37090
|
this.recallPruneTimer = setInterval(() => {
|
|
36867
37091
|
if (this.closingDown || this.recallPruneWork) return;
|
|
36868
37092
|
this.recallPruneWork = this.runRecallPruneTick().finally(() => {
|
|
@@ -36870,6 +37094,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36870
37094
|
});
|
|
36871
37095
|
}, 60 * 60 * 1e3);
|
|
36872
37096
|
this.recallPruneTimer.unref?.();
|
|
37097
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36873
37098
|
if (this.machinePaths) this.recallMaintenanceTimer = setInterval(() => {
|
|
36874
37099
|
if (this.closingDown) return;
|
|
36875
37100
|
const n = ++this.maintenanceTickN;
|
|
@@ -36880,6 +37105,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36880
37105
|
});
|
|
36881
37106
|
}, 1e3);
|
|
36882
37107
|
this.recallMaintenanceTimer?.unref?.();
|
|
37108
|
+
if (this.supersededDuringStart()) throw new SessionSupersededError();
|
|
36883
37109
|
this.startReceivePump();
|
|
36884
37110
|
if (drainedInTime) {
|
|
36885
37111
|
this.armSelfHealSweeps();
|
|
@@ -41143,7 +41369,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41143
41369
|
}
|
|
41144
41370
|
}
|
|
41145
41371
|
startReceivePump() {
|
|
41146
|
-
const sub = this.transport.subscribeDeliveryPings("");
|
|
41372
|
+
const sub = this.earlyPings ?? this.transport.subscribeDeliveryPings("");
|
|
41373
|
+
this.earlyPings = null;
|
|
41147
41374
|
this.pingHandle = { close: sub.close };
|
|
41148
41375
|
void (async () => {
|
|
41149
41376
|
try {
|
|
@@ -42728,7 +42955,42 @@ ${channelId}`;
|
|
|
42728
42955
|
build: BRIDGE_VERSION
|
|
42729
42956
|
};
|
|
42730
42957
|
}
|
|
42731
|
-
|
|
42958
|
+
/** C9: `session_superseded` once a 4409 said another copy took this
|
|
42959
|
+
* agent's session (the gated fetch already refuses every request). The
|
|
42960
|
+
* local HTTP routes read it: every route but `/healthz` answers 503. */
|
|
42961
|
+
get sessionState() {
|
|
42962
|
+
return this.sessionSuperseded ? "session_superseded" : "live";
|
|
42963
|
+
}
|
|
42964
|
+
/** C9: subscribe to `session_superseded`. runMode subscribes right after
|
|
42965
|
+
* construction, before start(), so a 4409 during start() is heard too. */
|
|
42966
|
+
on(event, listener) {
|
|
42967
|
+
this.sessionSupersededListeners.add(listener);
|
|
42968
|
+
return () => this.sessionSupersededListeners.delete(listener);
|
|
42969
|
+
}
|
|
42970
|
+
/** P-1 step 0, then the event (spec §4.1.6): another copy of this agent took
|
|
42971
|
+
* the session (4409). The gated-fetch switch is set FIRST, so no HTTP
|
|
42972
|
+
* request leaves the process after this, whatever a listener does. */
|
|
42973
|
+
onSessionSuperseded() {
|
|
42974
|
+
this.sessionSuperseded = true;
|
|
42975
|
+
for (const listener of [...this.sessionSupersededListeners]) {
|
|
42976
|
+
try {
|
|
42977
|
+
listener();
|
|
42978
|
+
} catch (e) {
|
|
42979
|
+
writeServiceLog("error", "transport.session_superseded_listener_failed", {}, e);
|
|
42980
|
+
}
|
|
42981
|
+
}
|
|
42982
|
+
}
|
|
42983
|
+
/**
|
|
42984
|
+
* Idempotent (C9, spec §4.1.6 low 3): every call returns the FIRST call's
|
|
42985
|
+
* promise, so a SIGTERM during or after the supersede quiesce never runs
|
|
42986
|
+
* store.close() twice. A rejected close stays rejected: a retry would close
|
|
42987
|
+
* the store a second time.
|
|
42988
|
+
*/
|
|
42989
|
+
close() {
|
|
42990
|
+
this.closing ??= this.closeOnce();
|
|
42991
|
+
return this.closing;
|
|
42992
|
+
}
|
|
42993
|
+
async closeOnce() {
|
|
42732
42994
|
this.closingDown = true;
|
|
42733
42995
|
this.statusGate.close();
|
|
42734
42996
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
@@ -42741,6 +43003,28 @@ ${channelId}`;
|
|
|
42741
43003
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
42742
43004
|
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
42743
43005
|
if (this.membershipSweepTimer) clearInterval(this.membershipSweepTimer);
|
|
43006
|
+
const ingest = this.ingestInFlight;
|
|
43007
|
+
if (ingest) {
|
|
43008
|
+
let timer;
|
|
43009
|
+
await Promise.race([
|
|
43010
|
+
ingest.released,
|
|
43011
|
+
new Promise((resolve4) => {
|
|
43012
|
+
timer = setTimeout(resolve4, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
43013
|
+
})
|
|
43014
|
+
]);
|
|
43015
|
+
if (timer) clearTimeout(timer);
|
|
43016
|
+
}
|
|
43017
|
+
const starting = this.startInFlight;
|
|
43018
|
+
if (starting) {
|
|
43019
|
+
let timer;
|
|
43020
|
+
await Promise.race([
|
|
43021
|
+
starting,
|
|
43022
|
+
new Promise((resolve4) => {
|
|
43023
|
+
timer = setTimeout(resolve4, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
43024
|
+
})
|
|
43025
|
+
]);
|
|
43026
|
+
if (timer) clearTimeout(timer);
|
|
43027
|
+
}
|
|
42744
43028
|
if (this.membershipSweepWork) {
|
|
42745
43029
|
let timer;
|
|
42746
43030
|
await Promise.race([
|
|
@@ -42899,6 +43183,9 @@ var BRIDGE_ERRORS = {
|
|
|
42899
43183
|
// longer holds the shared bytes or key. Terminal for that link: never fall
|
|
42900
43184
|
// back to the current version. `detail.pin` names the refused pin.
|
|
42901
43185
|
vault_ref_changed: { http: 422, message: "this file changed in Files after it was shared; not opened" },
|
|
43186
|
+
// C9 (spec §4.1.6 step 1): another copy of this agent took its SecureComs
|
|
43187
|
+
// session (WS 4409), so this copy stopped. Every local route but /healthz.
|
|
43188
|
+
session_superseded: { http: 503, message: "another copy of this agent took its session; this copy stopped" },
|
|
42902
43189
|
internal: { http: 500, message: "internal error" }
|
|
42903
43190
|
};
|
|
42904
43191
|
function errorBody(code, detail) {
|
|
@@ -43129,6 +43416,10 @@ function createHttpServer(agent, opts) {
|
|
|
43129
43416
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
43130
43417
|
const path = url.pathname;
|
|
43131
43418
|
const method = req.method ?? "GET";
|
|
43419
|
+
if (agent.sessionState === "session_superseded") {
|
|
43420
|
+
if (method === "GET" && path === "/healthz") return json(res, 200, { state: "session_superseded" });
|
|
43421
|
+
return jsonError(res, "session_superseded");
|
|
43422
|
+
}
|
|
43132
43423
|
if (method === "GET" && path === "/healthz") {
|
|
43133
43424
|
return json(res, 200, { ...agent.health(), capabilities: capabilities(opts) });
|
|
43134
43425
|
}
|
|
@@ -43340,6 +43631,7 @@ var PING_INTERVAL_MS = 3e4;
|
|
|
43340
43631
|
var INFLIGHT_STALE_SWEEP_MS = 6e4;
|
|
43341
43632
|
var INFLIGHT_STALE_AFTER_MS = 15 * 6e4;
|
|
43342
43633
|
var HELLO_WAIT_MS = 2e3;
|
|
43634
|
+
var WS_CLOSE_SESSION_SUPERSEDED3 = 4409;
|
|
43343
43635
|
var MEMORY_NOTES_FEATURE = "memory_notes";
|
|
43344
43636
|
var MEMORY_RECALL_FEATURE = "memory_recall";
|
|
43345
43637
|
var MEMORY_UNIFIED_FEATURE = "memory_unified";
|
|
@@ -43360,7 +43652,12 @@ function authedUpgrade(req, opts) {
|
|
|
43360
43652
|
return true;
|
|
43361
43653
|
}
|
|
43362
43654
|
function attachAgentWebSocket(server, agent, opts) {
|
|
43363
|
-
|
|
43655
|
+
let sessionSuperseded = false;
|
|
43656
|
+
const wss = new WebSocketServer({
|
|
43657
|
+
server,
|
|
43658
|
+
path: "/v1/ws",
|
|
43659
|
+
verifyClient: (_info, done) => sessionSuperseded ? done(false, 503, "session_superseded") : done(true)
|
|
43660
|
+
});
|
|
43364
43661
|
const keepalive = setInterval(() => {
|
|
43365
43662
|
for (const client of wss.clients) {
|
|
43366
43663
|
const sock = client;
|
|
@@ -43500,6 +43797,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43500
43797
|
if (helloTimer) clearTimeout(helloTimer);
|
|
43501
43798
|
agent.noteSessionClose();
|
|
43502
43799
|
unsubscribe();
|
|
43800
|
+
if (sessionSuperseded) inflight.clear();
|
|
43503
43801
|
if (inflight.size) {
|
|
43504
43802
|
agent.requeueFailedRuntimeDeliveries(
|
|
43505
43803
|
[...inflight.values()].filter((m) => !agent.isRuntimeMessageRetired(m.message_id))
|
|
@@ -43521,7 +43819,10 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43521
43819
|
sock.on("close", cleanup);
|
|
43522
43820
|
sock.on("error", cleanup);
|
|
43523
43821
|
sock.on("message", (data) => {
|
|
43524
|
-
|
|
43822
|
+
if (sessionSuperseded) return;
|
|
43823
|
+
handleInbound(sock, agent, data, inflight, flush, session).catch((e) => {
|
|
43824
|
+
writeServiceLog("error", "ws.inbound_failed", {}, e);
|
|
43825
|
+
});
|
|
43525
43826
|
});
|
|
43526
43827
|
flush();
|
|
43527
43828
|
});
|
|
@@ -43536,6 +43837,13 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43536
43837
|
}
|
|
43537
43838
|
}
|
|
43538
43839
|
wss.close();
|
|
43840
|
+
},
|
|
43841
|
+
/** C9 (spec §4.1.6 step 1, OQ-9): close every open socket with 4409 /
|
|
43842
|
+
* `session_superseded`, and answer every later upgrade with 503. The
|
|
43843
|
+
* runtime's reconnect backoff does the rest; idempotent. */
|
|
43844
|
+
enterSessionSuperseded() {
|
|
43845
|
+
sessionSuperseded = true;
|
|
43846
|
+
for (const client of wss.clients) client.close(WS_CLOSE_SESSION_SUPERSEDED3, "session_superseded");
|
|
43539
43847
|
}
|
|
43540
43848
|
};
|
|
43541
43849
|
}
|
|
@@ -43837,11 +44145,12 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43837
44145
|
var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
43838
44146
|
|
|
43839
44147
|
// src/bin.ts
|
|
43840
|
-
import { readFileSync as
|
|
43841
|
-
import { join as join25, dirname as
|
|
44148
|
+
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";
|
|
44149
|
+
import { join as join25, dirname as dirname13, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
|
|
43842
44150
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
43843
44151
|
import { homedir as homedir5, userInfo } from "node:os";
|
|
43844
44152
|
import { spawnSync } from "node:child_process";
|
|
44153
|
+
import { createServer as netCreateServer } from "node:net";
|
|
43845
44154
|
|
|
43846
44155
|
// src/service/install.ts
|
|
43847
44156
|
import { dirname as dirname8 } from "node:path";
|
|
@@ -43922,7 +44231,7 @@ function slugify(s) {
|
|
|
43922
44231
|
function deriveServiceConfig(input) {
|
|
43923
44232
|
if (isVersionEmbeddedNodePath(input.execPath)) {
|
|
43924
44233
|
throw new Error(
|
|
43925
|
-
`
|
|
44234
|
+
`The Node at ${input.execPath} moves when it is upgraded, so the background service can't use it. Install Node ${REQUIRED_NODE_MAJOR} from https://nodejs.org, then run the install command again.`
|
|
43926
44235
|
);
|
|
43927
44236
|
}
|
|
43928
44237
|
const stem = input.label ?? basename5(input.credentialPath, extname(input.credentialPath));
|
|
@@ -43962,7 +44271,10 @@ ${programArgs}
|
|
|
43962
44271
|
<key>RunAtLoad</key>
|
|
43963
44272
|
<true/>
|
|
43964
44273
|
<key>KeepAlive</key>
|
|
43965
|
-
<
|
|
44274
|
+
<dict>
|
|
44275
|
+
<key>SuccessfulExit</key>
|
|
44276
|
+
<false/>
|
|
44277
|
+
</dict>
|
|
43966
44278
|
<key>StandardOutPath</key>
|
|
43967
44279
|
<string>${escapeXml(cfg.logPath)}</string>
|
|
43968
44280
|
<key>StandardErrorPath</key>
|
|
@@ -43989,7 +44301,7 @@ Wants=network-online.target
|
|
|
43989
44301
|
[Service]
|
|
43990
44302
|
Type=simple
|
|
43991
44303
|
ExecStart=${execStart}
|
|
43992
|
-
Restart=
|
|
44304
|
+
Restart=on-failure
|
|
43993
44305
|
RestartSec=5
|
|
43994
44306
|
|
|
43995
44307
|
[Install]
|
|
@@ -44000,6 +44312,26 @@ WantedBy=default.target
|
|
|
44000
44312
|
// src/service/install.ts
|
|
44001
44313
|
var ServiceUnsupportedError = class extends Error {
|
|
44002
44314
|
};
|
|
44315
|
+
var PREVIOUS_EXIT_WAIT_MS = 1e4;
|
|
44316
|
+
var PREVIOUS_EXIT_POLL_MS = 100;
|
|
44317
|
+
function launchdPid(deps, target) {
|
|
44318
|
+
const r = deps.run("launchctl", ["print", target]);
|
|
44319
|
+
if (r.status !== 0) return void 0;
|
|
44320
|
+
const m = /^\s*pid = (\d+)\s*$/m.exec(r.stdout);
|
|
44321
|
+
const pid = m ? Number(m[1]) : NaN;
|
|
44322
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
44323
|
+
}
|
|
44324
|
+
function waitForPreviousExit(deps, pid) {
|
|
44325
|
+
const deadline = deps.now() + PREVIOUS_EXIT_WAIT_MS;
|
|
44326
|
+
while (deps.pidAlive(pid)) {
|
|
44327
|
+
if (deps.now() >= deadline) {
|
|
44328
|
+
throw new Error(
|
|
44329
|
+
`the previous bridge (pid ${pid}) did not exit within ${PREVIOUS_EXIT_WAIT_MS / 1e3} s of launchctl bootout, so a new copy would find the instance lock held and stay down. Wait for that process to exit (check with: ps -p ${pid}), then run install again.`
|
|
44330
|
+
);
|
|
44331
|
+
}
|
|
44332
|
+
deps.sleepMs(PREVIOUS_EXIT_POLL_MS);
|
|
44333
|
+
}
|
|
44334
|
+
}
|
|
44003
44335
|
function tokenHint(input) {
|
|
44004
44336
|
return `${dirname8(input.credentialPath)}/bridge-token`;
|
|
44005
44337
|
}
|
|
@@ -44012,7 +44344,10 @@ function installService(input, deps) {
|
|
|
44012
44344
|
deps.mkdirp(dirname8(cfg.plistPath));
|
|
44013
44345
|
deps.mkdirp(dirname8(cfg.logPath));
|
|
44014
44346
|
deps.writeFile(cfg.plistPath, renderLaunchdPlist(cfg));
|
|
44015
|
-
|
|
44347
|
+
const target = `gui/${cfg.uid}/${cfg.label}`;
|
|
44348
|
+
const previousPid = launchdPid(deps, target);
|
|
44349
|
+
deps.run("launchctl", ["bootout", target]);
|
|
44350
|
+
if (previousPid !== void 0) waitForPreviousExit(deps, previousPid);
|
|
44016
44351
|
requireOk(deps.run("launchctl", ["bootstrap", `gui/${cfg.uid}`, cfg.plistPath]), "launchctl bootstrap");
|
|
44017
44352
|
deps.run("launchctl", ["enable", `gui/${cfg.uid}/${cfg.label}`]);
|
|
44018
44353
|
requireOk(deps.run("launchctl", ["kickstart", "-k", `gui/${cfg.uid}/${cfg.label}`]), "launchctl kickstart");
|
|
@@ -44462,6 +44797,10 @@ ${customFacts(input)}`, wired: false, error: false };
|
|
|
44462
44797
|
}
|
|
44463
44798
|
|
|
44464
44799
|
// src/service/install-prep.ts
|
|
44800
|
+
function isDaemonRouted(h) {
|
|
44801
|
+
return h != null && h.state !== "session_superseded";
|
|
44802
|
+
}
|
|
44803
|
+
var VERIFY_BUDGET_MS = BOOT_DRAIN_WAIT_MS_DEFAULT + 15e3;
|
|
44465
44804
|
function assertReadableCredential(absPath, deps) {
|
|
44466
44805
|
let raw;
|
|
44467
44806
|
try {
|
|
@@ -44489,6 +44828,44 @@ function durableDir(home, slug) {
|
|
|
44489
44828
|
function durableCredentialPath(home, slug) {
|
|
44490
44829
|
return join21(durableDir(home, slug), `${slug}.json`);
|
|
44491
44830
|
}
|
|
44831
|
+
var DURABLE_PREFIX = "securecoms-agent-";
|
|
44832
|
+
var InstallRefusedError = class extends Error {
|
|
44833
|
+
};
|
|
44834
|
+
function existingIdentityError(dir, label) {
|
|
44835
|
+
return new InstallRefusedError(
|
|
44836
|
+
`This agent is already installed in ${dir} with a different key. Uninstall it first: agent-bridge uninstall --label ${label}`
|
|
44837
|
+
);
|
|
44838
|
+
}
|
|
44839
|
+
function refuseExistingIdentity(args2, deps) {
|
|
44840
|
+
const destName = `${DURABLE_PREFIX}${args2.slug}`;
|
|
44841
|
+
let names;
|
|
44842
|
+
try {
|
|
44843
|
+
names = deps.listDir(args2.home);
|
|
44844
|
+
} catch {
|
|
44845
|
+
names = [];
|
|
44846
|
+
}
|
|
44847
|
+
const others = names.filter((n) => n.startsWith(DURABLE_PREFIX) && n !== destName).sort();
|
|
44848
|
+
for (const name of [destName, ...others]) {
|
|
44849
|
+
const slug = name.slice(DURABLE_PREFIX.length);
|
|
44850
|
+
const dir = join21(args2.home, name);
|
|
44851
|
+
const credPath = join21(dir, `${slug}.json`);
|
|
44852
|
+
if (!deps.fileExists(credPath)) continue;
|
|
44853
|
+
let existing = null;
|
|
44854
|
+
try {
|
|
44855
|
+
const j = JSON.parse(deps.readFile(credPath));
|
|
44856
|
+
if (j && typeof j === "object") existing = j;
|
|
44857
|
+
} catch {
|
|
44858
|
+
}
|
|
44859
|
+
if (!existing || typeof existing.identity_id !== "string" || !existing.identity_id) {
|
|
44860
|
+
if (deps.fileExists(join21(dir, "bridge-state.db"))) throw existingIdentityError(dir, slug);
|
|
44861
|
+
continue;
|
|
44862
|
+
}
|
|
44863
|
+
if (existing.identity_id !== args2.parsed.identityId) continue;
|
|
44864
|
+
if (name !== destName || existing.device_id !== args2.parsed.deviceId) {
|
|
44865
|
+
throw existingIdentityError(dir, slug);
|
|
44866
|
+
}
|
|
44867
|
+
}
|
|
44868
|
+
}
|
|
44492
44869
|
function takeCustody(args2, deps) {
|
|
44493
44870
|
const dir = durableDir(args2.home, args2.slug);
|
|
44494
44871
|
const dest = durableCredentialPath(args2.home, args2.slug);
|
|
@@ -44496,10 +44873,11 @@ function takeCustody(args2, deps) {
|
|
|
44496
44873
|
if (deps.fileExists(dest) && deps.realpath(args2.sourceAbsPath) === deps.realpath(dest)) return dest;
|
|
44497
44874
|
} catch {
|
|
44498
44875
|
}
|
|
44876
|
+
refuseExistingIdentity(args2, deps);
|
|
44499
44877
|
if (deps.fileExists(dest)) {
|
|
44500
44878
|
try {
|
|
44501
44879
|
const existing = JSON.parse(deps.readFile(dest));
|
|
44502
|
-
if (existing.
|
|
44880
|
+
if (existing.identity_id && existing.identity_id !== args2.parsed.identityId) {
|
|
44503
44881
|
throw new Error(
|
|
44504
44882
|
`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
44883
|
);
|
|
@@ -44554,21 +44932,19 @@ function selectPort(args2, deps) {
|
|
|
44554
44932
|
throw new Error(`install: no free port in ${PORT_MIN}-${PORT_MAX} \u2014 stop an unused bridge or pass --port`);
|
|
44555
44933
|
}
|
|
44556
44934
|
async function verifyDaemonUp(args2, deps) {
|
|
44557
|
-
const attempts = args2.attempts ?? 20;
|
|
44558
44935
|
const delayMs = args2.delayMs ?? 250;
|
|
44559
|
-
|
|
44560
|
-
|
|
44561
|
-
|
|
44562
|
-
|
|
44563
|
-
|
|
44564
|
-
}
|
|
44936
|
+
const deadline = deps.now() + VERIFY_BUDGET_MS;
|
|
44937
|
+
const maxAttempts = args2.attempts ?? Number.POSITIVE_INFINITY;
|
|
44938
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
44939
|
+
if (isDaemonRouted(deps.getHealth(args2.port))) return true;
|
|
44940
|
+
if (deps.now() >= deadline) return false;
|
|
44565
44941
|
await deps.sleep(delayMs);
|
|
44566
44942
|
}
|
|
44567
44943
|
return false;
|
|
44568
44944
|
}
|
|
44569
44945
|
async function orchestrateInstall(ctx) {
|
|
44570
44946
|
const parsed = assertReadableCredential(ctx.credentialAbsPath, ctx.prepDeps);
|
|
44571
|
-
const stemForSlug = ctx.label ?? ctx.credentialAbsPath.replace(/^.*[\\/]/, "").replace(/\.[^.]*$/, "");
|
|
44947
|
+
const stemForSlug = ctx.label ?? ctx.credentialAbsPath.replace(/^.*[\\/]/, "").replace(/\.[^.]*$/, "").replace(/ \(\d+\)$/, "");
|
|
44572
44948
|
const slug = slugify(stemForSlug);
|
|
44573
44949
|
const durablePath = takeCustody(
|
|
44574
44950
|
{ home: ctx.home, slug, sourceAbsPath: ctx.credentialAbsPath, parsed },
|
|
@@ -44692,13 +45068,13 @@ function readFleet(deps) {
|
|
|
44692
45068
|
// src/service/status.ts
|
|
44693
45069
|
function readFleetStatus(deps) {
|
|
44694
45070
|
return deps.readFleet().map((e) => {
|
|
44695
|
-
const
|
|
45071
|
+
const h = e.port != null ? deps.getHealth(e.port) : null;
|
|
45072
|
+
const up = isDaemonRouted(h);
|
|
44696
45073
|
let harness = "-";
|
|
44697
45074
|
let evictions = 0;
|
|
44698
|
-
if (up
|
|
44699
|
-
|
|
44700
|
-
|
|
44701
|
-
evictions = h?.evictions ?? 0;
|
|
45075
|
+
if (up) {
|
|
45076
|
+
harness = (h.agent_sessions ?? 0) >= 1 ? "attached" : "no brain";
|
|
45077
|
+
evictions = h.evictions ?? 0;
|
|
44702
45078
|
}
|
|
44703
45079
|
return {
|
|
44704
45080
|
slug: e.slug,
|
|
@@ -44790,20 +45166,339 @@ function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_F
|
|
|
44790
45166
|
}
|
|
44791
45167
|
}
|
|
44792
45168
|
|
|
45169
|
+
// src/service/instance-lock.ts
|
|
45170
|
+
import { execFileSync } from "node:child_process";
|
|
45171
|
+
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";
|
|
45172
|
+
import { hostname, uptime as uptime2 } from "node:os";
|
|
45173
|
+
import { dirname as dirname11 } from "node:path";
|
|
45174
|
+
var LOCK_PARTIAL_WRITE_GRACE_MS = 1e4;
|
|
45175
|
+
var LOCK_TAKEOVER_GUARD_STALE_MS = 3e4;
|
|
45176
|
+
var BOOT_TOLERANCE_SEC = 120;
|
|
45177
|
+
var LOCK_HEARTBEAT_MS = 6e4;
|
|
45178
|
+
var LOCK_FOREIGN_STALE_MS = 10 * LOCK_HEARTBEAT_MS;
|
|
45179
|
+
function errno(error) {
|
|
45180
|
+
const code = error?.code;
|
|
45181
|
+
return typeof code === "string" ? code : void 0;
|
|
45182
|
+
}
|
|
45183
|
+
function readTrimmed(path) {
|
|
45184
|
+
try {
|
|
45185
|
+
const v = readFileSync13(path, "utf8").trim();
|
|
45186
|
+
return v === "" ? void 0 : v;
|
|
45187
|
+
} catch {
|
|
45188
|
+
return void 0;
|
|
45189
|
+
}
|
|
45190
|
+
}
|
|
45191
|
+
function procStartFromStat(stat) {
|
|
45192
|
+
const close = stat.lastIndexOf(")");
|
|
45193
|
+
if (close < 0) return void 0;
|
|
45194
|
+
const rest = stat.slice(close + 1).trim().split(/\s+/);
|
|
45195
|
+
const v = rest[19];
|
|
45196
|
+
return v !== void 0 && /^\d+$/.test(v) ? v : void 0;
|
|
45197
|
+
}
|
|
45198
|
+
function pidAliveByKill(pid) {
|
|
45199
|
+
try {
|
|
45200
|
+
process.kill(pid, 0);
|
|
45201
|
+
return true;
|
|
45202
|
+
} catch (error) {
|
|
45203
|
+
return errno(error) === "EPERM";
|
|
45204
|
+
}
|
|
45205
|
+
}
|
|
45206
|
+
var bootIdCache;
|
|
45207
|
+
function readBootId() {
|
|
45208
|
+
if (bootIdCache) return bootIdCache.v;
|
|
45209
|
+
let v = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
45210
|
+
if (v === void 0 && process.platform === "darwin") {
|
|
45211
|
+
try {
|
|
45212
|
+
const out = execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], {
|
|
45213
|
+
encoding: "utf8",
|
|
45214
|
+
timeout: 2e3,
|
|
45215
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
45216
|
+
}).trim();
|
|
45217
|
+
v = /^[0-9A-Fa-f-]{16,64}$/.test(out) ? out : void 0;
|
|
45218
|
+
} catch {
|
|
45219
|
+
v = void 0;
|
|
45220
|
+
}
|
|
45221
|
+
}
|
|
45222
|
+
bootIdCache = { v };
|
|
45223
|
+
return v;
|
|
45224
|
+
}
|
|
45225
|
+
var realLockEnv = {
|
|
45226
|
+
pid: process.pid,
|
|
45227
|
+
hostname: () => hostname(),
|
|
45228
|
+
bootId: readBootId,
|
|
45229
|
+
bootEpochSec: () => Math.round(Date.now() / 1e3 - uptime2()),
|
|
45230
|
+
procStart: (pid) => {
|
|
45231
|
+
const stat = readTrimmed(`/proc/${pid}/stat`);
|
|
45232
|
+
return stat ? procStartFromStat(stat) : void 0;
|
|
45233
|
+
},
|
|
45234
|
+
pidAlive: pidAliveByKill
|
|
45235
|
+
};
|
|
45236
|
+
function parseBody(raw) {
|
|
45237
|
+
try {
|
|
45238
|
+
const b = JSON.parse(raw);
|
|
45239
|
+
if (b && b.v === 2 && Number.isInteger(b.pid) && b.pid > 0 && typeof b.host === "string" && Number.isFinite(b.boot)) {
|
|
45240
|
+
return b;
|
|
45241
|
+
}
|
|
45242
|
+
} catch {
|
|
45243
|
+
}
|
|
45244
|
+
return void 0;
|
|
45245
|
+
}
|
|
45246
|
+
var InstanceLock = class {
|
|
45247
|
+
constructor(path, env = realLockEnv, log = () => {
|
|
45248
|
+
}, heartbeatMs = LOCK_HEARTBEAT_MS) {
|
|
45249
|
+
this.path = path;
|
|
45250
|
+
this.env = env;
|
|
45251
|
+
this.log = log;
|
|
45252
|
+
this.heartbeatMs = heartbeatMs;
|
|
45253
|
+
}
|
|
45254
|
+
held = false;
|
|
45255
|
+
/** The exact body this process wrote (#808 N1): release and the ownership
|
|
45256
|
+
* check compare the file against THIS, never against live machine facts
|
|
45257
|
+
* (a hostname can change under a running process). */
|
|
45258
|
+
written;
|
|
45259
|
+
heartbeat;
|
|
45260
|
+
/** The error behind the last `unreadable` / `failed`, for the caller's one
|
|
45261
|
+
* log line (the lock itself never logs a refusal: it is retried per call). */
|
|
45262
|
+
lastError;
|
|
45263
|
+
/** PID read from the lock body of the live holder behind the last `held`. */
|
|
45264
|
+
holderPid;
|
|
45265
|
+
body() {
|
|
45266
|
+
const bootId = this.env.bootId();
|
|
45267
|
+
const start = this.env.procStart(this.env.pid);
|
|
45268
|
+
const b = {
|
|
45269
|
+
v: 2,
|
|
45270
|
+
pid: this.env.pid,
|
|
45271
|
+
host: this.env.hostname(),
|
|
45272
|
+
...bootId !== void 0 ? { boot_id: bootId } : {},
|
|
45273
|
+
boot: this.env.bootEpochSec(),
|
|
45274
|
+
...start !== void 0 ? { start } : {}
|
|
45275
|
+
};
|
|
45276
|
+
return JSON.stringify(b);
|
|
45277
|
+
}
|
|
45278
|
+
/** Try to hold the lock. Never throws (#808 L-e: machine reads included). */
|
|
45279
|
+
acquire() {
|
|
45280
|
+
this.holderPid = void 0;
|
|
45281
|
+
let result;
|
|
45282
|
+
try {
|
|
45283
|
+
result = this.acquireUnguarded();
|
|
45284
|
+
} catch (error) {
|
|
45285
|
+
this.lastError = error;
|
|
45286
|
+
result = "failed";
|
|
45287
|
+
}
|
|
45288
|
+
return result === "held" && this.holderPid !== void 0 ? { result, holderPid: this.holderPid } : { result };
|
|
45289
|
+
}
|
|
45290
|
+
/**
|
|
45291
|
+
* True while the file is still exactly the body this process wrote. Read
|
|
45292
|
+
* before every reserve (one small read), so a holder whose lock was taken
|
|
45293
|
+
* while it was frozen stops before it spends (#808 N1). A lost lock is
|
|
45294
|
+
* dropped: the caller then refuses, and later calls try to re-acquire.
|
|
45295
|
+
*/
|
|
45296
|
+
stillHeld() {
|
|
45297
|
+
if (!this.held) return false;
|
|
45298
|
+
let current;
|
|
45299
|
+
try {
|
|
45300
|
+
current = readFileSync13(this.path, "utf8");
|
|
45301
|
+
} catch {
|
|
45302
|
+
current = void 0;
|
|
45303
|
+
}
|
|
45304
|
+
if (current !== void 0 && current === this.written) return true;
|
|
45305
|
+
this.drop();
|
|
45306
|
+
this.log("warn", "bridge.instance_lock_lost", {});
|
|
45307
|
+
return false;
|
|
45308
|
+
}
|
|
45309
|
+
acquireUnguarded() {
|
|
45310
|
+
if (this.held) return "acquired";
|
|
45311
|
+
const created = this.create();
|
|
45312
|
+
if (created === "created") return this.took();
|
|
45313
|
+
if (created === "failed") return "failed";
|
|
45314
|
+
let raw;
|
|
45315
|
+
let ageMs;
|
|
45316
|
+
this.env.beforeLockRead?.();
|
|
45317
|
+
try {
|
|
45318
|
+
raw = readFileSync13(this.path, "utf8");
|
|
45319
|
+
ageMs = Date.now() - statSync6(this.path).mtimeMs;
|
|
45320
|
+
} catch (error) {
|
|
45321
|
+
if (errno(error) === "ENOENT") {
|
|
45322
|
+
const again = this.create();
|
|
45323
|
+
return again === "created" ? this.took() : again === "failed" ? "failed" : "busy";
|
|
45324
|
+
}
|
|
45325
|
+
this.lastError = error;
|
|
45326
|
+
return "unreadable";
|
|
45327
|
+
}
|
|
45328
|
+
const b = parseBody(raw);
|
|
45329
|
+
if (!b) {
|
|
45330
|
+
if (ageMs < LOCK_PARTIAL_WRITE_GRACE_MS) return "busy";
|
|
45331
|
+
return this.takeOver(raw, void 0);
|
|
45332
|
+
}
|
|
45333
|
+
if (b.host !== this.env.hostname()) {
|
|
45334
|
+
const bootId = this.env.bootId();
|
|
45335
|
+
const sameKernel = b.boot_id !== void 0 && bootId !== void 0 && b.boot_id === bootId;
|
|
45336
|
+
if (sameKernel && ageMs > LOCK_FOREIGN_STALE_MS) return this.takeOver(raw, b.pid);
|
|
45337
|
+
return "foreign_host";
|
|
45338
|
+
}
|
|
45339
|
+
if (!this.sameBoot(b)) return this.takeOver(raw, b.pid);
|
|
45340
|
+
const liveStart = this.env.procStart(b.pid);
|
|
45341
|
+
const reused = b.start !== void 0 && liveStart !== void 0 && liveStart !== b.start;
|
|
45342
|
+
if (b.pid === this.env.pid) {
|
|
45343
|
+
if (!reused) return this.took();
|
|
45344
|
+
return this.takeOver(raw, b.pid);
|
|
45345
|
+
}
|
|
45346
|
+
if (this.env.pidAlive(b.pid) && !reused) {
|
|
45347
|
+
this.holderPid = b.pid;
|
|
45348
|
+
return "held";
|
|
45349
|
+
}
|
|
45350
|
+
return this.takeOver(raw, b.pid);
|
|
45351
|
+
}
|
|
45352
|
+
/** Remove the lock only while it is still exactly the body this process
|
|
45353
|
+
* wrote (#808 N1: not the live hostname, which can change under us). */
|
|
45354
|
+
release() {
|
|
45355
|
+
if (!this.held) return;
|
|
45356
|
+
const written = this.written;
|
|
45357
|
+
this.drop();
|
|
45358
|
+
try {
|
|
45359
|
+
if (written !== void 0 && readFileSync13(this.path, "utf8") === written) unlinkSync3(this.path);
|
|
45360
|
+
} catch {
|
|
45361
|
+
}
|
|
45362
|
+
}
|
|
45363
|
+
drop() {
|
|
45364
|
+
this.held = false;
|
|
45365
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
45366
|
+
this.heartbeat = void 0;
|
|
45367
|
+
}
|
|
45368
|
+
/** We hold it: remember what we wrote (the file as it is now, ours) and
|
|
45369
|
+
* start the heartbeat. */
|
|
45370
|
+
took() {
|
|
45371
|
+
this.held = true;
|
|
45372
|
+
try {
|
|
45373
|
+
this.written = readFileSync13(this.path, "utf8");
|
|
45374
|
+
} catch {
|
|
45375
|
+
this.written = void 0;
|
|
45376
|
+
}
|
|
45377
|
+
if (!this.heartbeat && this.heartbeatMs > 0) {
|
|
45378
|
+
this.heartbeat = setInterval(() => this.beat(), this.heartbeatMs);
|
|
45379
|
+
this.heartbeat.unref?.();
|
|
45380
|
+
}
|
|
45381
|
+
return "acquired";
|
|
45382
|
+
}
|
|
45383
|
+
/** Refresh the mtime while the file is still ours; stop if it is not. */
|
|
45384
|
+
beat() {
|
|
45385
|
+
if (!this.stillHeld()) return;
|
|
45386
|
+
try {
|
|
45387
|
+
const now = /* @__PURE__ */ new Date();
|
|
45388
|
+
utimesSync(this.path, now, now);
|
|
45389
|
+
} catch {
|
|
45390
|
+
}
|
|
45391
|
+
}
|
|
45392
|
+
sameBoot(b) {
|
|
45393
|
+
const bootId = this.env.bootId();
|
|
45394
|
+
if (b.boot_id !== void 0 && bootId !== void 0) return b.boot_id === bootId;
|
|
45395
|
+
return Math.abs(b.boot - this.env.bootEpochSec()) <= BOOT_TOLERANCE_SEC;
|
|
45396
|
+
}
|
|
45397
|
+
/** O_EXCL create with our body. A write failure removes the half-written
|
|
45398
|
+
* file (it is ours: we created it) and reports `failed`, never throws. */
|
|
45399
|
+
create() {
|
|
45400
|
+
let fd;
|
|
45401
|
+
try {
|
|
45402
|
+
mkdirSync7(dirname11(this.path), { recursive: true, mode: 448 });
|
|
45403
|
+
fd = openSync4(this.path, "wx", 384);
|
|
45404
|
+
} catch (error) {
|
|
45405
|
+
if (errno(error) === "EEXIST") return "exists";
|
|
45406
|
+
this.lastError = error;
|
|
45407
|
+
return "failed";
|
|
45408
|
+
}
|
|
45409
|
+
try {
|
|
45410
|
+
writeSync2(fd, this.body());
|
|
45411
|
+
fsyncSync(fd);
|
|
45412
|
+
} catch (error) {
|
|
45413
|
+
try {
|
|
45414
|
+
closeSync4(fd);
|
|
45415
|
+
} catch {
|
|
45416
|
+
}
|
|
45417
|
+
try {
|
|
45418
|
+
unlinkSync3(this.path);
|
|
45419
|
+
} catch {
|
|
45420
|
+
}
|
|
45421
|
+
this.lastError = error;
|
|
45422
|
+
return "failed";
|
|
45423
|
+
}
|
|
45424
|
+
closeSync4(fd);
|
|
45425
|
+
return "created";
|
|
45426
|
+
}
|
|
45427
|
+
/**
|
|
45428
|
+
* Replace a stale lock. Only the holder of the O_EXCL takeover guard may
|
|
45429
|
+
* remove a lock, and only after re-reading it UNCHANGED, so two runtimes
|
|
45430
|
+
* that both saw the same stale lock cannot each delete the other's new one.
|
|
45431
|
+
* A fresh guard means a takeover is in progress: busy this time.
|
|
45432
|
+
*/
|
|
45433
|
+
takeOver(staleRaw, stalePid, retryGuard = true) {
|
|
45434
|
+
const guard = `${this.path}.takeover`;
|
|
45435
|
+
let fd;
|
|
45436
|
+
try {
|
|
45437
|
+
fd = openSync4(guard, "wx", 384);
|
|
45438
|
+
} catch (error) {
|
|
45439
|
+
if (errno(error) !== "EEXIST") return "failed";
|
|
45440
|
+
let crashed = false;
|
|
45441
|
+
try {
|
|
45442
|
+
crashed = Date.now() - statSync6(guard).mtimeMs > LOCK_TAKEOVER_GUARD_STALE_MS;
|
|
45443
|
+
if (crashed) unlinkSync3(guard);
|
|
45444
|
+
} catch {
|
|
45445
|
+
crashed = false;
|
|
45446
|
+
}
|
|
45447
|
+
if (crashed && retryGuard) return this.takeOver(staleRaw, stalePid, false);
|
|
45448
|
+
return "busy";
|
|
45449
|
+
}
|
|
45450
|
+
closeSync4(fd);
|
|
45451
|
+
try {
|
|
45452
|
+
this.env.beforeTakeoverReread?.();
|
|
45453
|
+
let current;
|
|
45454
|
+
try {
|
|
45455
|
+
current = readFileSync13(this.path, "utf8");
|
|
45456
|
+
} catch {
|
|
45457
|
+
current = void 0;
|
|
45458
|
+
}
|
|
45459
|
+
if (current !== void 0 && current !== staleRaw) {
|
|
45460
|
+
this.holderPid = parseBody(current)?.pid;
|
|
45461
|
+
return "held";
|
|
45462
|
+
}
|
|
45463
|
+
if (current !== void 0) unlinkSync3(this.path);
|
|
45464
|
+
this.env.beforeTakeoverCreate?.();
|
|
45465
|
+
const created = this.create();
|
|
45466
|
+
if (created !== "created") {
|
|
45467
|
+
if (created === "failed") return "failed";
|
|
45468
|
+
try {
|
|
45469
|
+
this.holderPid = parseBody(readFileSync13(this.path, "utf8"))?.pid;
|
|
45470
|
+
} catch {
|
|
45471
|
+
}
|
|
45472
|
+
return "held";
|
|
45473
|
+
}
|
|
45474
|
+
this.log("warn", "bridge.instance_lock_stale_taken_over", stalePid !== void 0 ? { stale_pid: stalePid } : {});
|
|
45475
|
+
return this.took();
|
|
45476
|
+
} catch (error) {
|
|
45477
|
+
this.lastError = error;
|
|
45478
|
+
return "failed";
|
|
45479
|
+
} finally {
|
|
45480
|
+
try {
|
|
45481
|
+
unlinkSync3(guard);
|
|
45482
|
+
} catch {
|
|
45483
|
+
}
|
|
45484
|
+
}
|
|
45485
|
+
}
|
|
45486
|
+
};
|
|
45487
|
+
|
|
44793
45488
|
// src/grants/pin-command.ts
|
|
44794
45489
|
import {
|
|
44795
|
-
closeSync as
|
|
45490
|
+
closeSync as closeSync5,
|
|
44796
45491
|
fchmodSync,
|
|
44797
|
-
fsyncSync,
|
|
45492
|
+
fsyncSync as fsyncSync2,
|
|
44798
45493
|
lstatSync as lstatSync8,
|
|
44799
|
-
openSync as
|
|
44800
|
-
readFileSync as
|
|
45494
|
+
openSync as openSync5,
|
|
45495
|
+
readFileSync as readFileSync14,
|
|
44801
45496
|
realpathSync as realpathSync2,
|
|
44802
45497
|
renameSync as renameSync5,
|
|
44803
|
-
unlinkSync as
|
|
44804
|
-
writeSync as
|
|
45498
|
+
unlinkSync as unlinkSync4,
|
|
45499
|
+
writeSync as writeSync3
|
|
44805
45500
|
} from "node:fs";
|
|
44806
|
-
import { dirname as
|
|
45501
|
+
import { dirname as dirname12, join as join23, resolve as resolve2 } from "node:path";
|
|
44807
45502
|
var PIN_COPY = Object.freeze({
|
|
44808
45503
|
saved: "pin saved. Restart the Connector to use it.",
|
|
44809
45504
|
// [COPY, approved by Eric 2026-10-12]
|
|
@@ -44834,27 +45529,27 @@ function isSymlink(path) {
|
|
|
44834
45529
|
}
|
|
44835
45530
|
}
|
|
44836
45531
|
function readJson(path) {
|
|
44837
|
-
return JSON.parse(
|
|
45532
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
44838
45533
|
}
|
|
44839
45534
|
function publicKeyOf(v) {
|
|
44840
45535
|
const r = parsePinObject(v);
|
|
44841
45536
|
return r.ok ? r.pin.publicKeyB64 : null;
|
|
44842
45537
|
}
|
|
44843
45538
|
function writeTemp(path, bytes) {
|
|
44844
|
-
const fd =
|
|
45539
|
+
const fd = openSync5(path, "wx", 384);
|
|
44845
45540
|
try {
|
|
44846
45541
|
fchmodSync(fd, 384);
|
|
44847
|
-
|
|
44848
|
-
|
|
45542
|
+
writeSync3(fd, bytes);
|
|
45543
|
+
fsyncSync2(fd);
|
|
44849
45544
|
} finally {
|
|
44850
|
-
|
|
45545
|
+
closeSync5(fd);
|
|
44851
45546
|
}
|
|
44852
45547
|
}
|
|
44853
45548
|
function runPinCommand(opts) {
|
|
44854
|
-
const credDir =
|
|
45549
|
+
const credDir = dirname12(resolve2(opts.credentialPath));
|
|
44855
45550
|
const target = join23(credDir, PIN_FILE_NAME);
|
|
44856
45551
|
try {
|
|
44857
|
-
if (realpathSync2(
|
|
45552
|
+
if (realpathSync2(dirname12(resolve2(opts.pinSourcePath))) !== realpathSync2(credDir)) return refuse("directory_mismatch");
|
|
44858
45553
|
} catch {
|
|
44859
45554
|
return refuse("io_error");
|
|
44860
45555
|
}
|
|
@@ -44901,7 +45596,7 @@ function runPinCommand(opts) {
|
|
|
44901
45596
|
const fail = () => {
|
|
44902
45597
|
for (const p of created) {
|
|
44903
45598
|
try {
|
|
44904
|
-
|
|
45599
|
+
unlinkSync4(p);
|
|
44905
45600
|
} catch {
|
|
44906
45601
|
}
|
|
44907
45602
|
}
|
|
@@ -44937,11 +45632,11 @@ function runPinCommand(opts) {
|
|
|
44937
45632
|
created.splice(created.indexOf(credTmp), 1);
|
|
44938
45633
|
step("cred_renamed");
|
|
44939
45634
|
try {
|
|
44940
|
-
const dfd =
|
|
45635
|
+
const dfd = openSync5(credDir, "r");
|
|
44941
45636
|
try {
|
|
44942
|
-
|
|
45637
|
+
fsyncSync2(dfd);
|
|
44943
45638
|
} finally {
|
|
44944
|
-
|
|
45639
|
+
closeSync5(dfd);
|
|
44945
45640
|
}
|
|
44946
45641
|
} catch {
|
|
44947
45642
|
}
|
|
@@ -44979,6 +45674,20 @@ function runPinCli(argv, io) {
|
|
|
44979
45674
|
return 1;
|
|
44980
45675
|
}
|
|
44981
45676
|
|
|
45677
|
+
// src/service/credential-arg.ts
|
|
45678
|
+
function readCredentialArg(argv, env) {
|
|
45679
|
+
const i = argv.indexOf("--credential");
|
|
45680
|
+
if (i >= 0) {
|
|
45681
|
+
const v = argv[i + 1];
|
|
45682
|
+
return v ? { kind: "path", path: v } : { kind: "empty_flag" };
|
|
45683
|
+
}
|
|
45684
|
+
const fromEnv = env.SECURECOMS_CREDENTIAL;
|
|
45685
|
+
return fromEnv ? { kind: "path", path: fromEnv } : { kind: "absent" };
|
|
45686
|
+
}
|
|
45687
|
+
function noCredentialLine(verb) {
|
|
45688
|
+
return verb === "run" ? "No key file was given. If you used the copied run command, no key file for this agent was found in your Downloads folder." : "No key file was found in your Downloads folder. Download the key file again from SecureComs, or add --credential <path>.";
|
|
45689
|
+
}
|
|
45690
|
+
|
|
44982
45691
|
// src/bin.ts
|
|
44983
45692
|
function nodeMajorOk(version) {
|
|
44984
45693
|
return Number(version.split(".")[0]) >= 24;
|
|
@@ -44997,7 +45706,7 @@ function parseVerb(argv) {
|
|
|
44997
45706
|
return v === "install" || v === "uninstall" || v === "status" || v === "wire" || v === "unwire" || v === "models" || v === "pin" ? v : null;
|
|
44998
45707
|
}
|
|
44999
45708
|
function slugFromCredentialPath(credentialPath) {
|
|
45000
|
-
const dir =
|
|
45709
|
+
const dir = dirname13(credentialPath);
|
|
45001
45710
|
const base = dir.split(/[\\/]/).pop() ?? "";
|
|
45002
45711
|
const m = base.match(/^securecoms-agent-(.+)$/);
|
|
45003
45712
|
if (m) return m[1];
|
|
@@ -45013,17 +45722,24 @@ function realServiceDeps() {
|
|
|
45013
45722
|
},
|
|
45014
45723
|
writeFile: (p, data) => writeFileSync5(p, data, { mode: 420 }),
|
|
45015
45724
|
removeFile: (p) => rmSync8(p, { force: true }),
|
|
45016
|
-
mkdirp: (p) =>
|
|
45725
|
+
mkdirp: (p) => mkdirSync8(p, { recursive: true }),
|
|
45017
45726
|
fileExists: (p) => existsSync7(p),
|
|
45018
|
-
user: userInfo().username
|
|
45727
|
+
user: userInfo().username,
|
|
45728
|
+
pidAlive: pidAliveByKill,
|
|
45729
|
+
// installService is synchronous; Atomics.wait is a real blocking sleep.
|
|
45730
|
+
sleepMs: (ms) => {
|
|
45731
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
45732
|
+
},
|
|
45733
|
+
now: () => Date.now()
|
|
45019
45734
|
};
|
|
45020
45735
|
}
|
|
45021
45736
|
function chooseInstallExecPath(runningExecPath, override, deps) {
|
|
45022
45737
|
if (override) return override;
|
|
45023
45738
|
const r = resolveStableNodePath(runningExecPath, deps);
|
|
45024
45739
|
if (!r.stable) {
|
|
45740
|
+
const list = STABLE_LOCATIONS.slice(0, -1).join(", ") + " or " + STABLE_LOCATIONS[STABLE_LOCATIONS.length - 1];
|
|
45025
45741
|
throw new Error(
|
|
45026
|
-
"
|
|
45742
|
+
"The agent's background service needs Node " + REQUIRED_NODE_MAJOR + " or newer at a fixed location (" + list + "), and none was found. Node from nvm, fnm and similar tools moves when it is upgraded, so it is not used. Install Node " + REQUIRED_NODE_MAJOR + " from https://nodejs.org, then run the install command again."
|
|
45027
45743
|
);
|
|
45028
45744
|
}
|
|
45029
45745
|
return r.path;
|
|
@@ -45057,10 +45773,10 @@ function realResolveDeps() {
|
|
|
45057
45773
|
}
|
|
45058
45774
|
function realPrepDeps() {
|
|
45059
45775
|
return {
|
|
45060
|
-
readFile: (p) =>
|
|
45776
|
+
readFile: (p) => readFileSync16(p, "utf8"),
|
|
45061
45777
|
writeFileMode: (p, data, mode) => writeFileSync5(p, data, { mode }),
|
|
45062
45778
|
mkdirp: (p, mode) => {
|
|
45063
|
-
|
|
45779
|
+
mkdirSync8(p, { recursive: true, mode: mode ?? 493 });
|
|
45064
45780
|
},
|
|
45065
45781
|
fileExists: (p) => existsSync7(p),
|
|
45066
45782
|
realpath: (p) => realpathSync3(p),
|
|
@@ -45080,25 +45796,22 @@ function realPrepDeps() {
|
|
|
45080
45796
|
]);
|
|
45081
45797
|
return r.status === 0;
|
|
45082
45798
|
},
|
|
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
|
-
},
|
|
45799
|
+
getHealth: realGetHealth,
|
|
45090
45800
|
// No unref: an unref'd timer would let the process exit mid-verification.
|
|
45091
|
-
sleep: (ms) => new Promise((res) => setTimeout(res, ms))
|
|
45801
|
+
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
|
|
45802
|
+
now: () => Date.now()
|
|
45092
45803
|
};
|
|
45093
45804
|
}
|
|
45094
45805
|
function realGetHealth(port) {
|
|
45095
45806
|
const r = spawnSync(process.execPath, [
|
|
45096
45807
|
"-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" });
|
|
45808
|
+
`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));`
|
|
45809
|
+
], { encoding: "utf8", timeout: 2e3 });
|
|
45099
45810
|
if (r.status !== 0) return null;
|
|
45811
|
+
const nl = r.stdout.indexOf("\n");
|
|
45812
|
+
if (nl < 0 || r.stdout.slice(0, nl) !== "200") return null;
|
|
45100
45813
|
try {
|
|
45101
|
-
return JSON.parse(r.stdout);
|
|
45814
|
+
return JSON.parse(r.stdout.slice(nl + 1));
|
|
45102
45815
|
} catch {
|
|
45103
45816
|
return null;
|
|
45104
45817
|
}
|
|
@@ -45113,19 +45826,12 @@ function realReadFleet() {
|
|
|
45113
45826
|
return [];
|
|
45114
45827
|
}
|
|
45115
45828
|
},
|
|
45116
|
-
readFile: (p) =>
|
|
45829
|
+
readFile: (p) => readFileSync16(p, "utf8")
|
|
45117
45830
|
});
|
|
45118
45831
|
}
|
|
45119
45832
|
function runStatus() {
|
|
45120
45833
|
const deps = {
|
|
45121
45834
|
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
45835
|
getHealth: realGetHealth
|
|
45130
45836
|
};
|
|
45131
45837
|
const rows = readFleetStatus(deps);
|
|
@@ -45135,26 +45841,26 @@ function runStatus() {
|
|
|
45135
45841
|
}
|
|
45136
45842
|
function realCopyConnector(srcDir, dstDir, harness) {
|
|
45137
45843
|
const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
|
|
45138
|
-
|
|
45844
|
+
mkdirSync8(dstDir, { recursive: true });
|
|
45139
45845
|
for (const name of readdirSync4(srcDir)) {
|
|
45140
45846
|
const srcPath = join25(srcDir, name);
|
|
45141
|
-
if (
|
|
45847
|
+
if (statSync8(srcPath).isDirectory()) {
|
|
45142
45848
|
if (CONNECTOR_SKIP_DIRS.has(name)) continue;
|
|
45143
45849
|
realCopyConnector(srcPath, join25(dstDir, name), harness);
|
|
45144
45850
|
continue;
|
|
45145
45851
|
}
|
|
45146
45852
|
if (name.includes(".test.")) continue;
|
|
45147
45853
|
if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
|
|
45148
|
-
writeFileSync5(join25(dstDir, name),
|
|
45854
|
+
writeFileSync5(join25(dstDir, name), readFileSync16(srcPath));
|
|
45149
45855
|
}
|
|
45150
45856
|
}
|
|
45151
45857
|
function realWriteBinding(dstDir, identityId) {
|
|
45152
|
-
|
|
45858
|
+
mkdirSync8(dstDir, { recursive: true });
|
|
45153
45859
|
writeFileSync5(join25(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
|
|
45154
45860
|
}
|
|
45155
45861
|
function realReadBinding(dstDir) {
|
|
45156
45862
|
try {
|
|
45157
|
-
const parsed = JSON.parse(
|
|
45863
|
+
const parsed = JSON.parse(readFileSync16(join25(dstDir, "securecoms.binding.json"), "utf8"));
|
|
45158
45864
|
return parsed.identity_id ?? null;
|
|
45159
45865
|
} catch {
|
|
45160
45866
|
return null;
|
|
@@ -45165,14 +45871,14 @@ function realRemoveBinding(dstDir) {
|
|
|
45165
45871
|
}
|
|
45166
45872
|
function realStatDir(p) {
|
|
45167
45873
|
try {
|
|
45168
|
-
return
|
|
45874
|
+
return statSync8(p).isDirectory();
|
|
45169
45875
|
} catch {
|
|
45170
45876
|
return false;
|
|
45171
45877
|
}
|
|
45172
45878
|
}
|
|
45173
45879
|
function realReadHermesConfig(p) {
|
|
45174
45880
|
try {
|
|
45175
|
-
return
|
|
45881
|
+
return readFileSync16(p, "utf8");
|
|
45176
45882
|
} catch {
|
|
45177
45883
|
return null;
|
|
45178
45884
|
}
|
|
@@ -45213,9 +45919,17 @@ function runWireVerb(verb) {
|
|
|
45213
45919
|
process.exit(verb === "wire" ? runWire(realWireDeps()) : runUnwire(realWireDeps()));
|
|
45214
45920
|
}
|
|
45215
45921
|
async function runServiceVerb(verb) {
|
|
45216
|
-
|
|
45922
|
+
let credentialPath;
|
|
45923
|
+
if (verb === "install") {
|
|
45924
|
+
const cred = readCredentialArg(process.argv, process.env);
|
|
45925
|
+
credentialPath = cred.kind === "path" ? cred.path : void 0;
|
|
45926
|
+
} else {
|
|
45927
|
+
credentialPath = argOrEnv("--credential", "SECURECOMS_CREDENTIAL");
|
|
45928
|
+
}
|
|
45217
45929
|
const label = argOrEnv("--label", "SECURECOMS_LABEL");
|
|
45218
45930
|
if (verb === "install" ? !credentialPath : !credentialPath && !label) {
|
|
45931
|
+
if (verb === "install") process.stderr.write(`${noCredentialLine("install")}
|
|
45932
|
+
`);
|
|
45219
45933
|
const execPathUsage = verb === "install" ? " [--exec-path <path>]" : "";
|
|
45220
45934
|
process.stderr.write(`usage: agent-bridge ${verb} --credential <path> [--label <name>] [--store <path>] [--port N] [--harness openclaw|hermes|universal]${execPathUsage}
|
|
45221
45935
|
`);
|
|
@@ -45293,12 +46007,15 @@ async function runServiceVerb(verb) {
|
|
|
45293
46007
|
}
|
|
45294
46008
|
process.exit(0);
|
|
45295
46009
|
} catch (e) {
|
|
45296
|
-
|
|
45297
|
-
process.stderr.write(`${raw ? e.message : `service ${verb} failed: ${e.message ?? e}`}
|
|
45298
|
-
`);
|
|
46010
|
+
process.stderr.write(formatServiceError(verb, e));
|
|
45299
46011
|
process.exit(1);
|
|
45300
46012
|
}
|
|
45301
46013
|
}
|
|
46014
|
+
function formatServiceError(verb, e) {
|
|
46015
|
+
const raw = e instanceof ServiceUnsupportedError || e instanceof GlobalInstallPermissionError || e instanceof InstallRefusedError;
|
|
46016
|
+
return `${raw ? e.message : `service ${verb} failed: ${e.message ?? e}`}
|
|
46017
|
+
`;
|
|
46018
|
+
}
|
|
45302
46019
|
async function startThenServe(agent, opts) {
|
|
45303
46020
|
await agent.start();
|
|
45304
46021
|
const tls = opts.readTls();
|
|
@@ -45330,55 +46047,237 @@ async function runModelsVerb() {
|
|
|
45330
46047
|
function runPinVerb() {
|
|
45331
46048
|
process.exit(runPinCli(process.argv, { out: (l) => process.stdout.write(l + "\n"), err: (l) => process.stderr.write(l + "\n") }));
|
|
45332
46049
|
}
|
|
45333
|
-
|
|
45334
|
-
|
|
45335
|
-
|
|
45336
|
-
|
|
45337
|
-
|
|
45338
|
-
|
|
45339
|
-
|
|
45340
|
-
|
|
45341
|
-
|
|
45342
|
-
|
|
45343
|
-
|
|
45344
|
-
|
|
45345
|
-
|
|
45346
|
-
|
|
45347
|
-
|
|
45348
|
-
|
|
45349
|
-
|
|
45350
|
-
|
|
45351
|
-
|
|
45352
|
-
|
|
45353
|
-
|
|
45354
|
-
|
|
45355
|
-
|
|
45356
|
-
await runServiceVerb(verb);
|
|
45357
|
-
return;
|
|
45358
|
-
}
|
|
45359
|
-
const credentialPath = argOrEnv("--credential", "SECURECOMS_CREDENTIAL");
|
|
45360
|
-
if (!credentialPath) {
|
|
45361
|
-
writeServiceLog("error", "startup.credential_required.use_--credential");
|
|
45362
|
-
process.exit(2);
|
|
46050
|
+
var CANONICAL_UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
46051
|
+
var HELD_POLL_TOTAL_MS = 1e4;
|
|
46052
|
+
var HELD_POLL_INTERVAL_MS = 500;
|
|
46053
|
+
var HELD_NO_PID_REACQUIRES = 3;
|
|
46054
|
+
var SHUTDOWN_HARD_EXIT_MS = 8e3;
|
|
46055
|
+
function configErrorLine(reason, o) {
|
|
46056
|
+
const at = o.isTTY ? ` at ${o.path}` : "";
|
|
46057
|
+
switch (reason) {
|
|
46058
|
+
case "credential_not_found":
|
|
46059
|
+
return `Can't find the key file${at}. Check the path after --credential.`;
|
|
46060
|
+
case "credential_unreadable":
|
|
46061
|
+
return `Can't open the key file${at}. Check that it exists and that your user can read it.`;
|
|
46062
|
+
case "credential_not_json":
|
|
46063
|
+
case "identity_id_invalid":
|
|
46064
|
+
return `The key file${at} is damaged. Get a new key file from the agent's page in SecureComs.`;
|
|
46065
|
+
case "credential_missing_field":
|
|
46066
|
+
return `The key file${at} is incomplete (no ${o.field}). Get a new key file from the agent's page in SecureComs.`;
|
|
46067
|
+
case "credential_voice_invalid":
|
|
46068
|
+
return o.source === "env" ? "The voice settings in this computer's environment are not valid." : `The voice settings in the key file${at} are not valid.`;
|
|
46069
|
+
case "bind_refused":
|
|
46070
|
+
return "The Connector only accepts connections from this computer unless TLS is set. Remove --bind, or add --tls-cert and --tls-key.";
|
|
46071
|
+
case "unknown":
|
|
46072
|
+
return void 0;
|
|
45363
46073
|
}
|
|
45364
|
-
|
|
46074
|
+
}
|
|
46075
|
+
function realRunModeDeps(credentialPath, storePath) {
|
|
46076
|
+
return {
|
|
46077
|
+
credentialPath,
|
|
46078
|
+
storePath,
|
|
46079
|
+
argv: process.argv,
|
|
46080
|
+
env: process.env,
|
|
46081
|
+
home: homedir5(),
|
|
46082
|
+
newLock: (path) => new InstanceLock(path, void 0, writeServiceLog),
|
|
46083
|
+
// 7b Task 19 fix M2: the production construction owns the machine paths (heartbeat, model marker and GC).
|
|
46084
|
+
newAgent: (cfg) => new BridgeAgent(cfg, { hostMachine: true }),
|
|
46085
|
+
net: { createServer: netCreateServer },
|
|
46086
|
+
serve: startThenServe,
|
|
46087
|
+
now: () => Date.now(),
|
|
46088
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
46089
|
+
setHardExitTimer: (cb, ms) => {
|
|
46090
|
+
const t = setTimeout(cb, ms);
|
|
46091
|
+
t.unref?.();
|
|
46092
|
+
return () => clearTimeout(t);
|
|
46093
|
+
},
|
|
46094
|
+
onSignal: (signal, handler) => {
|
|
46095
|
+
process.on(signal, handler);
|
|
46096
|
+
},
|
|
46097
|
+
onExit: (handler) => {
|
|
46098
|
+
process.once("exit", handler);
|
|
46099
|
+
},
|
|
46100
|
+
exit: (code) => process.exit(code),
|
|
46101
|
+
print: (line) => {
|
|
46102
|
+
process.stderr.write(`${line}
|
|
46103
|
+
`);
|
|
46104
|
+
},
|
|
46105
|
+
isTTY: process.stderr.isTTY === true,
|
|
46106
|
+
log: writeServiceLog
|
|
46107
|
+
};
|
|
46108
|
+
}
|
|
46109
|
+
async function runMode(deps) {
|
|
46110
|
+
const { credentialPath, storePath, log } = deps;
|
|
46111
|
+
const refuse2 = (reason, extra, e) => {
|
|
46112
|
+
const fields = {
|
|
46113
|
+
reason: ServiceLogEnum.of(reason, [
|
|
46114
|
+
"credential_not_found",
|
|
46115
|
+
"credential_unreadable",
|
|
46116
|
+
"credential_not_json",
|
|
46117
|
+
"credential_missing_field",
|
|
46118
|
+
"credential_voice_invalid",
|
|
46119
|
+
"identity_id_invalid",
|
|
46120
|
+
"bind_refused",
|
|
46121
|
+
"unknown"
|
|
46122
|
+
])
|
|
46123
|
+
};
|
|
46124
|
+
if (extra.field) {
|
|
46125
|
+
fields.field = ServiceLogEnum.of(extra.field, [
|
|
46126
|
+
"api_key",
|
|
46127
|
+
"identity_id",
|
|
46128
|
+
"device_id",
|
|
46129
|
+
"signing_private_key",
|
|
46130
|
+
"enclave_id",
|
|
46131
|
+
"api_base"
|
|
46132
|
+
]);
|
|
46133
|
+
}
|
|
46134
|
+
log("error", "startup.config_invalid", fields, e);
|
|
46135
|
+
const line = configErrorLine(reason, { path: credentialPath, isTTY: deps.isTTY, ...extra });
|
|
46136
|
+
if (line !== void 0) deps.print(line);
|
|
46137
|
+
};
|
|
45365
46138
|
let cfg, daemon;
|
|
45366
46139
|
try {
|
|
45367
46140
|
cfg = loadConfig({ credentialPath, storePath });
|
|
45368
|
-
daemon = parseDaemonArgs(
|
|
46141
|
+
daemon = parseDaemonArgs(deps.argv, deps.env);
|
|
46142
|
+
} catch (e) {
|
|
46143
|
+
if (e instanceof ConfigError) refuse2(e.reason, { field: e.field, source: e.source }, e);
|
|
46144
|
+
else refuse2("unknown", {}, e);
|
|
46145
|
+
return deps.exit(1);
|
|
46146
|
+
}
|
|
46147
|
+
try {
|
|
45369
46148
|
validateBind(daemon.bind, daemon.tls);
|
|
45370
46149
|
} catch (e) {
|
|
45371
|
-
|
|
45372
|
-
|
|
46150
|
+
refuse2("bind_refused", {}, e);
|
|
46151
|
+
return deps.exit(1);
|
|
46152
|
+
}
|
|
46153
|
+
if (!CANONICAL_UUID2.test(cfg.identityId)) {
|
|
46154
|
+
refuse2("identity_id_invalid", { field: "identity_id" });
|
|
46155
|
+
return deps.exit(1);
|
|
45373
46156
|
}
|
|
45374
|
-
const
|
|
46157
|
+
const lockDir = join25(deps.home, ".securecoms", "locks");
|
|
46158
|
+
const lockPath = join25(lockDir, `${cfg.identityId.toLowerCase()}.lock`);
|
|
46159
|
+
try {
|
|
46160
|
+
mkdirSync8(lockDir, { recursive: true, mode: 448 });
|
|
46161
|
+
chmodSync6(lockDir, 448);
|
|
46162
|
+
} catch (e) {
|
|
46163
|
+
log("error", "startup.instance_lock_failed", {}, e);
|
|
46164
|
+
return deps.exit(1);
|
|
46165
|
+
}
|
|
46166
|
+
const lock = deps.newLock(lockPath);
|
|
46167
|
+
let got = lock.acquire();
|
|
46168
|
+
const pollStart = deps.now();
|
|
46169
|
+
while (got.result === "held" && deps.now() - pollStart < HELD_POLL_TOTAL_MS) {
|
|
46170
|
+
await deps.sleep(HELD_POLL_INTERVAL_MS);
|
|
46171
|
+
got = lock.acquire();
|
|
46172
|
+
}
|
|
46173
|
+
for (let i = 0; i < HELD_NO_PID_REACQUIRES && got.result === "held" && got.holderPid === void 0; i++) {
|
|
46174
|
+
await deps.sleep(HELD_POLL_INTERVAL_MS);
|
|
46175
|
+
got = lock.acquire();
|
|
46176
|
+
}
|
|
46177
|
+
switch (got.result) {
|
|
46178
|
+
case "acquired":
|
|
46179
|
+
break;
|
|
46180
|
+
case "held":
|
|
46181
|
+
if (got.holderPid === void 0) {
|
|
46182
|
+
log("error", "startup.instance_lock_busy");
|
|
46183
|
+
return deps.exit(1);
|
|
46184
|
+
}
|
|
46185
|
+
log("info", "startup.already_running");
|
|
46186
|
+
deps.print(`This agent is already running on this computer (process ${got.holderPid}). Stop that copy first, or run: agent-bridge status`);
|
|
46187
|
+
return deps.exit(0);
|
|
46188
|
+
case "foreign_host":
|
|
46189
|
+
log("error", "startup.instance_lock_foreign_host", {
|
|
46190
|
+
lock_path: ServiceLogEnum.of("home_securecoms_locks_identity", ["home_securecoms_locks_identity"]),
|
|
46191
|
+
remedy: ServiceLogEnum.of("delete_lock_if_no_other_host", ["delete_lock_if_no_other_host"])
|
|
46192
|
+
});
|
|
46193
|
+
deps.print(
|
|
46194
|
+
deps.isTTY ? `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.` : "This agent's lock file was written under a different computer name. If no other computer runs this agent, delete that file and start the agent again."
|
|
46195
|
+
);
|
|
46196
|
+
return deps.exit(1);
|
|
46197
|
+
case "unreadable":
|
|
46198
|
+
log("error", "startup.instance_lock_unreadable", {}, lock.lastError);
|
|
46199
|
+
return deps.exit(1);
|
|
46200
|
+
case "busy":
|
|
46201
|
+
log("error", "startup.instance_lock_busy");
|
|
46202
|
+
return deps.exit(1);
|
|
46203
|
+
case "failed":
|
|
46204
|
+
log("error", "startup.instance_lock_failed", {}, lock.lastError);
|
|
46205
|
+
return deps.exit(1);
|
|
46206
|
+
}
|
|
46207
|
+
deps.onExit(() => lock.release());
|
|
46208
|
+
const tokenFile = daemon.tokenFile ?? join25(dirname13(credentialPath), "bridge-token");
|
|
45375
46209
|
const token = resolveToken({ tokenFile, token: daemon.token });
|
|
45376
|
-
const
|
|
45377
|
-
const
|
|
45378
|
-
|
|
45379
|
-
|
|
45380
|
-
|
|
46210
|
+
const preBound = deps.net.createServer((sock) => sock.destroy());
|
|
46211
|
+
const bindError = await new Promise((resolveBind) => {
|
|
46212
|
+
const onError = (e) => resolveBind(e);
|
|
46213
|
+
preBound.once("error", onError);
|
|
46214
|
+
preBound.listen(daemon.port, daemon.bind, () => {
|
|
46215
|
+
preBound.removeListener("error", onError);
|
|
46216
|
+
resolveBind(void 0);
|
|
46217
|
+
});
|
|
46218
|
+
});
|
|
46219
|
+
if (bindError) {
|
|
46220
|
+
if (bindError.code === "EADDRINUSE") {
|
|
46221
|
+
log("error", "startup.port_in_use", { port: daemon.port });
|
|
46222
|
+
deps.print(`Port ${daemon.port} is already in use by another program on this computer, so the agent did not start.`);
|
|
46223
|
+
} else {
|
|
46224
|
+
log("error", "startup.listen_failed", {}, bindError);
|
|
46225
|
+
}
|
|
46226
|
+
lock.release();
|
|
46227
|
+
return deps.exit(1);
|
|
46228
|
+
}
|
|
46229
|
+
const agent = deps.newAgent(cfg);
|
|
46230
|
+
const instanceId = newInstanceId();
|
|
46231
|
+
const identityId = cfg.identityId;
|
|
46232
|
+
let served = null;
|
|
46233
|
+
let superseded = false;
|
|
46234
|
+
const stopIngress = (s) => {
|
|
46235
|
+
s.agentWs.enterSessionSuperseded();
|
|
46236
|
+
s.closeAllSseStreams();
|
|
46237
|
+
};
|
|
46238
|
+
agent.on("session_superseded", () => {
|
|
46239
|
+
if (superseded) return;
|
|
46240
|
+
superseded = true;
|
|
46241
|
+
if (served) stopIngress(served);
|
|
46242
|
+
log("warn", "transport.session_superseded");
|
|
46243
|
+
deps.print("This agent connected from another place, so this copy stopped. Stop the other copy, then restart this one.");
|
|
46244
|
+
void (async () => {
|
|
46245
|
+
try {
|
|
46246
|
+
await agent.close();
|
|
46247
|
+
} catch (e) {
|
|
46248
|
+
log("error", "transport.session_superseded_close_failed", {}, e);
|
|
46249
|
+
}
|
|
46250
|
+
removeConnectionFileIfOwned(deps.home, identityId, instanceId);
|
|
46251
|
+
})();
|
|
45381
46252
|
});
|
|
46253
|
+
let exiting = false;
|
|
46254
|
+
const closeReleaseExit = () => {
|
|
46255
|
+
if (exiting) return;
|
|
46256
|
+
exiting = true;
|
|
46257
|
+
const cancelHard = deps.setHardExitTimer(() => {
|
|
46258
|
+
lock.release();
|
|
46259
|
+
deps.exit(0);
|
|
46260
|
+
}, SHUTDOWN_HARD_EXIT_MS);
|
|
46261
|
+
void agent.close().finally(() => {
|
|
46262
|
+
cancelHard();
|
|
46263
|
+
lock.release();
|
|
46264
|
+
deps.exit(0);
|
|
46265
|
+
});
|
|
46266
|
+
};
|
|
46267
|
+
try {
|
|
46268
|
+
served = await deps.serve(agent, {
|
|
46269
|
+
token,
|
|
46270
|
+
allowOrigins: daemon.allowOrigins,
|
|
46271
|
+
readTls: () => daemon.tls ? { cert: readFileSync16(daemon.tlsCert, "utf8"), key: readFileSync16(daemon.tlsKey, "utf8") } : void 0
|
|
46272
|
+
});
|
|
46273
|
+
} catch (e) {
|
|
46274
|
+
if (!(e instanceof SessionSupersededError)) throw e;
|
|
46275
|
+
deps.onSignal("SIGINT", closeReleaseExit);
|
|
46276
|
+
deps.onSignal("SIGTERM", closeReleaseExit);
|
|
46277
|
+
return;
|
|
46278
|
+
}
|
|
46279
|
+
if (superseded) stopIngress(served);
|
|
46280
|
+
const { server, closeAllSseStreams, agentWs } = served;
|
|
45382
46281
|
let down = false;
|
|
45383
46282
|
const shutdown = () => {
|
|
45384
46283
|
if (down) return;
|
|
@@ -45386,26 +46285,20 @@ async function main() {
|
|
|
45386
46285
|
agentWs.close();
|
|
45387
46286
|
server.close();
|
|
45388
46287
|
closeAllSseStreams();
|
|
45389
|
-
|
|
45390
|
-
hard.unref?.();
|
|
45391
|
-
void agent.close().finally(() => {
|
|
45392
|
-
clearTimeout(hard);
|
|
45393
|
-
process.exit(0);
|
|
45394
|
-
});
|
|
46288
|
+
closeReleaseExit();
|
|
45395
46289
|
};
|
|
45396
|
-
|
|
45397
|
-
|
|
45398
|
-
const instanceId = newInstanceId();
|
|
45399
|
-
const identityId = agent.health().identity_id;
|
|
46290
|
+
deps.onSignal("SIGINT", shutdown);
|
|
46291
|
+
deps.onSignal("SIGTERM", shutdown);
|
|
45400
46292
|
const slug = slugFromCredentialPath(credentialPath);
|
|
45401
|
-
|
|
45402
|
-
server.listen(
|
|
45403
|
-
|
|
45404
|
-
|
|
46293
|
+
deps.onExit(() => removeConnectionFileIfOwned(deps.home, identityId, instanceId));
|
|
46294
|
+
server.listen(preBound, () => {
|
|
46295
|
+
log("info", "startup.listening", { port: daemon.port, tls: !!daemon.tls });
|
|
46296
|
+
log("info", "startup.token_loaded");
|
|
45405
46297
|
const addr = server.address();
|
|
45406
46298
|
const boundPort = typeof addr === "object" && addr ? addr.port : daemon.port;
|
|
46299
|
+
if (superseded) return;
|
|
45407
46300
|
try {
|
|
45408
|
-
writeConnectionFile(
|
|
46301
|
+
writeConnectionFile(deps.home, buildConnectionFile({
|
|
45409
46302
|
identityId,
|
|
45410
46303
|
port: boundPort,
|
|
45411
46304
|
tokenFile,
|
|
@@ -45414,12 +46307,52 @@ async function main() {
|
|
|
45414
46307
|
instanceId,
|
|
45415
46308
|
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
45416
46309
|
}));
|
|
45417
|
-
|
|
46310
|
+
log("info", "startup.connection_file_written");
|
|
45418
46311
|
} catch (e) {
|
|
45419
|
-
|
|
46312
|
+
log("warn", "startup.connection_file_write_failed", {}, e);
|
|
45420
46313
|
}
|
|
45421
46314
|
});
|
|
45422
46315
|
}
|
|
46316
|
+
function printNoCredentialRunLine() {
|
|
46317
|
+
process.stderr.write(`${noCredentialLine("run")}
|
|
46318
|
+
`);
|
|
46319
|
+
}
|
|
46320
|
+
async function main() {
|
|
46321
|
+
if (!nodeMajorOk(process.versions.node)) {
|
|
46322
|
+
writeServiceLog("error", "startup.node_unsupported");
|
|
46323
|
+
process.exit(1);
|
|
46324
|
+
}
|
|
46325
|
+
const verb = parseVerb(process.argv);
|
|
46326
|
+
if (verb === "pin") {
|
|
46327
|
+
runPinVerb();
|
|
46328
|
+
return;
|
|
46329
|
+
}
|
|
46330
|
+
if (verb === "status") {
|
|
46331
|
+
runStatus();
|
|
46332
|
+
return;
|
|
46333
|
+
}
|
|
46334
|
+
if (verb === "wire" || verb === "unwire") {
|
|
46335
|
+
runWireVerb(verb);
|
|
46336
|
+
return;
|
|
46337
|
+
}
|
|
46338
|
+
if (verb === "models") {
|
|
46339
|
+
await runModelsVerb();
|
|
46340
|
+
return;
|
|
46341
|
+
}
|
|
46342
|
+
if (verb) {
|
|
46343
|
+
await runServiceVerb(verb);
|
|
46344
|
+
return;
|
|
46345
|
+
}
|
|
46346
|
+
const cred = readCredentialArg(process.argv, process.env);
|
|
46347
|
+
if (cred.kind !== "path") {
|
|
46348
|
+
printNoCredentialRunLine();
|
|
46349
|
+
writeServiceLog("error", "startup.credential_required.use_--credential");
|
|
46350
|
+
process.exit(2);
|
|
46351
|
+
}
|
|
46352
|
+
const credentialPath = cred.path;
|
|
46353
|
+
const storePath = argOrEnv("--store", "SECURECOMS_STORE");
|
|
46354
|
+
await runMode(realRunModeDeps(credentialPath, storePath));
|
|
46355
|
+
}
|
|
45423
46356
|
var here = fileURLToPath4(import.meta.url);
|
|
45424
46357
|
var isMain = !!process.argv[1] && refersToSameFile(here, process.argv[1], realpathSync3);
|
|
45425
46358
|
if (isMain) {
|
|
@@ -45429,10 +46362,18 @@ if (isMain) {
|
|
|
45429
46362
|
});
|
|
45430
46363
|
}
|
|
45431
46364
|
export {
|
|
46365
|
+
HELD_NO_PID_REACQUIRES,
|
|
46366
|
+
HELD_POLL_INTERVAL_MS,
|
|
46367
|
+
HELD_POLL_TOTAL_MS,
|
|
46368
|
+
SHUTDOWN_HARD_EXIT_MS,
|
|
45432
46369
|
chooseInstallExecPath,
|
|
46370
|
+
configErrorLine,
|
|
46371
|
+
formatServiceError,
|
|
45433
46372
|
nodeMajorOk,
|
|
45434
46373
|
parseVerb,
|
|
45435
46374
|
realCopyConnector,
|
|
46375
|
+
realGetHealth,
|
|
46376
|
+
runMode,
|
|
45436
46377
|
slugFromCredentialPath,
|
|
45437
46378
|
startThenServe,
|
|
45438
46379
|
tokenFingerprint
|