@agentvault/grok-bridge 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -64004,6 +64004,8 @@ var init_mls_group = __esm2({
|
|
|
64004
64004
|
continue;
|
|
64005
64005
|
seenIdentities.push(identity);
|
|
64006
64006
|
const leaf = this.memberLeafIndex(identity);
|
|
64007
|
+
if (leaf !== null && leaf === this.ownLeafIndex())
|
|
64008
|
+
continue;
|
|
64007
64009
|
if (leaf !== null)
|
|
64008
64010
|
leavesToRemove.push(leaf);
|
|
64009
64011
|
}
|
|
@@ -64059,6 +64061,20 @@ var init_mls_group = __esm2({
|
|
|
64059
64061
|
* then re-adds it with a fresh KeyPackage so it gets a Welcome at the
|
|
64060
64062
|
* current epoch it can actually apply.
|
|
64061
64063
|
*/
|
|
64064
|
+
/**
|
|
64065
|
+
* The leaf index THIS manager occupies — the leaf whose private key it holds.
|
|
64066
|
+
*
|
|
64067
|
+
* Read from `privatePath.leafIndex`, which is the same value ts-mls uses to
|
|
64068
|
+
* decide whether a commit removes its own committer, so a caller checking
|
|
64069
|
+
* against this cannot disagree with the library about who "we" are.
|
|
64070
|
+
*
|
|
64071
|
+
* Exists so a fulfiller can tell a request from ANOTHER device apart from one
|
|
64072
|
+
* from itself (#1147) without any "who am I" plumbing: the manager already
|
|
64073
|
+
* knows, and a guard that reads it here cannot be forgotten by a call site.
|
|
64074
|
+
*/
|
|
64075
|
+
ownLeafIndex() {
|
|
64076
|
+
return this._requireState().privatePath.leafIndex;
|
|
64077
|
+
}
|
|
64062
64078
|
memberLeafIndex(identity) {
|
|
64063
64079
|
const s2 = this._requireState();
|
|
64064
64080
|
const tree = s2.ratchetTree;
|
|
@@ -64589,11 +64605,11 @@ var init_mls_kp_pool = __esm2({
|
|
|
64589
64605
|
}
|
|
64590
64606
|
});
|
|
64591
64607
|
function ownIdentity() {
|
|
64592
|
-
const v2 = true ? "0.23.
|
|
64608
|
+
const v2 = true ? "0.23.32" : FALLBACK;
|
|
64593
64609
|
return `${PACKAGE}@${v2}`;
|
|
64594
64610
|
}
|
|
64595
64611
|
function buildSha() {
|
|
64596
|
-
const raw = true ? "
|
|
64612
|
+
const raw = true ? "564f8b1-dirty" : "";
|
|
64597
64613
|
const cleaned = (raw ?? "").trim();
|
|
64598
64614
|
return !cleaned || cleaned === "unknown" ? null : cleaned;
|
|
64599
64615
|
}
|
|
@@ -66341,6 +66357,118 @@ var init_channel = __esm2({
|
|
|
66341
66357
|
* Encrypt and send a message to ALL owner devices (fanout).
|
|
66342
66358
|
* Each session gets the same plaintext encrypted independently.
|
|
66343
66359
|
*/
|
|
66360
|
+
/**
|
|
66361
|
+
* #952 — sends held because MLS is expected for this conversation but the
|
|
66362
|
+
* group is not joined yet.
|
|
66363
|
+
*
|
|
66364
|
+
* ⚠️ IN MEMORY ONLY. These entries hold PLAINTEXT. `_persisted` is written to
|
|
66365
|
+
* disk, and the product's entire premise is that plaintext does not rest
|
|
66366
|
+
* where it does not have to; a crash losing a held greeting is the correct
|
|
66367
|
+
* trade. This is why the existing `outboundQueue` could not be reused — that
|
|
66368
|
+
* one is persisted, and holds already-DR-encrypted bytes, which is exactly
|
|
66369
|
+
* the encryption we are trying not to perform yet.
|
|
66370
|
+
*/
|
|
66371
|
+
_mlsHold = /* @__PURE__ */ new Map();
|
|
66372
|
+
/** How long a send waits for the Welcome. The two measured bursts were each
|
|
66373
|
+
* confined to a single minute, with the group already existing server-side
|
|
66374
|
+
* in the same second, so this is generous rather than tight. */
|
|
66375
|
+
static MLS_HOLD_MS = 3e4;
|
|
66376
|
+
/** Bound on held sends per group — see `_holdForMls`. */
|
|
66377
|
+
static MLS_HOLD_MAX = 50;
|
|
66378
|
+
/**
|
|
66379
|
+
* Should this 1:1 send wait for MLS rather than go out over Double Ratchet?
|
|
66380
|
+
*
|
|
66381
|
+
* The discriminator exists at the moment of the bug: `_sessionGroupIds`
|
|
66382
|
+
* already carries the conversation's group id — the server told us the group
|
|
66383
|
+
* exists — while `_mlsGroups` has no usable manager for it. Group expected,
|
|
66384
|
+
* group not ready.
|
|
66385
|
+
*
|
|
66386
|
+
* The negative case is load-bearing. A conversation with NO group id is
|
|
66387
|
+
* legitimately Double Ratchet, and holding it would break owner-to-agent
|
|
66388
|
+
* messaging outright, which is far worse than the bug this fixes. "Present"
|
|
66389
|
+
* is also not "ready": a manager parked at epoch 0 has not joined (#1092),
|
|
66390
|
+
* and reading presence as readiness is how #991 shipped.
|
|
66391
|
+
*/
|
|
66392
|
+
_shouldHoldForMls(convGroupId, mlsGroup, mlsGroupId) {
|
|
66393
|
+
if (!convGroupId) return false;
|
|
66394
|
+
const ready = Boolean(mlsGroup?.isInitialized) && Boolean(mlsGroupId) && Number(mlsGroup?.epoch) > 0;
|
|
66395
|
+
return !ready;
|
|
66396
|
+
}
|
|
66397
|
+
/** Park a send until the Welcome lands, or until the hold expires. */
|
|
66398
|
+
_holdForMls(convGroupId, convId, plaintext, options) {
|
|
66399
|
+
let entry = this._mlsHold.get(convGroupId);
|
|
66400
|
+
if (!entry) {
|
|
66401
|
+
const timer = setTimeout(() => {
|
|
66402
|
+
void this._releaseHeldOverDr(convGroupId);
|
|
66403
|
+
}, _SecureChannel.MLS_HOLD_MS);
|
|
66404
|
+
timer.unref?.();
|
|
66405
|
+
entry = { items: [], timer };
|
|
66406
|
+
this._mlsHold.set(convGroupId, entry);
|
|
66407
|
+
}
|
|
66408
|
+
if (entry.items.length >= _SecureChannel.MLS_HOLD_MAX) {
|
|
66409
|
+
entry.items.shift();
|
|
66410
|
+
console.warn(
|
|
66411
|
+
`[SecureChannel] MLS hold full for group ${convGroupId.slice(0, 8)} \u2014 dropped the oldest held message`
|
|
66412
|
+
);
|
|
66413
|
+
}
|
|
66414
|
+
entry.items.push({ convId, plaintext, options });
|
|
66415
|
+
console.log(
|
|
66416
|
+
`[SecureChannel] Holding send for group ${convGroupId.slice(0, 8)} until its Welcome lands (held=${entry.items.length})`
|
|
66417
|
+
);
|
|
66418
|
+
}
|
|
66419
|
+
_takeHold(convGroupId) {
|
|
66420
|
+
const entry = this._mlsHold.get(convGroupId);
|
|
66421
|
+
if (!entry) return null;
|
|
66422
|
+
clearTimeout(entry.timer);
|
|
66423
|
+
this._mlsHold.delete(convGroupId);
|
|
66424
|
+
return entry;
|
|
66425
|
+
}
|
|
66426
|
+
/** Re-drive one held send through the normal path, now that MLS is ready. */
|
|
66427
|
+
async _resendHeld(item) {
|
|
66428
|
+
await this.send(item.plaintext, {
|
|
66429
|
+
...item.options ?? {},
|
|
66430
|
+
conversationId: item.convId,
|
|
66431
|
+
bypassMlsHold: true
|
|
66432
|
+
});
|
|
66433
|
+
}
|
|
66434
|
+
/** The Welcome landed — release everything held for this group, in order. */
|
|
66435
|
+
async _flushMlsHold(convGroupId) {
|
|
66436
|
+
const entry = this._takeHold(convGroupId);
|
|
66437
|
+
if (!entry) return;
|
|
66438
|
+
console.log(
|
|
66439
|
+
`[SecureChannel] Welcome landed for group ${convGroupId.slice(0, 8)} \u2014 releasing ${entry.items.length} held message(s) over MLS`
|
|
66440
|
+
);
|
|
66441
|
+
for (const item of entry.items) {
|
|
66442
|
+
try {
|
|
66443
|
+
await this._resendHeld(item);
|
|
66444
|
+
} catch (err) {
|
|
66445
|
+
console.warn("[SecureChannel] Held message failed to send after Welcome:", err);
|
|
66446
|
+
}
|
|
66447
|
+
}
|
|
66448
|
+
}
|
|
66449
|
+
/**
|
|
66450
|
+
* The hold expired. Send over Double Ratchet exactly as before.
|
|
66451
|
+
*
|
|
66452
|
+
* This is deliberately NOT a drop. #853 and #831 are Welcomes that never
|
|
66453
|
+
* arrive; under those an unbounded hold turns a lost minute into an agent
|
|
66454
|
+
* that never speaks again. The hold removes the race — it must not invent a
|
|
66455
|
+
* new failure mode. The warning is the part that is new: today the loss is
|
|
66456
|
+
* silent.
|
|
66457
|
+
*/
|
|
66458
|
+
async _releaseHeldOverDr(convGroupId) {
|
|
66459
|
+
const entry = this._takeHold(convGroupId);
|
|
66460
|
+
if (!entry) return;
|
|
66461
|
+
console.warn(
|
|
66462
|
+
`[SecureChannel] No Welcome for group ${convGroupId.slice(0, 8)} within ${_SecureChannel.MLS_HOLD_MS}ms \u2014 releasing ${entry.items.length} message(s) over Double Ratchet. The recipient may not hold a ratchet with this device, in which case they are undeliverable (#952).`
|
|
66463
|
+
);
|
|
66464
|
+
for (const item of entry.items) {
|
|
66465
|
+
try {
|
|
66466
|
+
await this._resendHeld(item);
|
|
66467
|
+
} catch (err) {
|
|
66468
|
+
console.warn("[SecureChannel] Held message failed on Double Ratchet release:", err);
|
|
66469
|
+
}
|
|
66470
|
+
}
|
|
66471
|
+
}
|
|
66344
66472
|
async send(plaintext, options) {
|
|
66345
66473
|
if (isAbstention(plaintext)) return;
|
|
66346
66474
|
if (this._state === "error" || this._state === "idle") {
|
|
@@ -66502,6 +66630,10 @@ var init_channel = __esm2({
|
|
|
66502
66630
|
);
|
|
66503
66631
|
addressedMlsGroupIds.push(mlsGroupId);
|
|
66504
66632
|
if (convGroupId) sentSharedGroupIds.add(convGroupId);
|
|
66633
|
+
} else if (!options?.bypassMlsHold && this._shouldHoldForMls(convGroupId, mlsGroup, mlsGroupId)) {
|
|
66634
|
+
this._holdForMls(convGroupId, convId, plaintext, options);
|
|
66635
|
+
sentCount++;
|
|
66636
|
+
continue;
|
|
66505
66637
|
} else {
|
|
66506
66638
|
const encrypted = session.ratchet.encrypt(plaintext);
|
|
66507
66639
|
const transport = encryptedMessageToTransport(encrypted);
|
|
@@ -66671,7 +66803,7 @@ var init_channel = __esm2({
|
|
|
66671
66803
|
*/
|
|
66672
66804
|
sendActivitySpan(spanData) {
|
|
66673
66805
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
66674
|
-
const pluginVersion = true ? "0.23.
|
|
66806
|
+
const pluginVersion = true ? "0.23.32" : "0.0.0-dev";
|
|
66675
66807
|
const agentName = this.config.agentName ?? "Agent";
|
|
66676
66808
|
const resource = {
|
|
66677
66809
|
"service.name": "agentvault-agent",
|
|
@@ -68609,7 +68741,7 @@ var init_channel = __esm2({
|
|
|
68609
68741
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68610
68742
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68611
68743
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68612
|
-
pluginVersion: true ? "0.23.
|
|
68744
|
+
pluginVersion: true ? "0.23.32" : "0.0.0-dev"
|
|
68613
68745
|
});
|
|
68614
68746
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68615
68747
|
}
|
|
@@ -68933,7 +69065,7 @@ var init_channel = __esm2({
|
|
|
68933
69065
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68934
69066
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68935
69067
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68936
|
-
pluginVersion: true ? "0.23.
|
|
69068
|
+
pluginVersion: true ? "0.23.32" : "0.0.0-dev"
|
|
68937
69069
|
});
|
|
68938
69070
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68939
69071
|
}
|
|
@@ -69349,11 +69481,13 @@ var init_channel = __esm2({
|
|
|
69349
69481
|
let text = plaintext;
|
|
69350
69482
|
let messageType = "text";
|
|
69351
69483
|
let topicId = data.topic_id;
|
|
69484
|
+
let mlsAttachment = null;
|
|
69352
69485
|
try {
|
|
69353
69486
|
const parsed = JSON.parse(plaintext);
|
|
69354
69487
|
text = parsed.text ?? plaintext;
|
|
69355
69488
|
messageType = parsed.type ?? "text";
|
|
69356
69489
|
topicId = parsed.topicId ?? topicId;
|
|
69490
|
+
if (parsed.attachment) mlsAttachment = parsed.attachment;
|
|
69357
69491
|
} catch {
|
|
69358
69492
|
}
|
|
69359
69493
|
if (messageType === "history_catchup_request") {
|
|
@@ -69420,6 +69554,9 @@ var init_channel = __esm2({
|
|
|
69420
69554
|
console.log(`[SecureChannel] Session ${convId.slice(0, 8)} activated via MLS message`);
|
|
69421
69555
|
}
|
|
69422
69556
|
}
|
|
69557
|
+
if (mlsAttachment) {
|
|
69558
|
+
text = await this._augmentWithAttachment(mlsAttachment, text);
|
|
69559
|
+
}
|
|
69423
69560
|
const metadata = {
|
|
69424
69561
|
messageId: data.message_id ?? `mls_${Date.now()}`,
|
|
69425
69562
|
conversationId: convId ?? this._primaryConversationId,
|
|
@@ -69669,10 +69806,70 @@ ${messageText}`;
|
|
|
69669
69806
|
* Download an encrypted attachment blob, decrypt it, verify integrity,
|
|
69670
69807
|
* and save the plaintext file to disk.
|
|
69671
69808
|
*/
|
|
69809
|
+
/** Where an attachment may be written — always directly inside `attachDir` (#1143).
|
|
69810
|
+
*
|
|
69811
|
+
* The filename arrives INSIDE the envelope and is chosen by whoever sent the
|
|
69812
|
+
* message. `join(attachDir, info.filename)` resolves `..` normally, so
|
|
69813
|
+
* `../../agentvault.json` wrote over the device credential store. Only the
|
|
69814
|
+
* final path component is honoured, and anything that is not a usable name
|
|
69815
|
+
* falls back to a fixed one.
|
|
69816
|
+
*/
|
|
69817
|
+
_safeAttachmentPath(attachDir, filename) {
|
|
69818
|
+
const raw = String(filename ?? "");
|
|
69819
|
+
const last = raw.split(/[\\/]/).pop() ?? "";
|
|
69820
|
+
const cleaned = last.replace(/\0/g, "").trim();
|
|
69821
|
+
const safe = cleaned === "" || cleaned === "." || cleaned === ".." ? "attachment.bin" : cleaned;
|
|
69822
|
+
return join4(attachDir, safe);
|
|
69823
|
+
}
|
|
69824
|
+
/** Download an attachment and fold it into the text the agent sees (#1139).
|
|
69825
|
+
*
|
|
69826
|
+
* Extracted so the MLS receive path can use it too. It used to live inline in
|
|
69827
|
+
* `_handleIncomingMessage`, which meant attachments worked on the Double
|
|
69828
|
+
* Ratchet path ONLY — and hermes drops DR by design (`secure_channel.py:594`,
|
|
69829
|
+
* "DR fallback is logged-and-dropped"), so no single transport could serve
|
|
69830
|
+
* both client families. That is why an attachment reached ohtani and never
|
|
69831
|
+
* reached smartcoder2 on 2026-08-31.
|
|
69832
|
+
*
|
|
69833
|
+
* Returns the text unchanged when there is no attachment or the fetch fails —
|
|
69834
|
+
* but a failure is LOGGED and announced in the text, never silent.
|
|
69835
|
+
*/
|
|
69836
|
+
async _augmentWithAttachment(attachmentInfo, baseText) {
|
|
69837
|
+
if (!attachmentInfo) return baseText;
|
|
69838
|
+
try {
|
|
69839
|
+
const { filePath, decrypted } = await this._downloadAndDecryptAttachment(attachmentInfo);
|
|
69840
|
+
const mime = String(attachmentInfo.mime ?? "application/octet-stream");
|
|
69841
|
+
const name = String(attachmentInfo.filename ?? "attachment");
|
|
69842
|
+
const textMimes = ["text/", "application/json", "application/xml", "application/csv"];
|
|
69843
|
+
if (textMimes.some((m2) => mime.startsWith(m2))) {
|
|
69844
|
+
const content = new TextDecoder().decode(decrypted);
|
|
69845
|
+
return `[Attachment: ${name} (${mime}) saved to ${filePath}]
|
|
69846
|
+
---
|
|
69847
|
+
${content}
|
|
69848
|
+
---
|
|
69849
|
+
|
|
69850
|
+
${baseText}`;
|
|
69851
|
+
}
|
|
69852
|
+
if (mime.startsWith("image/")) {
|
|
69853
|
+
return `[Image attachment: ${name} saved to ${filePath}]
|
|
69854
|
+
Use your Read tool to view this image file.
|
|
69855
|
+
|
|
69856
|
+
${baseText}`;
|
|
69857
|
+
}
|
|
69858
|
+
return `[Attachment: ${name} saved to ${filePath}]
|
|
69859
|
+
|
|
69860
|
+
${baseText}`;
|
|
69861
|
+
} catch (err) {
|
|
69862
|
+
console.error("[SecureChannel] Failed to download attachment:", err);
|
|
69863
|
+
return `[Attachment: ${String(attachmentInfo.filename ?? "file")} was sent but could not be retrieved: ${err}]
|
|
69864
|
+
|
|
69865
|
+
${baseText}`;
|
|
69866
|
+
}
|
|
69867
|
+
}
|
|
69672
69868
|
async _downloadAndDecryptAttachment(info) {
|
|
69673
69869
|
const attachDir = join4(this.config.dataDir, "attachments");
|
|
69674
69870
|
await mkdir3(attachDir, { recursive: true });
|
|
69675
|
-
const
|
|
69871
|
+
const sep2 = info.blobUrl.includes("?") ? "&" : "?";
|
|
69872
|
+
const url22 = `${this.config.apiUrl}${info.blobUrl}${sep2}device_id=${encodeURIComponent(this._deviceId ?? "")}`;
|
|
69676
69873
|
const res = await fetch(url22, {
|
|
69677
69874
|
headers: { Authorization: `Bearer ${this._deviceJwt}` }
|
|
69678
69875
|
});
|
|
@@ -69688,7 +69885,7 @@ ${messageText}`;
|
|
|
69688
69885
|
const fileKey = base64ToBytes(info.fileKey);
|
|
69689
69886
|
const fileNonce = base64ToBytes(info.fileNonce);
|
|
69690
69887
|
const decrypted = decryptFile(encryptedData, fileKey, fileNonce);
|
|
69691
|
-
const filePath =
|
|
69888
|
+
const filePath = this._safeAttachmentPath(attachDir, info.filename);
|
|
69692
69889
|
await writeFile3(filePath, decrypted);
|
|
69693
69890
|
console.log(`[SecureChannel] Attachment saved: ${filePath} (${decrypted.length} bytes)`);
|
|
69694
69891
|
return { filePath, decrypted };
|
|
@@ -70224,10 +70421,12 @@ ${messageText}`;
|
|
|
70224
70421
|
const rawPlaintext = new TextDecoder().decode(result.plaintext);
|
|
70225
70422
|
let messageText;
|
|
70226
70423
|
let messageType;
|
|
70424
|
+
let roomAttachment = null;
|
|
70227
70425
|
try {
|
|
70228
70426
|
const parsed = JSON.parse(rawPlaintext);
|
|
70229
70427
|
messageType = parsed.type || "message";
|
|
70230
70428
|
messageText = parsed.text || rawPlaintext;
|
|
70429
|
+
if (parsed.attachment) roomAttachment = parsed.attachment;
|
|
70231
70430
|
} catch {
|
|
70232
70431
|
messageType = "message";
|
|
70233
70432
|
messageText = rawPlaintext;
|
|
@@ -70267,6 +70466,9 @@ ${messageText}`;
|
|
|
70267
70466
|
await this._refreshRoomRoster(resolvedRoomId);
|
|
70268
70467
|
}
|
|
70269
70468
|
}
|
|
70469
|
+
if (roomAttachment) {
|
|
70470
|
+
messageText = await this._augmentWithAttachment(roomAttachment, messageText);
|
|
70471
|
+
}
|
|
70270
70472
|
const roomMembers = this._persisted?.rooms?.[resolvedRoomId]?.members ?? [];
|
|
70271
70473
|
const senderMember = roomMembers.find((m2) => m2.deviceId === senderDeviceId);
|
|
70272
70474
|
const senderIsAgent = senderMember?.entityType === "agent";
|
|
@@ -71109,6 +71311,7 @@ ${messageText}`;
|
|
|
71109
71311
|
await this._persistState();
|
|
71110
71312
|
}
|
|
71111
71313
|
console.log(`[SecureChannel] Joined shared 1:1 MLS group for group ${conversationGroupId.slice(0, 8)} via Welcome (epoch=${mgr.epoch})`);
|
|
71314
|
+
await this._flushMlsHold(conversationGroupId);
|
|
71112
71315
|
return;
|
|
71113
71316
|
}
|
|
71114
71317
|
if (conversationId) {
|
|
@@ -99222,7 +99425,7 @@ var init_index = __esm2({
|
|
|
99222
99425
|
await init_skill_telemetry();
|
|
99223
99426
|
await init_policy_enforcer();
|
|
99224
99427
|
init_room_protocol();
|
|
99225
|
-
VERSION = true ? "0.23.
|
|
99428
|
+
VERSION = true ? "0.23.32" : "0.0.0-dev";
|
|
99226
99429
|
}
|
|
99227
99430
|
});
|
|
99228
99431
|
await init_index();
|
|
@@ -100148,7 +100351,7 @@ async function main() {
|
|
|
100148
100351
|
const info = parseInfoFlags(process.argv.slice(2));
|
|
100149
100352
|
if (info.showVersion || info.showHelp) {
|
|
100150
100353
|
const version2 = buildClientVersion2(
|
|
100151
|
-
true ? "0.4.
|
|
100354
|
+
true ? "0.4.5" : void 0
|
|
100152
100355
|
);
|
|
100153
100356
|
if (info.showVersion) printLine(version2);
|
|
100154
100357
|
if (info.showHelp) {
|
|
@@ -100201,7 +100404,7 @@ async function main() {
|
|
|
100201
100404
|
agentName: cfg.agentName,
|
|
100202
100405
|
platform: "node",
|
|
100203
100406
|
clientVersion: buildClientVersion2(
|
|
100204
|
-
true ? "0.4.
|
|
100407
|
+
true ? "0.4.5" : void 0
|
|
100205
100408
|
)
|
|
100206
100409
|
});
|
|
100207
100410
|
channelRef = channel;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type BridgeConfig } from "../config.js";
|
|
2
|
+
import type { ReapResult } from "../reap.js";
|
|
3
|
+
import { type ServiceBackend } from "./backend.js";
|
|
4
|
+
export interface InstallOnReadyDeps {
|
|
5
|
+
entrypoint: string;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
backend?: ServiceBackend;
|
|
8
|
+
hasCreds?: (dataDir: string) => boolean;
|
|
9
|
+
/** Contents of the installed launcher, or undefined when none is installed. */
|
|
10
|
+
readLauncher?: (path: string) => string | undefined;
|
|
11
|
+
fenceAuthExists?: (fenceHome: string) => boolean;
|
|
12
|
+
/** Resolves when no turn is in flight (GrokSession.idle). See rule 2. */
|
|
13
|
+
waitForIdle?: () => Promise<void>;
|
|
14
|
+
/** Bound on that wait. The terminal coming back is the product promise, so a
|
|
15
|
+
* wedged turn must never hold the customer's shell open. */
|
|
16
|
+
idleTimeoutMs?: number;
|
|
17
|
+
/** The ONE teardown (src/reap.ts). Must complete before launchctl runs. */
|
|
18
|
+
reap: () => Promise<ReapResult>;
|
|
19
|
+
exit: (code: number) => void;
|
|
20
|
+
log: (msg: string) => void;
|
|
21
|
+
}
|
|
22
|
+
interface ReadyChannel {
|
|
23
|
+
on(ev: "ready", cb: () => void): unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One-paste install: enrol in the foreground, hand off to the supervisor, exit —
|
|
27
|
+
* so the customer's terminal comes back and the agent survives a reboot.
|
|
28
|
+
*
|
|
29
|
+
* Four rules this module exists to enforce.
|
|
30
|
+
*
|
|
31
|
+
* 1. REFUSE BEFORE REAPING. Everything that can say no does so before anything is
|
|
32
|
+
* torn down. Refusing afterwards leaves the customer with an approved device,
|
|
33
|
+
* a spent invite token, and no running process — strictly worse than never
|
|
34
|
+
* having guarded at all.
|
|
35
|
+
*
|
|
36
|
+
* 2. REAP COMPLETELY, THEN BOOTSTRAP. `LaunchdBackend.install` runs bootout →
|
|
37
|
+
* poll-until-gone → bootstrap → kickstart synchronously, so the service is
|
|
38
|
+
* live the instant install returns. A partial teardown means two ACP sessions
|
|
39
|
+
* and two WebSockets on one device. `reap()` reports failure rather than
|
|
40
|
+
* swallowing it, and a failed reap is a refusal.
|
|
41
|
+
*
|
|
42
|
+
* 3. COMPARE, DON'T DETECT. `status().loaded` means only that a plist with this
|
|
43
|
+
* label exists. The old `install` subcommand never guarded its entrypoint
|
|
44
|
+
* (subcommand.ts), so a customer can hold a LOADED plist baked against a
|
|
45
|
+
* pruned npx cache; restarting that repairs nothing and reports success.
|
|
46
|
+
* Restart only when a service is loaded AND its baked entrypoint is the one we
|
|
47
|
+
* would install. Compare the ENTRYPOINT, never the whole launcher script —
|
|
48
|
+
* that embeds the install-time PATH and is volatile across shells.
|
|
49
|
+
*
|
|
50
|
+
* 4. VERIFY BEFORE CLAIMING. `backend.install` returns void, and launchd's
|
|
51
|
+
* implementation runs `kickstart` regardless of every `bootstrap` exit code.
|
|
52
|
+
* This log line is the customer's only feedback, so it comes after a
|
|
53
|
+
* `status()` that returned loaded:true.
|
|
54
|
+
*/
|
|
55
|
+
export declare function installOnReady(channel: ReadyChannel, cfg: BridgeConfig, deps: InstallOnReadyDeps): void;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=install-on-ready.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-on-ready.d.ts","sourceRoot":"","sources":["../../src/service/install-on-ready.ts"],"names":[],"mappings":"AAEA,OAAO,EAA4C,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAElE,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACpD,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC;iEAC6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B;AAMD,UAAU,YAAY;IACpB,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,kBAAkB,GACvB,IAAI,CAgHN"}
|
package/package.json
CHANGED