@bivy/bivy 0.20.9-staging.2 → 0.20.9-staging.21
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 +6 -5
- package/bin/bivy.mjs +13 -56
- package/bin/cli-commands.mjs +2 -4
- package/bin/guides/talk-to-the-user.md +13 -24
- package/dist/agent-instructions.js +12 -14
- package/dist/agents/pi/integration.js +1 -1
- package/dist/apps/remote-preview.js +3 -0
- package/dist/apps/service.js +6 -1
- package/dist/apps/styles.css +40 -102
- package/dist/automation-reseal.js +30 -0
- package/dist/certification/generated.js +1 -1
- package/dist/device-registry.js +27 -0
- package/dist/ephemeral-teardown.js +11 -1
- package/dist/exec.js +4 -1
- package/dist/runtime/model-catalog.js +16 -0
- package/dist/server.js +73 -62
- package/dist/session/event-log.js +98 -41
- package/dist/session/inline-image-fetch.js +24 -33
- package/dist/session/message-components.js +323 -0
- package/dist/session/run-terminal.js +9 -7
- package/dist/session/session-tokens.js +1 -1
- package/dist/session/transcript-persistence.js +106 -11
- package/package.json +6 -6
- package/web/assets/AutomationsView-Cobo4Kep.js +51 -0
- package/web/assets/{ChangesCard-DcSRy5g_.js → ChangesCard-Bo6E8C_y.js} +3 -3
- package/web/assets/LibraryView-BTeMc6BO.js +2 -0
- package/web/assets/{ProviderConnect-cpGwBMrU.js → ProviderConnect-CsidS7kq.js} +1 -1
- package/web/assets/SessionList-B7IbeXSa.js +1 -0
- package/web/assets/Settings-BQRQsQ36.js +10 -0
- package/web/assets/{Sheet-D-5t3lJ0.js → Sheet-C0BIPbMr.js} +1 -1
- package/web/assets/{Terminal-CjMjKB1f.js → Terminal-BTRgonB5.js} +1 -1
- package/web/assets/{VoiceSettings-xv6rjaY2.js → VoiceSettings-UZ1SA0lQ.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-BLT3MLyK.js → abnfDiagram-VCTEODGH-CJoWsvVk.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-CuzcodTO.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-BGinLETW.js → architectureDiagram-5GKGNRK7-_qpa4O1v.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-BNiY2HLB.js → blockDiagram-I7D4REHJ-DmLgwSr-.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-46aGfRMd.js → c4Diagram-7LVT6UL2-DeZuOOSk.js} +1 -1
- package/web/assets/channel-B0OL-aPd.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-CRzer-vm.js → chunk-4HAMMTFA-Cr6O6lBG.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-MFkgf2Ar.js → chunk-75Z2AOVW-gSgNIYMY.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-Bpze6ID8.js → chunk-DU6HZSFF-BacnRf2Y.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-Dq3OmNXG.js → chunk-F27PBJKO-DRrpfF1z.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-umDWdG9g.js → chunk-GMAD6QVW-DXCi6v6B.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-DuT1Awt9.js → chunk-GVQU2GXP-Yd74hIeT.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-CpFFccVb.js → chunk-IMKFNOWR-D5xz0-QC.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-0jRWrASa.js → chunk-L3NEJ4N5-BrS0CGPM.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-CBErW4-p.js → chunk-OSK3NFVY-C6d1DSJe.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-BPMii7LM.js → chunk-P2QGCYS3-BrKgIngm.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-CHTRMb_C.js → chunk-POPQ4Y6H-Bkwx5BZs.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-BOcNKrmu.js → chunk-PWAF6VOD-AeCCeE0F.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-Dvb-oEDN.js → chunk-SHT3W25Y-01NYjozy.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-B4AQIUTK.js → chunk-SVP7TREG-CCh7wXAI.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-CWzLTsHW.js → chunk-TICWLB2K-CcWKqwdM.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-D8dSdFQt.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-D8dSdFQt.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-BPkKT48j.js → cynefin-OW5HDTMX-CYYc_tBE.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-Ck2qLmpR.js → cynefinDiagram-5FMLGOSQ-Dnz7OfxK.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-BPKEs_49.js → dagre-GXQ25YYZ-di5CyaMt.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-ou-L5HdJ.js → diagram-S7CK7UJ4-BVC93OLB.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-DihVn6fF.js → diagram-UQ7AKVKN-D7pXHAvQ.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-BkLtxbP7.js → diagram-VSXAHHWV-d0zW-IUz.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-BZ-Z-CwP.js → diagram-VX7I27RA-BRlMo9oa.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-BQN-bGqD.js → diagram-Z3DM3KII-BmfntB1i.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-aKK1a_it.js → ebnfDiagram-PWID7BFC-BxUYvqsz.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-BGsXtJl-.js → erDiagram-RLTQ6QDP-CbUl1Cqa.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-sTVSUyNU.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-Beo3zBEU.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-lrYuyNAe.js → ganttDiagram-EL5Y4UJY-CkpR0avQ.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-raj_IxZ8.js → gitGraph-4MIJSDKK-CmbJxSaM.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-DzqRGXuk.js → gitGraphDiagram-WWUBYQGX-iyO408KF.js} +1 -1
- package/web/assets/index-B4HaCD-n.css +1 -0
- package/web/assets/{index-B6GPr2Pt.js → index-fdEPLcPs.js} +2 -2
- package/web/assets/{info-A6RAGUB7-DJVi9SAN.js → info-A6RAGUB7--6JCM0xX.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-B2HrGywP.js → infoDiagram-27XIBGKW-Dchx6QcB.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-CczIkvWw.js → ishikawaDiagram-5VMMS53U-DOY3izUi.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-D-h-kwep.js → journeyDiagram-3NMN7TZE-oqZgC9oJ.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-GClrFWfH.js → kanban-definition-UXKFOSKX-HCRC0D9X.js} +1 -1
- package/web/assets/{line-pweWV0oc.js → line-qcY2eok7.js} +1 -1
- package/web/assets/{mermaid-parser.core-C1vHZCtC.js → mermaid-parser.core-Bullp1xl.js} +3 -3
- package/web/assets/{mermaid.core-Df8DJ7AE.js → mermaid.core-FBmsAtMC.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-BpqA307n.js → mindmap-definition-YA3MSWOX-CfgOAkVQ.js} +1 -1
- package/web/assets/mount-IY4-2elJ.js +31 -0
- package/web/assets/{packet-AYTQ26CC-afE6T0Hc.js → packet-AYTQ26CC-COAmiopo.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-AAEw7Khy.js → pegDiagram-XKGWAZYB-BYafVbeG.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-vtwSUVVK.js → pie-WAS4IAKB-ZYe346n6.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-CDg0SkhS.js → pieDiagram-E7YTZNPT-DnRbaA7i.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-_o0rOR26.js → quadrantDiagram-AXDQQJYC-B2uxyKLd.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-_Tf7tpz8.js → radar-RG4KPBEZ-DwFlqYgB.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-DM_a0HtL.js → railroad-74A4TZTK-DBv5jh9o.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-VQxaaUVU.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-DWKO1s90.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-DvViPquF.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-BpQZeQ-f.js → railroadDiagram-O6MQD6OU-CIUFwDWd.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-DC4US4K_.js → requirementDiagram-BXWQKSXE-BXwDlf9Q.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-BUJ9KIqV.js → sankeyDiagram-P5KCCOFB-DTiRcy5E.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-By7D5XFY.js → sequenceDiagram-WJ2MYXX4-DFPRDr4a.js} +1 -1
- package/web/assets/settingsRoute-3489vHS0.js +1 -0
- package/web/assets/{stateDiagram-D77RDMKH-D0BA3EeM.js → stateDiagram-D77RDMKH-CFQ-4xlY.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-DIIQkE2r.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-BNUI3ghm.js → swimlanes-42K2YHIH-Bke6VXs0.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-Drds9YUy.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-DVzeoz4F.js → timeline-definition-24CTP7MA-Cs2-dpWN.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-CnvdZw-U.js → treeView-Q6P3EWNA-Dr8vsD3U.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-Ax_RXzPk.js → treemap-WGGIJYW6-DbvC3nsA.js} +1 -1
- package/web/assets/useStore-BDEjghiw.js +23 -0
- package/web/assets/{vennDiagram-4TSXK5OY-CNauH8S4.js → vennDiagram-4TSXK5OY-CdUoLWPo.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-q8WnmSMu.js → wardley-WFR3VGLG-BL7j6fb-.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-DKOY4P11.js → wardleyDiagram-VM6X3IG4-BEVYmHWI.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-m8KaPfml.js → xychartDiagram-S5SC5T6Z-C9zn_1JB.js} +1 -1
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/dist/ephemeral-exec.js +0 -108
- package/dist/session/suggestions.js +0 -17
- package/web/assets/AutomationsView-DEild20v.js +0 -51
- package/web/assets/LibraryView-Cu0tU0gU.js +0 -2
- package/web/assets/Settings-BoTU83-i.js +0 -10
- package/web/assets/architecture-7GRP2DOG-s28sMQ0-.js +0 -1
- package/web/assets/channel-CPSM7rj7.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-CZ-So67n.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-CZ-So67n.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-Dd2mk6iz.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-D7f1jYIL.js +0 -1
- package/web/assets/index-D76QD5dl.css +0 -1
- package/web/assets/mount-ISRAcaLu.js +0 -33
- package/web/assets/railroad-abnf-HS5TGJTU-BzFZIgti.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-BkKCtTE4.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-CuXWl9dG.js +0 -1
- package/web/assets/settingsRoute-DSj-6Pdo.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-2_bDqGCB.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-SmXx3nAj.js +0 -8
- package/web/assets/useStore-BznE40dl.js +0 -29
package/dist/device-registry.js
CHANGED
|
@@ -7,6 +7,7 @@ import { generatePairingKeypair, generateRoomKey, generatePairSecret, deriveWrap
|
|
|
7
7
|
import { seal, open } from "./e2e.js";
|
|
8
8
|
const DEFAULT_PAIR_TTL_MS = 5 * 60_000;
|
|
9
9
|
const ROOM_KEY_BYTES = 32;
|
|
10
|
+
const MAX_RETIRED_ROOM_KEYS = 8;
|
|
10
11
|
/**
|
|
11
12
|
* Validate + canonicalize a pre-shared room-key seed. Returns the canonical
|
|
12
13
|
* base64 of a well-formed 32-byte key, or undefined for anything malformed
|
|
@@ -81,6 +82,7 @@ export class PairingStore {
|
|
|
81
82
|
return new PairingStore(filePath, {
|
|
82
83
|
nodeKeypair: parsed.nodeKeypair,
|
|
83
84
|
roomKeyB64: parsed.roomKeyB64,
|
|
85
|
+
...(Array.isArray(parsed.retiredRoomKeysB64) && parsed.retiredRoomKeysB64.length ? { retiredRoomKeysB64: parsed.retiredRoomKeysB64 } : {}),
|
|
84
86
|
devices: Array.isArray(parsed.devices) ? parsed.devices : [],
|
|
85
87
|
});
|
|
86
88
|
}
|
|
@@ -130,6 +132,30 @@ export class PairingStore {
|
|
|
130
132
|
roomKey() {
|
|
131
133
|
return Buffer.from(this.data.roomKeyB64, "base64");
|
|
132
134
|
}
|
|
135
|
+
/** Room keys replaced by earlier rotations that still guard stored data. */
|
|
136
|
+
retiredRoomKeys() {
|
|
137
|
+
return (this.data.retiredRoomKeysB64 ?? []).map((key) => Buffer.from(key, "base64"));
|
|
138
|
+
}
|
|
139
|
+
/** Drop retired room keys once nothing stored is sealed under them. */
|
|
140
|
+
forgetRetiredRoomKeys() {
|
|
141
|
+
if (!this.data.retiredRoomKeysB64)
|
|
142
|
+
return;
|
|
143
|
+
delete this.data.retiredRoomKeysB64;
|
|
144
|
+
this.persist();
|
|
145
|
+
}
|
|
146
|
+
/** Open a payload sealed under the current room key or a retired one. */
|
|
147
|
+
openSealed(payload) {
|
|
148
|
+
let lastError;
|
|
149
|
+
for (const key of [this.roomKey(), ...this.retiredRoomKeys()]) {
|
|
150
|
+
try {
|
|
151
|
+
return open(key, payload);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
lastError = error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw lastError;
|
|
158
|
+
}
|
|
133
159
|
/** Issue a single-use, expiring pairing secret to embed in a linking QR. */
|
|
134
160
|
issuePairSecret(ttlMs = DEFAULT_PAIR_TTL_MS) {
|
|
135
161
|
this.sweepSecrets();
|
|
@@ -215,6 +241,7 @@ export class PairingStore {
|
|
|
215
241
|
}
|
|
216
242
|
/** Rotate the room key and re-wrap it for all current devices. */
|
|
217
243
|
rotateRoomKey() {
|
|
244
|
+
this.data.retiredRoomKeysB64 = [this.data.roomKeyB64, ...(this.data.retiredRoomKeysB64 ?? [])].slice(0, MAX_RETIRED_ROOM_KEYS);
|
|
218
245
|
this.data.roomKeyB64 = generateRoomKey().toString("base64");
|
|
219
246
|
this.persist();
|
|
220
247
|
const roomKey = this.roomKey();
|
|
@@ -9,6 +9,7 @@ export function readEphemeralTeardownConfig(env = process.env) {
|
|
|
9
9
|
onFinish: env.BIVY_TEARDOWN_ON_FINISH === "1",
|
|
10
10
|
finishGraceMs: Number(env.BIVY_TEARDOWN_FINISH_GRACE_MS) || 10_000,
|
|
11
11
|
idleGraceMs: Number(env.BIVY_SESSION_IDLE_CLOSE_MS) || 30 * 60 * 1000,
|
|
12
|
+
sleep: env.BIVY_EPHEMERAL_SLEEP === "1",
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
@@ -20,8 +21,10 @@ export function readEphemeralTeardownConfig(env = process.env) {
|
|
|
20
21
|
export function shouldSelfTeardown(cfg, state) {
|
|
21
22
|
if (!cfg.enabled)
|
|
22
23
|
return false;
|
|
24
|
+
// A fresh box waits for its first work. A sleeping machine woken for
|
|
25
|
+
// nothing goes back to sleep after the idle window instead of running to TTL.
|
|
23
26
|
if (!state.everBusy)
|
|
24
|
-
return
|
|
27
|
+
return cfg.sleep && state.idleForMs >= cfg.idleGraceMs;
|
|
25
28
|
if (state.anyWorking || state.anyRemoteActive || state.inFlightWork > 0)
|
|
26
29
|
return false;
|
|
27
30
|
const grace = cfg.onFinish ? cfg.finishGraceMs : cfg.idleGraceMs;
|
|
@@ -50,6 +53,13 @@ export async function performSelfTeardown(deps) {
|
|
|
50
53
|
torndown = true;
|
|
51
54
|
const log = deps.log ?? ((m) => console.log(`[ephemeral-teardown] ${m}`));
|
|
52
55
|
const exit = deps.exit ?? ((c) => process.exit(c));
|
|
56
|
+
if (deps.sleep) {
|
|
57
|
+
// Exit only: the provider keeps the stopped machine and its disk, and a
|
|
58
|
+
// wake starts it again. Signalling settled would get it reaped.
|
|
59
|
+
log(`machine idle — sleeping (provider=${deps.provider})`);
|
|
60
|
+
exit(0);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
53
63
|
log(`machine idle — self-teardown (provider=${deps.provider})`);
|
|
54
64
|
try {
|
|
55
65
|
await deps.signalSettled?.();
|
package/dist/exec.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// A turn that ends without any assistant text because the agent FAILED (e.g. an
|
|
15
15
|
// unauthenticated CLI) exits non-zero with the error on stderr, rather than
|
|
16
16
|
// reporting a silent empty success.
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { WebSocket } from "ws";
|
|
18
19
|
const USAGE = `Usage: bivy exec [options] "<prompt>" (or pipe the prompt on stdin, or pass -)
|
|
19
20
|
|
|
@@ -223,7 +224,9 @@ async function main() {
|
|
|
223
224
|
try {
|
|
224
225
|
await api(args.url, args.token, "/api/session/prompt", {
|
|
225
226
|
method: "POST",
|
|
226
|
-
|
|
227
|
+
// One id per exec run: the node drops a repeated id as a duplicate send
|
|
228
|
+
// for a day, so an id shared by every run in a session ate follow-ups.
|
|
229
|
+
body: JSON.stringify({ sessionId, text: prompt, clientMessageId: `exec-${randomUUID()}` }),
|
|
227
230
|
});
|
|
228
231
|
}
|
|
229
232
|
catch (error) {
|
|
@@ -147,3 +147,19 @@ export async function catalogReplyEvent(load, sessionId) {
|
|
|
147
147
|
return { type: "session.error", sessionId, error: `Couldn't read this machine's model catalog: ${error instanceof Error ? error.message : String(error)}` };
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Give a session's agent up to `timeoutMs` to publish its real model catalog
|
|
152
|
+
* before a caller reads it. A not-yet-started agent otherwise answers from a
|
|
153
|
+
* placeholder list. Never throws; a runtime without warmModels() is a no-op.
|
|
154
|
+
*/
|
|
155
|
+
export async function warmSessionCatalog(session, timeoutMs) {
|
|
156
|
+
if (!session.warmModels)
|
|
157
|
+
return;
|
|
158
|
+
let timer;
|
|
159
|
+
await Promise.race([
|
|
160
|
+
session.warmModels().catch(() => { }),
|
|
161
|
+
new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); }),
|
|
162
|
+
]);
|
|
163
|
+
if (timer)
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -44,7 +44,7 @@ import { createAppCommands } from "./controllers/app-commands.js";
|
|
|
44
44
|
import { createFileCommands } from "./controllers/file-commands.js";
|
|
45
45
|
import { bindClientCommandRoutes } from "./http/client-command-routes.js";
|
|
46
46
|
import { collectDiscoveredSessions, planNativeAdoption } from "./runtime/native-session-discovery.js";
|
|
47
|
-
import { aggregateModelCatalog, catalogReplyEvent, mergeProviderCatalog } from "./runtime/model-catalog.js";
|
|
47
|
+
import { aggregateModelCatalog, catalogReplyEvent, warmSessionCatalog, mergeProviderCatalog } from "./runtime/model-catalog.js";
|
|
48
48
|
import { resolveModelRef } from "./runtime/model-ref.js";
|
|
49
49
|
import { RuntimeHost, enforcementLevelFor, remoteRuntimeEnabled } from "./runtime/host.js";
|
|
50
50
|
import { RemoteRuntime, RemoteRuntimeSession } from "./runtime/remote.js";
|
|
@@ -54,6 +54,7 @@ import { ControlPlaneSessionLocationRegistry, LayeredSessionLocationRegistry } f
|
|
|
54
54
|
import { attachAdoptedSessions, classifyAttachFailure } from "./runtime/adoption.js";
|
|
55
55
|
import { createCredentialStore, testProviderCredential } from "./runtime/credentials.js";
|
|
56
56
|
import { decodeAutomationTemplate } from "./automation-template.js";
|
|
57
|
+
import { resealAutomationTemplates } from "./automation-reseal.js";
|
|
57
58
|
import { passWebhookFilter } from "./automation-filter-gate.js";
|
|
58
59
|
import { isModelAuthError, authProviderForSession, classifyModelAuthError } from "./runtime/auth-errors.js";
|
|
59
60
|
import { createCredentialVault, migrateVaultDir } from "./runtime/credential-store.js";
|
|
@@ -80,9 +81,7 @@ import { exportProviderAuth, exportAccountApiKeys, exportAccountOAuthCredentials
|
|
|
80
81
|
import { listProviders } from "./runtime/provider-catalog.js";
|
|
81
82
|
import { exportLocalModels, importLocalModels } from "./runtime/local-model-store.js";
|
|
82
83
|
import { sessionLikeFields } from "./session/start-like.js";
|
|
83
|
-
import { MAX_SUGGESTION_TEXT, MAX_SUGGESTION_TITLE, SUGGESTION_RUNS, isTaskSuggestion } from "./session/suggestions.js";
|
|
84
84
|
import { BIVY_AGENT_NOTE, mergeSyncedAgentInstructions, readAgentInstructions, sessionInstructions, writeAgentInstructions, MAX_AGENT_INSTRUCTIONS_BYTES } from "./agent-instructions.js";
|
|
85
|
-
import { execEphemeralRequest } from "./ephemeral-exec.js";
|
|
86
85
|
import { ApprovalManager } from "./approval.js";
|
|
87
86
|
import { QuestionManager, validQuestions, isAskUserQuestionTool, formatQuestionResult } from "./question.js";
|
|
88
87
|
import { NodeIdentity } from "./identity.js";
|
|
@@ -143,7 +142,7 @@ import { AgentQuestions, askQuestionsFrom } from "./session/agent-questions.js";
|
|
|
143
142
|
import { AutomationProposals, describeChange } from "./session/automation-proposals.js";
|
|
144
143
|
import { createSessionTokenCodec, isSessionToken, sessionTokenAllows } from "./session/session-tokens.js";
|
|
145
144
|
import { setSessionTokenSigner } from "./runtime/session-env.js";
|
|
146
|
-
import { MAX_NOTICE_TEXT,
|
|
145
|
+
import { MAX_NOTICE_TEXT, noticePush } from "./session/notices.js";
|
|
147
146
|
import { createForkCommands } from "./controllers/fork-commands.js";
|
|
148
147
|
import { createGithubCommands } from "./controllers/github-commands.js";
|
|
149
148
|
import { createCredentialCommands } from "./controllers/credential-commands.js";
|
|
@@ -1082,6 +1081,7 @@ const linkedDevices = createLinkedDeviceController({
|
|
|
1082
1081
|
syncPairingMetadata();
|
|
1083
1082
|
relay?.pushRotate(deliveries);
|
|
1084
1083
|
broadcast({ type: "devices.updated", devices });
|
|
1084
|
+
void resealRetiredAutomationTemplates();
|
|
1085
1085
|
},
|
|
1086
1086
|
});
|
|
1087
1087
|
const accessDevices = createAccessDeviceController({
|
|
@@ -2268,7 +2268,7 @@ const notAgentServers = (offers) => offers.filter((offer) => !agentCommands.has(
|
|
|
2268
2268
|
const appService = new AppService(appRegistry, appGateway ?? remotePreview, {
|
|
2269
2269
|
start: async (spec) => {
|
|
2270
2270
|
let failure = "Could not start the app terminal.";
|
|
2271
|
-
const id = await runTerms.openRunTerminal({ ...spec, agent: "app", label: spec.name }, (event) => {
|
|
2271
|
+
const id = await runTerms.openRunTerminal({ ...spec, agent: "app", label: spec.name, background: true }, (event) => {
|
|
2272
2272
|
const e = event;
|
|
2273
2273
|
if (e.type === "terminal.error" && e.error)
|
|
2274
2274
|
failure = e.error;
|
|
@@ -2898,6 +2898,13 @@ const RELAY_COMMANDS = {
|
|
|
2898
2898
|
if (!requestedSessionId && wantedRuntimeId && record?.runtimeId !== wantedRuntimeId)
|
|
2899
2899
|
record = null;
|
|
2900
2900
|
record ??= await sessionForModelQuery(wantedRuntimeId);
|
|
2901
|
+
// A session whose agent hasn't started yet answers from the runtime's
|
|
2902
|
+
// placeholder catalog, which can be missing the account's current models —
|
|
2903
|
+
// a freshly launched machine then rejected a saved model it actually has.
|
|
2904
|
+
// Warm the real catalog first (a no-op re-read once the agent is running),
|
|
2905
|
+
// bounded so a slow spawn still answers with what is known.
|
|
2906
|
+
if (requestedSessionId)
|
|
2907
|
+
await warmSessionCatalog(record.session, SESSION_MODELS_WARM_MS);
|
|
2901
2908
|
// Tag the list with the runtime it was resolved for so the client can
|
|
2902
2909
|
// tell whether it belongs to the agent it currently has selected. Without
|
|
2903
2910
|
// this a models.list answered for one agent (e.g. Codex's models) could
|
|
@@ -3188,22 +3195,6 @@ const RELAY_COMMANDS = {
|
|
|
3188
3195
|
return ctx.reply({ type: "session.promote.result", requestId, ok: false, error: "Promotion lost the epoch race" });
|
|
3189
3196
|
ctx.reply({ type: "session.promote.result", requestId, ok: true, sessionId, epoch });
|
|
3190
3197
|
},
|
|
3191
|
-
// Ephemeral provisioning transport (node-broker path). A remote device that
|
|
3192
|
-
// holds the user's cloud credentials asks this node to make ONE allowlisted
|
|
3193
|
-
// HTTPS request to a provider (Fly/Hetzner/AWS/...) on its behalf. The
|
|
3194
|
-
// token/credentials ride in the request headers and are used transiently —
|
|
3195
|
-
// never persisted here — so the provisioning stays end-to-end (the control
|
|
3196
|
-
// plane never sees it). The host allowlist is the SSRF guard.
|
|
3197
|
-
async "ephemeral.exec"(msg) {
|
|
3198
|
-
const requestId = String(msg.requestId ?? "");
|
|
3199
|
-
try {
|
|
3200
|
-
const result = await execEphemeralRequest(msg.request);
|
|
3201
|
-
relay?.sendEvent({ type: "ephemeral.exec.result", requestId, ...result });
|
|
3202
|
-
}
|
|
3203
|
-
catch (error) {
|
|
3204
|
-
relay?.sendEvent({ type: "ephemeral.exec.result", requestId, error: error instanceof Error ? error.message : String(error) });
|
|
3205
|
-
}
|
|
3206
|
-
},
|
|
3207
3198
|
// Subscription / OAuth login driven from a remote device. The node runs the
|
|
3208
3199
|
// provider's device-code (or paste-back) flow and reports the verification
|
|
3209
3200
|
// link + code over the relay; the phone never needs the node's localhost.
|
|
@@ -3548,6 +3539,7 @@ function startRelayIfConfigured() {
|
|
|
3548
3539
|
if (config.controlPlaneUrl && config.enrollmentToken) {
|
|
3549
3540
|
sessionAdvertiseTarget = { controlPlaneUrl: config.controlPlaneUrl, enrollmentToken: config.enrollmentToken };
|
|
3550
3541
|
void syncModelAuthFromControlPlane();
|
|
3542
|
+
void resealRetiredAutomationTemplates();
|
|
3551
3543
|
if (remoteRuntimeEnabled()) {
|
|
3552
3544
|
// Stage 3: layer a control-plane-backed location registry UNDER the in-memory
|
|
3553
3545
|
// one (so a lookup surviving a restart resolves from durable state), then
|
|
@@ -3709,6 +3701,43 @@ async function modelAuthFetch(pathname, init = {}) {
|
|
|
3709
3701
|
headers.set("content-type", "application/json");
|
|
3710
3702
|
return fetch(`${sessionAdvertiseTarget.controlPlaneUrl.replace(/\/$/, "")}${pathname}`, { ...init, headers });
|
|
3711
3703
|
}
|
|
3704
|
+
// A device revoke rotates the room key. Re-seal this node's stored automation
|
|
3705
|
+
// instructions under the new key so the node and its devices can still read
|
|
3706
|
+
// them, then drop the retired keys. Retried at startup until it completes.
|
|
3707
|
+
async function resealRetiredAutomationTemplates() {
|
|
3708
|
+
const retired = pairingStore.retiredRoomKeys();
|
|
3709
|
+
if (!retired.length || !sessionAdvertiseTarget)
|
|
3710
|
+
return;
|
|
3711
|
+
const current = pairingStore.roomKey();
|
|
3712
|
+
try {
|
|
3713
|
+
const res = await modelAuthFetch("/node/automation-templates");
|
|
3714
|
+
if (!res?.ok)
|
|
3715
|
+
throw new Error(`control plane returned ${res?.status}`);
|
|
3716
|
+
const { templates } = await res.json();
|
|
3717
|
+
const result = await resealAutomationTemplates({
|
|
3718
|
+
nodeId: identity.nodeId,
|
|
3719
|
+
current,
|
|
3720
|
+
retired,
|
|
3721
|
+
templates,
|
|
3722
|
+
save: async (id, templateCiphertext) => {
|
|
3723
|
+
const saved = await modelAuthFetch(`/node/automation-templates/${encodeURIComponent(id)}`, { method: "PUT", body: JSON.stringify({ templateCiphertext }) });
|
|
3724
|
+
if (!saved?.ok)
|
|
3725
|
+
throw new Error(`control plane returned ${saved?.status}`);
|
|
3726
|
+
},
|
|
3727
|
+
});
|
|
3728
|
+
if (result.resealed)
|
|
3729
|
+
console.log(`[automation-reseal] re-sealed ${result.resealed} automation template(s) under the rotated room key`);
|
|
3730
|
+
if (result.unreadable.length)
|
|
3731
|
+
console.warn(`[automation-reseal] ${result.unreadable.length} automation template(s) were sealed under a key this node no longer has: ${result.unreadable.join(", ")}`);
|
|
3732
|
+
// Another rotation during this pass retired `current`; run again for it.
|
|
3733
|
+
if (!pairingStore.roomKey().equals(current))
|
|
3734
|
+
return void resealRetiredAutomationTemplates();
|
|
3735
|
+
pairingStore.forgetRetiredRoomKeys();
|
|
3736
|
+
}
|
|
3737
|
+
catch (error) {
|
|
3738
|
+
console.warn("[automation-reseal] will retry at next start:", error.message);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3712
3741
|
const remoteSessionAdmission = createRemoteSessionAdmission(async (idempotencyKey) => {
|
|
3713
3742
|
// Only remote scopes reach this callback. Self-hosted control planes answer
|
|
3714
3743
|
// allowed without an extension; losing enrollment mid-launch must fail closed.
|
|
@@ -5565,7 +5594,7 @@ async function executeWorkItem(item, report, signal) {
|
|
|
5565
5594
|
throw new Error("automation instructions were encrypted for a different node");
|
|
5566
5595
|
}
|
|
5567
5596
|
try {
|
|
5568
|
-
const template = decodeAutomationTemplate(
|
|
5597
|
+
const template = decodeAutomationTemplate(pairingStore.openSealed(payload.join(":")));
|
|
5569
5598
|
credentialLabels = template.credentialLabels;
|
|
5570
5599
|
filter = template.filter;
|
|
5571
5600
|
item = { ...item, body: template.instructions };
|
|
@@ -7620,7 +7649,7 @@ function evaluateEphemeralTeardown() {
|
|
|
7620
7649
|
const records = new Set(openSessions.values());
|
|
7621
7650
|
const anyWorking = [...records].some((r) => r.isWorking);
|
|
7622
7651
|
const anyRemoteActive = clients.size > 0 || (relay?.clientCount ?? 0) > 0
|
|
7623
|
-
|| [...records].some((r) => r.remoteActive);
|
|
7652
|
+
|| [...records].some((r) => r.remoteActive) || remotePreview.inUse;
|
|
7624
7653
|
const inFlightWork = controlPlanePoller?.inFlightCount() ?? 0;
|
|
7625
7654
|
if (anyWorking || anyRemoteActive || inFlightWork > 0) {
|
|
7626
7655
|
ephemeralEverBusy = true;
|
|
@@ -7643,6 +7672,7 @@ function evaluateEphemeralTeardown() {
|
|
|
7643
7672
|
}
|
|
7644
7673
|
await performSelfTeardown({
|
|
7645
7674
|
provider: ephemeralTeardownCfg.provider,
|
|
7675
|
+
sleep: ephemeralTeardownCfg.sleep,
|
|
7646
7676
|
signalSettled: signalSettledToControlPlane,
|
|
7647
7677
|
shutdown: () => { try {
|
|
7648
7678
|
spawnSync("shutdown", ["-h", "now"], { stdio: "ignore" });
|
|
@@ -7657,7 +7687,7 @@ if (ephemeralTeardownCfg.enabled) {
|
|
|
7657
7687
|
const ephemeralEvalMs = Math.max(2_000, Math.min(ephemeralTeardownCfg.finishGraceMs, 15_000));
|
|
7658
7688
|
const ephemeralTeardownTimer = setInterval(() => evaluateEphemeralTeardown(), ephemeralEvalMs);
|
|
7659
7689
|
ephemeralTeardownTimer.unref?.();
|
|
7660
|
-
console.log(`[ephemeral-teardown] armed: provider=${ephemeralTeardownCfg.provider} onFinish=${ephemeralTeardownCfg.onFinish} ttl=${ephemeralTeardownCfg.ttlMin}m`);
|
|
7690
|
+
console.log(`[ephemeral-teardown] armed: provider=${ephemeralTeardownCfg.provider} onFinish=${ephemeralTeardownCfg.onFinish} sleep=${ephemeralTeardownCfg.sleep} ttl=${ephemeralTeardownCfg.ttlMin}m`);
|
|
7661
7691
|
}
|
|
7662
7692
|
setTimeout(() => void sweepDiskGuardrails(), 30_000).unref?.();
|
|
7663
7693
|
// One sweep shortly after boot clears ghosts left by a previous run before any
|
|
@@ -8130,14 +8160,17 @@ function attachSessionListeners(record) {
|
|
|
8130
8160
|
if (event.type === "turn_start" || event.type === "message_boundary" || event.type === "message_end" || event.type === "turn_end") {
|
|
8131
8161
|
transcripts.persistTranscriptSnapshot(record);
|
|
8132
8162
|
}
|
|
8133
|
-
// A finalized assistant message may reference
|
|
8134
|
-
// (``)
|
|
8135
|
-
//
|
|
8136
|
-
//
|
|
8137
|
-
//
|
|
8138
|
-
//
|
|
8163
|
+
// A finalized assistant message may reference an image via markdown — a
|
|
8164
|
+
// remote URL (``) to fetch, or a file the agent just
|
|
8165
|
+
// produced in its workspace (``) to read. Resolve and
|
|
8166
|
+
// store either now, so the chat can render it and history keeps it after the
|
|
8167
|
+
// workspace is gone. Checked on both events: message_end is the precise
|
|
8168
|
+
// "this assistant message is done" signal most runtimes emit, but turn_end
|
|
8169
|
+
// is a safety net for one that only surfaces the final text there. Both
|
|
8170
|
+
// resolvers are internally deduped, so checking twice costs nothing.
|
|
8139
8171
|
if (event.type === "message_end" || event.type === "turn_end") {
|
|
8140
8172
|
transcripts.resolveInlineImages(record);
|
|
8173
|
+
transcripts.resolveWorkspaceRefs(record, harnessDirFor(record));
|
|
8141
8174
|
}
|
|
8142
8175
|
// Durably persist the throttled sidecars at the turn boundary so a crash
|
|
8143
8176
|
// loses at most the in-flight turn's UI detail, not the whole turn. A
|
|
@@ -9069,6 +9102,9 @@ async function sessionForModelQuery(runtimeId) {
|
|
|
9069
9102
|
modelQueryScratchPending.set(wanted, build);
|
|
9070
9103
|
return build;
|
|
9071
9104
|
}
|
|
9105
|
+
/** How long a session's models.list waits for its agent's real catalog before
|
|
9106
|
+
* answering with what is known (below the client's 15 s launch timeout). */
|
|
9107
|
+
const SESSION_MODELS_WARM_MS = 8_000;
|
|
9072
9108
|
// A fresh scratch answers its first models.list from the runtime's placeholder
|
|
9073
9109
|
// list (no live agent yet). Warm the real catalog in the background — for Claude
|
|
9074
9110
|
// Code this spins up the agent subprocess just far enough for supportedModels()
|
|
@@ -11741,10 +11777,9 @@ app.get("/api/session/:id/context", (req, res) => {
|
|
|
11741
11777
|
previewAvailable,
|
|
11742
11778
|
}));
|
|
11743
11779
|
});
|
|
11744
|
-
// `bivy notify
|
|
11745
|
-
//
|
|
11746
|
-
//
|
|
11747
|
-
// terminal has no chat to hold the card, so its notice is the push alone.
|
|
11780
|
+
// `bivy notify`: a push naming the session (never any text) when nobody has the
|
|
11781
|
+
// app open, or when it's urgent. At most one push a minute per session. It adds
|
|
11782
|
+
// nothing to the chat: what the agent has to say belongs in its reply there.
|
|
11748
11783
|
const lastNotifyPush = new Map();
|
|
11749
11784
|
app.post("/api/session/:id/notify", (req, res) => {
|
|
11750
11785
|
const record = openSessions.get(String(req.params.id));
|
|
@@ -11752,16 +11787,9 @@ app.post("/api/session/:id/notify", (req, res) => {
|
|
|
11752
11787
|
const sessionId = record?.id ?? run?.sessionId;
|
|
11753
11788
|
if (!sessionId)
|
|
11754
11789
|
return res.status(404).json({ error: "Session not found" });
|
|
11755
|
-
const
|
|
11756
|
-
const notice = { id: `notice-${randomBytes(8).toString("hex")}`, text, ...(req.body?.urgent === true ? { urgent: true } : {}) };
|
|
11757
|
-
if (!isAgentNotice(notice))
|
|
11758
|
-
return res.status(400).json({ error: `A notice needs text (up to ${MAX_NOTICE_TEXT} characters).` });
|
|
11759
|
-
if (record) {
|
|
11760
|
-
eventLog.appendNotice(record.id, { afterMessageCount: record.session.getMessages().length, notice });
|
|
11761
|
-
broadcast(stampSessionEvent({ type: "session.event", sessionId: record.id, event: { type: "notice", id: notice.id, notice } }));
|
|
11762
|
-
}
|
|
11790
|
+
const urgent = req.body?.urgent === true;
|
|
11763
11791
|
const watching = Boolean(record) && (clients.size > 0 || (relay?.clientCount ?? 0) > 0);
|
|
11764
|
-
const push = noticePush({ urgent
|
|
11792
|
+
const push = noticePush({ urgent, userWatching: watching, lastPushAt: lastNotifyPush.get(sessionId), now: Date.now(), pushConfigured: Boolean(sessionAdvertiseTarget) });
|
|
11765
11793
|
if (push === "sent") {
|
|
11766
11794
|
lastNotifyPush.set(sessionId, Date.now());
|
|
11767
11795
|
void sendNotificationHint({
|
|
@@ -11769,10 +11797,10 @@ app.post("/api/session/:id/notify", (req, res) => {
|
|
|
11769
11797
|
sessionId,
|
|
11770
11798
|
targetSessionId: sessionId,
|
|
11771
11799
|
title: sessionNotifyLabel(record, run?.name || "A terminal session"),
|
|
11772
|
-
body: "
|
|
11800
|
+
body: "Wants you to take a look — tap to open the session.",
|
|
11773
11801
|
});
|
|
11774
11802
|
}
|
|
11775
|
-
res.json({ ok: true,
|
|
11803
|
+
res.json({ ok: true, push, userWatching: watching });
|
|
11776
11804
|
});
|
|
11777
11805
|
// `bivy ask`: the question card and "needs your input" push, for any agent.
|
|
11778
11806
|
// Returns at once with an id; `/wait` blocks up to 240s per call (under common
|
|
@@ -11830,23 +11858,6 @@ app.post("/api/session/:id/title", (req, res) => {
|
|
|
11830
11858
|
sessionNamer.setSessionName(record, title);
|
|
11831
11859
|
res.json({ ok: true, title });
|
|
11832
11860
|
});
|
|
11833
|
-
// `bivy suggest "<task>"`: the agent proposes a task the user can start in one
|
|
11834
|
-
// tap, here, through this agent's sub-agents, or in a parallel session; `run`
|
|
11835
|
-
// is the one the agent recommends (see packages/web SuggestionCard).
|
|
11836
|
-
app.post("/api/session/:id/suggest", (req, res) => {
|
|
11837
|
-
const record = openSessions.get(String(req.params.id));
|
|
11838
|
-
if (!record)
|
|
11839
|
-
return res.status(404).json({ error: "Session not found" });
|
|
11840
|
-
const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
|
|
11841
|
-
const title = typeof req.body?.title === "string" && req.body.title.trim() ? req.body.title.trim() : undefined;
|
|
11842
|
-
const run = req.body?.run || undefined;
|
|
11843
|
-
const suggestion = { id: `suggestion-${randomBytes(8).toString("hex")}`, text, ...(title ? { title } : {}), ...(run ? { run } : {}) };
|
|
11844
|
-
if (!isTaskSuggestion(suggestion))
|
|
11845
|
-
return res.status(400).json({ error: `A suggestion needs text (up to ${MAX_SUGGESTION_TEXT} characters), an optional title (up to ${MAX_SUGGESTION_TITLE}) and an optional run (${SUGGESTION_RUNS.join(", ")}).` });
|
|
11846
|
-
eventLog.appendSuggestion(record.id, { afterMessageCount: record.session.getMessages().length, suggestion });
|
|
11847
|
-
broadcast(stampSessionEvent({ type: "session.event", sessionId: record.id, event: { type: "suggestion", id: suggestion.id, suggestion } }));
|
|
11848
|
-
res.json({ ok: true, id: suggestion.id });
|
|
11849
|
-
});
|
|
11850
11861
|
// Explicit child Run API for first-party/user-directed workflows. These routes
|
|
11851
11862
|
// are intentionally not advertised to agents as tools; the service still
|
|
11852
11863
|
// authorizes every status lookup against this parent Session's provenance.
|