@bivy/bivy 0.20.9-staging.8 → 0.21.0-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +1 -2
  2. package/dist/agent-instructions.js +14 -4
  3. package/dist/apps/remote-preview.js +7 -0
  4. package/dist/apps/service.js +6 -1
  5. package/dist/apps/styles.css +56 -70
  6. package/dist/automation-reseal.js +30 -0
  7. package/dist/config-cli.js +2 -0
  8. package/dist/controllers/credential-commands.js +3 -27
  9. package/dist/credentials/api.js +2 -43
  10. package/dist/credentials/document.js +6 -14
  11. package/dist/credentials/store.js +5 -6
  12. package/dist/device-registry.js +27 -0
  13. package/dist/harness/checkpoint.js +0 -0
  14. package/dist/node-config.js +4 -3
  15. package/dist/runtime/model-catalog.js +16 -0
  16. package/dist/runtime/tool-call-map.js +1 -1
  17. package/dist/runtime/tool-trace.js +1 -1
  18. package/dist/server.js +136 -244
  19. package/dist/session/event-log.js +136 -29
  20. package/dist/session/inline-image-fetch.js +24 -33
  21. package/dist/session/message-components.js +429 -0
  22. package/dist/session/run-terminal.js +9 -7
  23. package/dist/session/transcript-persistence.js +135 -10
  24. package/dist/work-instructions.js +50 -0
  25. package/package.json +4 -4
  26. package/web/assets/AutomationsView-DA9_ilGJ.js +51 -0
  27. package/web/assets/ChangesCard-D1_285PU.js +4 -0
  28. package/web/assets/LibraryView-Cnvd9lkW.js +2 -0
  29. package/web/assets/{ProviderConnect-DId9XOvG.js → ProviderConnect-B_ZjrMZY.js} +1 -1
  30. package/web/assets/SessionList-DUdqO_nL.js +1 -0
  31. package/web/assets/Settings--6U-Td7F.js +10 -0
  32. package/web/assets/{Sheet-Q2ePGQye.js → Sheet-DCRQrcPM.js} +1 -1
  33. package/web/assets/{StatusDot-dpbztK53.js → Spinner-CPdRfDcT.js} +1 -1
  34. package/web/assets/{Terminal-DSZ8YBgi.js → Terminal-DpsRLIV7.js} +1 -1
  35. package/web/assets/{VoiceSettings-CGxPed9_.js → VoiceSettings-B4ftQ8Xr.js} +1 -1
  36. package/web/assets/{abnfDiagram-VCTEODGH-P-N-ZNBN.js → abnfDiagram-VCTEODGH-Cdw5yDXk.js} +1 -1
  37. package/web/assets/arc-BNLueWOY.js +1 -0
  38. package/web/assets/architecture-7GRP2DOG-CfYziiZA.js +1 -0
  39. package/web/assets/{architectureDiagram-5GKGNRK7-byCBwDFy.js → architectureDiagram-5GKGNRK7-CX_Knw6D.js} +1 -1
  40. package/web/assets/{blockDiagram-I7D4REHJ-8Ix3AyxA.js → blockDiagram-I7D4REHJ-CYlwdU3X.js} +1 -1
  41. package/web/assets/{c4Diagram-7LVT6UL2-DsaczzUF.js → c4Diagram-7LVT6UL2-PA3wEbxG.js} +1 -1
  42. package/web/assets/channel-DjmBScdY.js +1 -0
  43. package/web/assets/{chunk-4HAMMTFA-CNU1npt1.js → chunk-4HAMMTFA-DkNFPUyu.js} +1 -1
  44. package/web/assets/chunk-75Z2AOVW-Cb23_YVq.js +2 -0
  45. package/web/assets/{chunk-DU6HZSFF-kfSsBAl-.js → chunk-DU6HZSFF-CqaTLnF7.js} +15 -15
  46. package/web/assets/chunk-F27PBJKO-D0O8iotE.js +1 -0
  47. package/web/assets/{chunk-GMAD6QVW-DN9x2U-t.js → chunk-GMAD6QVW-Db7P-bVL.js} +1 -1
  48. package/web/assets/{chunk-GVQU2GXP-kNfCpzk-.js → chunk-GVQU2GXP-C6nQDHF7.js} +1 -1
  49. package/web/assets/{chunk-IMKFNOWR-RQCVneQm.js → chunk-IMKFNOWR-Byj0E7lr.js} +1 -1
  50. package/web/assets/{chunk-L3NEJ4N5-DCUakvZ4.js → chunk-L3NEJ4N5-BpH7QJFo.js} +1 -1
  51. package/web/assets/{chunk-OSK3NFVY-DwoeBdfs.js → chunk-OSK3NFVY-CzeAEhZO.js} +2 -2
  52. package/web/assets/{chunk-P2QGCYS3-Dr0BtBMh.js → chunk-P2QGCYS3-uL1IB5fR.js} +1 -1
  53. package/web/assets/{chunk-POPQ4Y6H-9OxfP46b.js → chunk-POPQ4Y6H-CXlNmGZG.js} +1 -1
  54. package/web/assets/{chunk-PWAF6VOD-CvD7tBZF.js → chunk-PWAF6VOD-BkHQH-na.js} +1 -1
  55. package/web/assets/{chunk-SHT3W25Y-jYLIsjaC.js → chunk-SHT3W25Y-1ZIHyWA5.js} +1 -1
  56. package/web/assets/{chunk-SVP7TREG-D2yvpT1y.js → chunk-SVP7TREG-DOkiRFbX.js} +1 -1
  57. package/web/assets/{chunk-TICWLB2K-V1PKTqXh.js → chunk-TICWLB2K-B3zv0oLo.js} +1 -1
  58. package/web/assets/chunk-XXDRQBXY-CjF14Z7v.js +1 -0
  59. package/web/assets/classDiagram-ZZMXUADV-0QtonV64.js +1 -0
  60. package/web/assets/classDiagram-v2-VYDZK3BY-0QtonV64.js +1 -0
  61. package/web/assets/colors-Cg5Vh3br.js +1 -0
  62. package/web/assets/{cose-bilkent-JH36ORCC-BIqBF2fY.js → cose-bilkent-JH36ORCC-CEUD3V3W.js} +1 -1
  63. package/web/assets/{cynefin-OW5HDTMX-S3-HDgGF.js → cynefin-OW5HDTMX-mKyDjyM_.js} +1 -1
  64. package/web/assets/cynefinDiagram-5FMLGOSQ-Cwe3TvQ5.js +62 -0
  65. package/web/assets/{dagre-GXQ25YYZ-DYFiDtUI.js → dagre-GXQ25YYZ-BK_vzPXJ.js} +1 -1
  66. package/web/assets/{defaultLocale-BFoDCU3G.js → defaultLocale-CNPGWpMu.js} +1 -1
  67. package/web/assets/{diagram-S7CK7UJ4-B98M2jJY.js → diagram-S7CK7UJ4-CVqyMnLL.js} +1 -1
  68. package/web/assets/diagram-UQ7AKVKN-XzAb6plC.js +41 -0
  69. package/web/assets/{diagram-VSXAHHWV-ds4j8snX.js → diagram-VSXAHHWV-Ba5Y2kBC.js} +1 -1
  70. package/web/assets/diagram-VX7I27RA-DNPLCmCh.js +24 -0
  71. package/web/assets/diagram-Z3DM3KII-DrAi87vL.js +24 -0
  72. package/web/assets/dist-kYfyPZP9.js +1 -0
  73. package/web/assets/{ebnfDiagram-PWID7BFC-BnpYqLy9.js → ebnfDiagram-PWID7BFC-DYYlJEgD.js} +1 -1
  74. package/web/assets/embed-BXV41wg7.js +201 -0
  75. package/web/assets/{erDiagram-RLTQ6QDP-BB7HZnkw.js → erDiagram-RLTQ6QDP-B_JY_DQk.js} +1 -1
  76. package/web/assets/eventmodeling-NTZA5JFV-BtSu2if9.js +1 -0
  77. package/web/assets/flowDiagram-HODETNUW-CGzQbPxy.js +1 -0
  78. package/web/assets/ganttDiagram-EL5Y4UJY-tyoaaBb0.js +292 -0
  79. package/web/assets/{gitGraph-4MIJSDKK-C4LumuQC.js → gitGraph-4MIJSDKK-C2bY1CGX.js} +1 -1
  80. package/web/assets/gitGraphDiagram-WWUBYQGX-DTlHPC5D.js +106 -0
  81. package/web/assets/{index-DqS1nQdi.js → index-BCWScGzL.js} +2 -2
  82. package/web/assets/index-BKSbod42.css +1 -0
  83. package/web/assets/{info-A6RAGUB7-D5ABcZgz.js → info-A6RAGUB7-sKkbqM_E.js} +1 -1
  84. package/web/assets/infoDiagram-27XIBGKW-Bvw2AcJr.js +2 -0
  85. package/web/assets/init-ZlXS9mIS.js +1 -0
  86. package/web/assets/{ishikawaDiagram-5VMMS53U-DrsbsHXm.js → ishikawaDiagram-5VMMS53U-Kza7MX-1.js} +1 -1
  87. package/web/assets/{journeyDiagram-3NMN7TZE-B7VhME2y.js → journeyDiagram-3NMN7TZE-DtMmNsof.js} +1 -1
  88. package/web/assets/{kanban-definition-UXKFOSKX-DH-NTkzb.js → kanban-definition-UXKFOSKX-DG-Uv-OI.js} +1 -1
  89. package/web/assets/linear-CeYXFQza.js +1 -0
  90. package/web/assets/{mermaid-parser.core-Dltug786.js → mermaid-parser.core-CVrhbt86.js} +3 -3
  91. package/web/assets/{mermaid.core--75UrgAP.js → mermaid.core-U3bnccth.js} +6 -6
  92. package/web/assets/{mindmap-definition-YA3MSWOX-D7pwHRXt.js → mindmap-definition-YA3MSWOX-B1liGoy5.js} +1 -1
  93. package/web/assets/mount-BSLnTLn1.js +36 -0
  94. package/web/assets/ordinal-BdBDtYTt.js +1 -0
  95. package/web/assets/{packet-AYTQ26CC-DpU7rkFQ.js → packet-AYTQ26CC-BmJrNR8t.js} +1 -1
  96. package/web/assets/{pegDiagram-XKGWAZYB-CyUHaaZw.js → pegDiagram-XKGWAZYB-dbZzxRiv.js} +1 -1
  97. package/web/assets/{pie-WAS4IAKB-k4r1AOoI.js → pie-WAS4IAKB-H_zYRQZS.js} +1 -1
  98. package/web/assets/{pieDiagram-E7YTZNPT-9vy-IUVb.js → pieDiagram-E7YTZNPT-D0Q3vzEh.js} +2 -2
  99. package/web/assets/{quadrantDiagram-AXDQQJYC-C-u0AR03.js → quadrantDiagram-AXDQQJYC-qNA-jyeM.js} +1 -1
  100. package/web/assets/{radar-RG4KPBEZ-DBX_1r-c.js → radar-RG4KPBEZ-DWs2LjJN.js} +1 -1
  101. package/web/assets/{railroad-74A4TZTK-C8ed06QJ.js → railroad-74A4TZTK-CHesrAHB.js} +1 -1
  102. package/web/assets/railroad-abnf-HS5TGJTU-9p3_CI3I.js +1 -0
  103. package/web/assets/railroad-ebnf-LZEXJU2U-CPUSpbug.js +1 -0
  104. package/web/assets/railroad-peg-WCYAUIDC-Dfhg5mGh.js +1 -0
  105. package/web/assets/{railroadDiagram-O6MQD6OU-Df9l6PLc.js → railroadDiagram-O6MQD6OU-C3-f4ErM.js} +1 -1
  106. package/web/assets/range-EplFIxc7.js +1 -0
  107. package/web/assets/{requirementDiagram-BXWQKSXE-DMbX29EP.js → requirementDiagram-BXWQKSXE-Duz3-frS.js} +1 -1
  108. package/web/assets/{sankeyDiagram-P5KCCOFB-DMdKyNDk.js → sankeyDiagram-P5KCCOFB-Dy2AjqHt.js} +1 -1
  109. package/web/assets/{sequenceDiagram-WJ2MYXX4-BZUKpd2D.js → sequenceDiagram-WJ2MYXX4-Bk4_fn7m.js} +3 -3
  110. package/web/assets/{settingsRoute-CxmTmim2.js → settingsRoute-CA4CTBbQ.js} +1 -1
  111. package/web/assets/src-CFHfg5gg.js +1 -0
  112. package/web/assets/stateDiagram-D77RDMKH-BDlN7MCW.js +1 -0
  113. package/web/assets/stateDiagram-v2-MP3YSRHH-BzK0RFf3.js +1 -0
  114. package/web/assets/step-BDqHAzds.js +1 -0
  115. package/web/assets/{swimlanes-42K2YHIH-DHDsLQTH.js → swimlanes-42K2YHIH-BKDYcVvH.js} +1 -1
  116. package/web/assets/swimlanesDiagram-VR7AAH4N-BCYnGbyq.js +8 -0
  117. package/web/assets/time-Dzw-YSCF.js +1 -0
  118. package/web/assets/{timeline-definition-24CTP7MA-MGKQWkpI.js → timeline-definition-24CTP7MA-65mGVsBy.js} +1 -1
  119. package/web/assets/{treeView-Q6P3EWNA-C_8iTJWi.js → treeView-Q6P3EWNA-WzobWotE.js} +1 -1
  120. package/web/assets/treemap-BfjIY-m6.js +1 -0
  121. package/web/assets/{treemap-WGGIJYW6-DX8MPwKV.js → treemap-WGGIJYW6-DywYNy-e.js} +1 -1
  122. package/web/assets/useStore-Cjpbp6X5.js +16 -0
  123. package/web/assets/{vennDiagram-4TSXK5OY-WSZd61f7.js → vennDiagram-4TSXK5OY-D7jOIO0P.js} +1 -1
  124. package/web/assets/{wardley-WFR3VGLG-Bi4EAVkj.js → wardley-WFR3VGLG-z6JfJlwZ.js} +1 -1
  125. package/web/assets/{wardleyDiagram-VM6X3IG4-L6mpluSU.js → wardleyDiagram-VM6X3IG4-DuwAEGCR.js} +1 -1
  126. package/web/assets/{xychartDiagram-S5SC5T6Z-CwBesblt.js → xychartDiagram-S5SC5T6Z-D2i-Q9Q1.js} +3 -3
  127. package/web/index.html +2 -2
  128. package/web/sw.js +1 -1
  129. package/dist/ephemeral-exec.js +0 -108
  130. package/dist/hosted-custody.js +0 -30
  131. package/web/assets/AutomationsView-DoDebW9z.js +0 -51
  132. package/web/assets/ChangesCard-Cm7TsFxw.js +0 -4
  133. package/web/assets/LibraryView-D_Za_-QQ.js +0 -2
  134. package/web/assets/Settings-DjfDtitx.js +0 -10
  135. package/web/assets/arc-Pry-7G1_.js +0 -1
  136. package/web/assets/architecture-7GRP2DOG-Bx9givzi.js +0 -1
  137. package/web/assets/array-BifhSqXX.js +0 -1
  138. package/web/assets/channel-CoFJqi63.js +0 -1
  139. package/web/assets/chunk-75Z2AOVW-2R8IkZ6K.js +0 -2
  140. package/web/assets/chunk-F27PBJKO-CnHKeJnk.js +0 -1
  141. package/web/assets/chunk-XXDRQBXY-DqAYeljN.js +0 -1
  142. package/web/assets/classDiagram-ZZMXUADV-CoCI6-mr.js +0 -1
  143. package/web/assets/classDiagram-v2-VYDZK3BY-CoCI6-mr.js +0 -1
  144. package/web/assets/cynefinDiagram-5FMLGOSQ-AMnKjokI.js +0 -62
  145. package/web/assets/diagram-UQ7AKVKN-B3rlK3KL.js +0 -41
  146. package/web/assets/diagram-VX7I27RA-B2SkgQlQ.js +0 -24
  147. package/web/assets/diagram-Z3DM3KII-6t1yNosN.js +0 -24
  148. package/web/assets/dist-v5Q1xZ2K.js +0 -1
  149. package/web/assets/eventmodeling-NTZA5JFV-Cz2DLFjE.js +0 -1
  150. package/web/assets/flowDiagram-HODETNUW-Dx9iPIiD.js +0 -1
  151. package/web/assets/ganttDiagram-EL5Y4UJY-CRGarlJz.js +0 -292
  152. package/web/assets/gitGraphDiagram-WWUBYQGX-BEFQqyEF.js +0 -106
  153. package/web/assets/index-kWXUrhG8.css +0 -1
  154. package/web/assets/infoDiagram-27XIBGKW-DM1N7dMj.js +0 -2
  155. package/web/assets/init-C-OQMol4.js +0 -1
  156. package/web/assets/line-CpnjiQNF.js +0 -1
  157. package/web/assets/linear-SzVmGSw4.js +0 -1
  158. package/web/assets/mount-B0yT9nH2.js +0 -31
  159. package/web/assets/ordinal-BDEzSJ7C.js +0 -1
  160. package/web/assets/path-fybaL0A-.js +0 -1
  161. package/web/assets/railroad-abnf-HS5TGJTU-CH3K-Q4R.js +0 -1
  162. package/web/assets/railroad-ebnf-LZEXJU2U-By7WQSrj.js +0 -1
  163. package/web/assets/railroad-peg-WCYAUIDC-J5fbvVLI.js +0 -1
  164. package/web/assets/src-B6xuSHsQ.js +0 -1
  165. package/web/assets/stateDiagram-D77RDMKH-A5Z3JkUu.js +0 -1
  166. package/web/assets/stateDiagram-v2-MP3YSRHH-CbBdq37k.js +0 -1
  167. package/web/assets/swimlanesDiagram-VR7AAH4N-uxsVNpHd.js +0 -8
  168. package/web/assets/useStore-DnnAeOPe.js +0 -29
package/README.md CHANGED
@@ -228,8 +228,7 @@ bivy secrets ref github.repo-token op://Bivy/GitHub/repo-token
228
228
 
229
229
  **Not every CLI login syncs.** Native agent logins may still be per-machine;
230
230
  GitHub App private-key sync is separately opt-in. If you lose every node and
231
- device able to unwrap a vault, you must sign in to providers again. Explicit
232
- hosted-provisioning custody grants are separate from ordinary encrypted sync.
231
+ device able to unwrap a vault, you must sign in to providers again.
233
232
 
234
233
  [Credential sync and runtime coverage →](docs/credential-sync.md) ·
235
234
  [Credentials guide →](docs/credentials-guide.md) ·
@@ -30,10 +30,20 @@ export const MAX_AGENT_INSTRUCTIONS_BYTES = 16 * 1024;
30
30
  export const BIVY_AGENT_NOTE = [
31
31
  "You are running inside Bivy. The user follows this session in a chat app (web or phone): they cannot see your " +
32
32
  "terminal or files you only write to disk. $BIVY_SESSION_ID identifies this session; the `bivy` commands below use it.",
33
- "- Show a LOCAL file or image (report, screenshot, chart, a file they asked for): run " +
34
- '`bivy attach <path> [--caption "short note"]`, or call the `attach_to_chat` tool if you have it. Images render ' +
35
- "inline, other files as downloads; the path must be inside the session workspace. Markdown image syntax like " +
36
- "![](path) does not render a local path. A REMOTE `https://` image in markdown does render.",
33
+ "- Show an IMAGE (a screenshot, or a picture you rendered to a file) inside your reply: write it as markdown, " +
34
+ "`![short caption](relative/path.png)`, where you want it to appear. The path must be inside the session " +
35
+ "workspace; a remote `https://` image works too. No tool call needed.",
36
+ "- Show a FILE (a report, a log, data they asked for) inside your reply: put " +
37
+ '`::view{src=relative/path.pdf caption="what it is"}` on its own line, where you want it. Same workspace-only ' +
38
+ "rule. A `.csv`/`.tsv` renders as a table; anything else arrives as a download.",
39
+ "- Show a NUMBER or a CHART inside your reply: fence a spec as ```bivy. A number is " +
40
+ '`{"type":"metric","value":"4 650 kWh","label":"per year"}`; a chart is a plain Vega-Lite spec with its data ' +
41
+ "inline, or pointing at a workspace data file with `\"data\":{\"url\":\"data/use.csv\"}` (.csv/.tsv/.json). " +
42
+ "A chart cannot fetch a remote URL.",
43
+ "- Send a file WITHOUT writing a reply (or from a script): run " +
44
+ '`bivy attach <path> [--caption "short note"]`, or call the `attach_to_chat` tool if you have it.',
45
+ "- Put a published app's preview INSIDE your reply: `::view{app=<app id>}` on its own line. It frames the app " +
46
+ "where you wrote it, and still opens full screen on demand.",
37
47
  "- Something with a UI: `bivy app publish <manifest.json>` (a web server's port, a static build, a terminal) or " +
38
48
  "`bivy app run -- <command>` (a desktop app) gives the user a live preview; `bivy app shot` screenshots it so you " +
39
49
  "can check your work; `bivy app present` tells the user a visible change is ready to look at, and " +
@@ -3,6 +3,7 @@
3
3
  import { WebSocket } from "ws";
4
4
  import { AppGateway, previewOriginTemplate } from "./gateway.js";
5
5
  import { PreviewStream } from "./preview-stream.js";
6
+ const RECENT_USE_MS = 2 * 60_000;
6
7
  /** Automatically discovered preview delivery. The app gateway receives ONLY
7
8
  * HTTP streams on a private in-process socket, never a publicly bound port. */
8
9
  export class RemotePreview {
@@ -16,6 +17,7 @@ export class RemotePreview {
16
17
  relayUrl;
17
18
  online = false;
18
19
  sockets = new Set();
20
+ lastConnectAt = -Infinity;
19
21
  constructor(registry, returnOrigins, signIn, captureNote, badge) {
20
22
  this.registry = registry;
21
23
  this.returnOrigins = returnOrigins;
@@ -24,6 +26,10 @@ export class RemotePreview {
24
26
  this.badge = badge;
25
27
  }
26
28
  get available() { return this.online && Boolean(this.gateway); }
29
+ /** Someone loaded part of a preview recently. Each request burst opens a
30
+ * stream; an open but silent live-reload socket from a forgotten tab doesn't
31
+ * count, so it can't keep a sleeping machine awake. */
32
+ inUse(now = Date.now()) { return now - this.lastConnectAt < RECENT_USE_MS; }
27
33
  ready(origin, relayUrl) {
28
34
  this.disconnect();
29
35
  if (!origin)
@@ -44,6 +50,7 @@ export class RemotePreview {
44
50
  if (!this.available || !this.relayUrl || !/^[a-f0-9]{64}$/.test(ticket) || this.sockets.size >= 64)
45
51
  return;
46
52
  const gateway = this.gateway;
53
+ this.lastConnectAt = Date.now();
47
54
  const ws = new WebSocket(this.relayUrl, { headers: { authorization: `Bearer ${ticket}` }, maxPayload: 64 * 1024, handshakeTimeout: 10_000, perMessageDeflate: false, followRedirects: false });
48
55
  this.sockets.add(ws);
49
56
  ws.on("error", () => ws.terminate());
@@ -814,8 +814,13 @@ export class AppService {
814
814
  return;
815
815
  const now = Date.now();
816
816
  server.restarts = server.restarts.filter((at) => now - at < RESTART_WINDOW);
817
- if (server.restarts.length >= MAX_RESTARTS)
817
+ // A crash loop stays down until the view is opened again, rather than
818
+ // retrying every time the window rolls over.
819
+ if (server.restarts.length >= MAX_RESTARTS) {
820
+ clearInterval(server.timer);
821
+ server.timer = undefined;
818
822
  return;
823
+ }
819
824
  server.restarts.push(now);
820
825
  server.termId = undefined;
821
826
  void this.ensureServer(current).catch(() => { });
@@ -639,6 +639,29 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
639
639
  .msg.assistant .mermaid-diagram:first-child { margin-top: 0; }
640
640
  .msg.assistant .mermaid-diagram:last-child { margin-bottom: 0; }
641
641
  .msg.assistant .mermaid-diagram svg { display: inline-block; max-width: none; height: auto; vertical-align: top; }
642
+ /* Mount point for a component the agent placed in its message (::view{…}, a
643
+ ```bivy fence). Layout only: what renders inside is an existing component
644
+ (.msg-attachment, .card), so this must not restyle its shell — it only gives
645
+ it the same vertical rhythm as a paragraph. Empty until the portal mounts,
646
+ and empty forever if a reference never resolves, so it carries no border or
647
+ background of its own that would leave a visible hole. */
648
+ .msg.assistant .md-component { margin: 0.75em 0; }
649
+ .msg.assistant .md-component:first-child { margin-top: 0; }
650
+ .msg.assistant .md-component:last-child { margin-bottom: 0; }
651
+ .msg.assistant .md-component:empty { margin: 0; }
652
+ /* A placed component is a block in the reading column, not a tile in a row: it
653
+ fills the message width so every kind lines up with every other one and with
654
+ the prose around it. The fixed chip width belongs to the composer's wrapping
655
+ attachment list, where several chips share a line. */
656
+ .msg.assistant .md-component > * { width: 100%; }
657
+ /* A single number the agent wants read at a glance. Inherits .card's shell;
658
+ only the value's type scale is its own. */
659
+ .metric-component { text-align: center; }
660
+ .metric-value { font-size: var(--text-3xl); line-height: var(--lh-3xl); font-weight: var(--weight-semibold); }
661
+ /* A table from a CSV or a spec reuses .markdown-table-wrap, so it reads exactly
662
+ like a table the agent wrote as markdown. The caption is the only addition. */
663
+ .table-component { margin: 0; }
664
+ .table-component figcaption { margin-top: var(--space-2); }
642
665
  /* Syntax highlighting for fenced code blocks (highlight.js token classes).
643
666
  Colors use light-dark() so one rule set tracks the app's light/dark scheme
644
667
  (color-scheme is set on :root). A GitHub-flavored palette that sits well on
@@ -1469,10 +1492,8 @@ button.sheet-action:last-child { border-bottom: 0; }
1469
1492
  }
1470
1493
  /* Password-manager-style provider credential vault. Saved items are the primary
1471
1494
  list; the provider catalog appears only inside Add. */
1472
- /* Wide settings panels that read as a shallow "vault": the credential vault and
1473
- the isolated-machine-profiles panel share this container + header layout. */
1474
- .credential-vault, .machine-profiles { gap: 14px; max-width: 680px; width: 100%; }
1475
- .credential-vault { overflow-wrap: anywhere; }
1495
+ /* The credential vault: a wide settings panel that reads as a shallow "vault". */
1496
+ .credential-vault { gap: 14px; max-width: 680px; width: 100%; overflow-wrap: anywhere; }
1476
1497
  .vault-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
1477
1498
  .vault-title-row > div { min-width: 0; }
1478
1499
  /* The header action (e.g. "+ Add profile") never shrinks or wraps — the title
@@ -1496,7 +1517,6 @@ button.sheet-action:last-child { border-bottom: 0; }
1496
1517
  .vault-detail-grid { display: grid; grid-template-columns: minmax(110px, auto) 1fr; gap: 10px 18px; padding: 14px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-2); }
1497
1518
  .vault-detail-grid strong { min-width: 0; overflow-wrap: anywhere; }
1498
1519
  .vault-detail-grid code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1499
- .vault-custody-note { padding: 12px 14px; border-left: 3px solid var(--accent); border-radius: var(--radius-sm) var(--radius-md) var(--radius-md) var(--radius-sm); background: var(--surface-2); line-height: 1.5; }
1500
1520
  .vault-advanced { padding: 10px 0; border-top: 1px solid var(--line); }
1501
1521
  .vault-advanced summary { cursor: pointer; color: var(--ink); font-weight: 600; }
1502
1522
  .vault-advanced[open] summary { margin-bottom: 10px; }
@@ -1857,21 +1877,8 @@ button.template-card.is-featured .template-card-icon {
1857
1877
  }
1858
1878
  .automation-row .row-menu-btn { opacity: 0.72; }
1859
1879
  .automation-row:hover .row-menu-btn { opacity: 1; }
1860
- .run-row { align-items: center; }
1861
- .run-row-open {
1862
- appearance: none; border: 0; background: transparent; color: inherit; font: inherit;
1863
- text-align: left; padding: 0; cursor: pointer;
1864
- }
1865
- .run-row-open:disabled { cursor: default; }
1866
- .run-row-open:not(:disabled):hover .automation-row-title strong { color: var(--accent); }
1867
- .run-row-chevron {
1868
- display: grid; place-items: center; flex: none; width: 40px; min-height: 40px;
1869
- border: 0; border-radius: var(--radius-md); padding: 0; background: transparent;
1870
- color: var(--muted); font: inherit; font-size: 24px; line-height: 1; cursor: pointer;
1871
- }
1872
- .run-row-chevron:hover { color: var(--ink); background: var(--surface-2); }
1873
1880
  .runs-setup {
1874
- margin-top: 8px; border: 1px solid var(--line); border-radius: var(--radius-md);
1881
+ margin-top: var(--space-6); border: 1px solid var(--line); border-radius: var(--radius-md);
1875
1882
  background: var(--surface); overflow: hidden;
1876
1883
  }
1877
1884
  .runs-setup > summary {
@@ -1886,16 +1893,13 @@ button.template-card.is-featured .template-card-icon {
1886
1893
  .runs-setup[open] .runs-setup-chevron { transform: rotate(90deg); }
1887
1894
  .runs-setup-body { display: flex; flex-direction: column; gap: 12px; padding: 14px; border-top: 1px solid var(--line); }
1888
1895
  .runs-setup-body .picker-search { max-width: 100%; box-sizing: border-box; }
1889
- .runs-overview { margin-top: 18px; }
1890
- .runs-overview .autom-section-head > div { min-width: 0; }
1891
- .runs-overview .autom-section-head .settings-hint { margin: 3px 0 0; }
1892
- /* Keep a single refresh action compact; stretching it to half the sheet makes
1893
- the run controls look like a form instead of a feed toolbar. */
1894
- .runs-overview .autom-section-actions { justify-content: flex-end; }
1895
- .runs-overview .autom-section-actions .btn { min-width: 96px; }
1896
- .incoming-work { margin-top: 22px; }
1897
- .incoming-work > .autom-section-label { margin-bottom: 8px; }
1898
- .incoming-work > .settings-form { gap: 14px; }
1896
+ /* Runs feed: grouped like the session list (attention, in progress, by day). */
1897
+ .run-history { margin-top: var(--space-2); }
1898
+ .run-history .session-group-label { margin: 0; padding-inline: 0; }
1899
+ .run-history > .settings-form { gap: var(--space-2); padding: 0 0 var(--space-3); border-bottom: 1px solid var(--line); }
1900
+ .run-history > .settings-form:empty { display: none; }
1901
+ .run-history-failure { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1902
+ .automation-history-row:disabled { cursor: default; }
1899
1903
 
1900
1904
  /* A runner is a first-class requirement, not a tiny select hidden under the
1901
1905
  prompt. Pairing/key readiness is visible before Save can become disabled. */
@@ -1911,6 +1915,7 @@ button.template-card.is-featured .template-card-icon {
1911
1915
  .autom-runner-select-copy span { color: var(--muted); font-size: 12px; line-height: 1.35; }
1912
1916
  .autom-runner-select-row .autom-inline-select { flex: none; width: min(42%, 190px); }
1913
1917
  .autom-runner-help {
1918
+ display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2);
1914
1919
  padding: 10px 12px; border-top: 1px solid var(--line);
1915
1920
  color: var(--ink); background: color-mix(in srgb, var(--warn) 9%, var(--surface));
1916
1921
  font-size: 12px; line-height: 1.45;
@@ -1947,14 +1952,6 @@ button.template-card.is-featured .template-card-icon {
1947
1952
  .run-timeline li span { color: var(--muted); font-size: 12px; }
1948
1953
  .run-timeline p { grid-column: 1 / -1; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.4; }
1949
1954
  .run-timeline code { grid-column: 1 / -1; overflow-wrap: anywhere; font-size: 11px; }
1950
- .run-history-filters {
1951
- display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0;
1952
- }
1953
- .run-history-filters .btn { white-space: nowrap; }
1954
- @media (max-width: 479px) {
1955
- .run-history-filters { flex-wrap: nowrap; overflow-x: auto; margin-inline: -4px; padding-inline: 4px; scrollbar-width: none; }
1956
- .run-history-filters::-webkit-scrollbar { display: none; }
1957
- }
1958
1955
  .run-details .run-sheet-failure { margin-top: 14px; border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--line)); border-radius: var(--radius-md); }
1959
1956
  .run-details-receipt { margin-top: 18px !important; }
1960
1957
  .run-details-state {
@@ -2139,7 +2136,7 @@ button.template-card.is-featured .template-card-icon {
2139
2136
  .automation-detail-head .icon { flex-shrink: 0; }
2140
2137
  .automation-detail-head svg { width: var(--space-5); height: var(--space-5); }
2141
2138
  .automations-view.is-history .automations-view-body { flex: 1; padding: var(--space-5) var(--space-4); }
2142
- .automation-history-view, .automation-history-view .runs-overview { margin-top: 0; }
2139
+ .automation-history-view { margin-top: 0; }
2143
2140
  .automation-history-row {
2144
2141
  display: flex; align-items: center; gap: var(--space-3); width: 100%;
2145
2142
  padding: var(--space-4) 0; border: 0; border-bottom: 1px solid var(--line);
@@ -2499,16 +2496,6 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
2499
2496
  .automation-row-actions { width: auto; justify-content: flex-end; border-top: 0; padding-top: 0; }
2500
2497
  .automation-row-actions .btn { min-height: 40px; }
2501
2498
  .automation-row .row-menu-btn { min-width: 40px; min-height: 40px; opacity: 1; }
2502
- .run-row { align-items: flex-start; flex-wrap: wrap; }
2503
- .run-row .automation-row-main { flex-basis: 100%; }
2504
- .run-row .automation-row-actions { width: 100%; padding-top: 6px; }
2505
- .run-row-chevron { margin-left: auto; }
2506
- .runs-overview .autom-section-head { align-items: stretch; flex-direction: column; }
2507
- .runs-overview .autom-section-actions { display: grid; grid-template-columns: 1fr 1fr; width: 100%; }
2508
- /* Keep two actions balanced when needed, but don't make a lone Refresh button
2509
- occupy half the screen. */
2510
- .runs-overview .autom-section-actions:has(> :only-child) { display: flex; justify-content: flex-start; }
2511
- .runs-overview .autom-section-actions .btn { min-width: 96px; width: auto; }
2512
2499
  .queue-head { align-items: flex-start; gap: 10px; }
2513
2500
  .queue-head-actions { justify-content: flex-end; gap: 8px; min-width: 0; text-align: right; }
2514
2501
  .queue-head-actions .btn.link { overflow-wrap: anywhere; }
@@ -2609,15 +2596,6 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
2609
2596
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
2610
2597
  }
2611
2598
  .connect-option:hover { border-color: var(--line-strong); box-shadow: var(--shadow-md); }
2612
- .connect-option-head { display: flex; align-items: flex-start; gap: 12px; }
2613
- .connect-option-badge {
2614
- flex: none; width: 36px; height: 36px; display: grid; place-items: center;
2615
- border-radius: var(--radius-md); color: var(--accent);
2616
- background: color-mix(in srgb, var(--accent) 12%, transparent);
2617
- }
2618
- .connect-option-copy { min-width: 0; }
2619
- .connect-option-copy h3 { margin: 1px 0 3px; font-size: 15px; }
2620
- .connect-option-copy p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
2621
2599
 
2622
2600
  .connect-command {
2623
2601
  display: flex; align-items: center; gap: 8px; padding: 4px 4px 4px 12px;
@@ -2637,7 +2615,6 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
2637
2615
  .connect-option-links { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
2638
2616
  .connect-option-link { align-self: flex-start; color: var(--accent); font-size: 12.5px; font-weight: 600; text-decoration: none; }
2639
2617
  .connect-option-link:hover { text-decoration: underline; }
2640
- .connect-option-cta { align-self: flex-start; }
2641
2618
  /* A one-time "next step" card above the composer (automations, notifications). */
2642
2619
  .next-step { box-sizing: border-box; width: min(100% - 2 * var(--space-3), 800px - 2 * var(--space-3)); margin: 0 auto var(--space-3); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
2643
2620
  .next-step p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-sm); }
@@ -3608,21 +3585,13 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3608
3585
 
3609
3586
  /* Compact, thumb-friendly automation list: the row is the editor link. */
3610
3587
  .automation-list { gap: var(--space-2); border: 0; }
3611
- .automation-row:not(.run-row) {
3588
+ .automation-row {
3612
3589
  min-height: 0; padding: var(--space-3); border: 1px solid var(--line);
3613
3590
  border-radius: var(--radius-lg); background: var(--surface-2);
3614
3591
  grid-template-columns: 36px minmax(0, 1fr) auto; gap: var(--space-3);
3615
3592
  align-items: center;
3616
3593
  }
3617
- .run-row {
3618
- display: flex; flex-wrap: wrap; align-items: flex-start;
3619
- gap: var(--space-3); padding: var(--space-4);
3620
- }
3621
- .run-row-open { flex: 1 1 100%; min-width: 0; }
3622
- .run-row .automation-row-main { width: 100%; }
3623
- .run-row .automation-row-actions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
3624
- .run-row .run-row-chevron { margin-left: auto; }
3625
- .automation-row:not(.run-row):hover { background: var(--surface-3); }
3594
+ .automation-row:hover { background: var(--surface-3); }
3626
3595
  .automation-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
3627
3596
  .automation-row-icon { width: 36px; height: 36px; }
3628
3597
  .automation-row-icon svg { width: 18px; height: 18px; }
@@ -3649,8 +3618,6 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3649
3618
  .automation-history-view .autom-section-head { margin-bottom: var(--space-3); }
3650
3619
  .automation-history-view .automation-list { gap: var(--space-2); }
3651
3620
  .automation-history-view .automation-row { padding: var(--space-3); }
3652
- .automation-history-view .run-history-filters { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: var(--space-1); }
3653
- .automation-history-view .run-history-filters::-webkit-scrollbar { display: none; }
3654
3621
  .automations-view.is-history .automations-view-head-actions > .btn.primary { display: none; }
3655
3622
  .automation-history-view .autom-section-head { align-items: flex-start; gap: var(--space-3); }
3656
3623
  .automation-history-view .autom-section-head > strong { min-width: 0; line-height: var(--lh-lg); }
@@ -3666,3 +3633,22 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3666
3633
  font-family: var(--font-serif);
3667
3634
  font-weight: var(--weight-medium);
3668
3635
  }
3636
+ /* A Vega-Lite chart an agent placed in a message. Vega owns the SVG inside
3637
+ .chart-host, so nothing here styles its internals — colors come from the
3638
+ tokens read in vega.ts, which is how the chart tracks light/dark. */
3639
+ .chart-component { margin: 0; }
3640
+ .chart-component .chart-host {
3641
+ width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; padding: 10px;
3642
+ border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface);
3643
+ }
3644
+ .chart-component figcaption { margin-top: var(--space-2); }
3645
+ /* An app preview framed inside a message. Same bordered surface as a chart or
3646
+ a mermaid diagram — all three are a figure in the prose, and should not read
3647
+ as three different things. */
3648
+ .inline-app { margin: 0; }
3649
+ .inline-app-frame {
3650
+ display: block; width: 100%; height: 420px; border: 1px solid var(--line);
3651
+ border-radius: var(--radius-sm); background: var(--surface);
3652
+ }
3653
+ .inline-app figcaption { display: flex; gap: var(--space-2); align-items: baseline; justify-content: space-between; margin-top: var(--space-2); }
3654
+ .inline-app-open { flex: none; }
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ import { open, seal } from "./e2e.js";
4
+ export async function resealAutomationTemplates(opts) {
5
+ const prefix = `bivy-room-v1:${opts.nodeId}:`;
6
+ const result = { resealed: 0, unreadable: [] };
7
+ for (const template of opts.templates) {
8
+ if (!template.templateCiphertext.startsWith(prefix))
9
+ continue;
10
+ const payload = template.templateCiphertext.slice(prefix.length);
11
+ if (tryOpen(opts.current, payload) !== undefined)
12
+ continue;
13
+ const plaintext = opts.retired.map((key) => tryOpen(key, payload)).find((text) => text !== undefined);
14
+ if (plaintext === undefined) {
15
+ result.unreadable.push(template.id);
16
+ continue;
17
+ }
18
+ await opts.save(template.id, `${prefix}${seal(opts.current, plaintext)}`);
19
+ result.resealed += 1;
20
+ }
21
+ return result;
22
+ }
23
+ function tryOpen(key, payload) {
24
+ try {
25
+ return open(key, payload);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
@@ -112,6 +112,7 @@ const ENV_FOR_KEY = {
112
112
  "sessions.previewBadge": "BIVY_PREVIEW_BADGE",
113
113
  "automation.checks": "BIVY_AUTOMATION_CHECKS",
114
114
  "automation.checkTimeoutMinutes": "BIVY_AUTOMATION_CHECK_TIMEOUT_MS",
115
+ "automation.slackPrompts": "BIVY_SLACK_PROMPTS",
115
116
  };
116
117
  const BUILTIN_VALUES = {
117
118
  "node.port": 4317,
@@ -128,6 +129,7 @@ const BUILTIN_VALUES = {
128
129
  "sessions.previewBadge": true,
129
130
  "automation.checks": ["test", "lint", "typecheck"],
130
131
  "automation.checkTimeoutMinutes": 10,
132
+ "automation.slackPrompts": false,
131
133
  };
132
134
  function baseEffectiveValue(config, key) {
133
135
  const configured = getConfigValue(config, key);
@@ -10,7 +10,7 @@
10
10
  // provider-list helpers) as composition deps.
11
11
  import { testProviderCredential } from "../runtime/credentials.js";
12
12
  import { NativeAuthPreviewService } from "../runtime/native-auth-preview.js";
13
- import { exportAccountApiKeys, exportAccountOAuthCredentials, importAccountOAuthCredentials, exportRecordTombstones, listCredentialRecords, removeProviderCredential, setProviderApiKeyLabeled, setProviderReferenceLabeled, setCredentialSync, setCredentialUnattended, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, } from "../credentials/api.js";
13
+ import { exportAccountApiKeys, exportAccountOAuthCredentials, importAccountOAuthCredentials, exportRecordTombstones, listCredentialRecords, removeProviderCredential, setProviderApiKeyLabeled, setProviderReferenceLabeled, setCredentialSync, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, } from "../credentials/api.js";
14
14
  export function createCredentialCommands(deps) {
15
15
  const { credsDir } = deps;
16
16
  const nativeAuth = new NativeAuthPreviewService(credsDir);
@@ -95,8 +95,7 @@ export function createCredentialCommands(deps) {
95
95
  async "credential.remove"(msg, ctx) {
96
96
  try {
97
97
  await removeProviderCredential(credsDir, String(msg.provider ?? ""), String(msg.label ?? ""));
98
- // Deleting a granted credential must also remove its escrowed cloud copy.
99
- await deps.pushModelAuthToControlPlane({ allowEmptyHostedEscrow: true });
98
+ await deps.pushModelAuthToControlPlane();
100
99
  await deps.refreshSessionAfterAuth();
101
100
  deps.sendEvent({ type: "credentials.records", records: await listCredentialRecords(credsDir) });
102
101
  deps.broadcast({ type: "providers.list", providers: await deps.listProvidersUnified() });
@@ -112,8 +111,7 @@ export function createCredentialCommands(deps) {
112
111
  try {
113
112
  const sync = msg.sync === "node" ? "node" : "account";
114
113
  await setCredentialSync(credsDir, String(msg.provider ?? ""), String(msg.label ?? ""), sync);
115
- // Demoting to machine-only revokes any custody grant; let it clear the escrow.
116
- await deps.pushModelAuthToControlPlane({ allowEmptyHostedEscrow: sync === "node" });
114
+ await deps.pushModelAuthToControlPlane();
117
115
  deps.sendEvent({ type: "credentials.records", records: await listCredentialRecords(credsDir) });
118
116
  ctx.reply({ type: "credential.sync.set.ok", requestId: msg.requestId });
119
117
  }
@@ -123,28 +121,6 @@ export function createCredentialCommands(deps) {
123
121
  ctx.reply({ type: "credential.sync.set.error", requestId: msg.requestId, error: message });
124
122
  }
125
123
  },
126
- async "credential.unattended.set"(msg, ctx) {
127
- const provider = String(msg.provider ?? "");
128
- const label = String(msg.label ?? "");
129
- const previous = (await listCredentialRecords(credsDir)).find((record) => record.provider === provider.trim().toLowerCase() && record.label === label)?.unattended === true;
130
- try {
131
- await setCredentialUnattended(credsDir, provider, label, msg.unattended === true);
132
- // Fail loud: the grant reply must reflect whether the encrypted cloud
133
- // copy actually reached the control plane. An explicit disable is the
134
- // one caller allowed to publish an empty escrow snapshot.
135
- await deps.pushModelAuthToControlPlane({ throwOnFailure: true, allowEmptyHostedEscrow: msg.unattended !== true });
136
- deps.sendEvent({ type: "credentials.records", records: await listCredentialRecords(credsDir) });
137
- ctx.reply({ type: "credential.unattended.set.ok", requestId: msg.requestId });
138
- }
139
- catch (error) {
140
- // The UI must never claim an encrypted Cloud copy exists when custody
141
- // publication failed. Restore the prior local grant before rejecting.
142
- await setCredentialUnattended(credsDir, provider, label, previous).catch(() => { });
143
- deps.sendEvent({ type: "credentials.records", records: await listCredentialRecords(credsDir) });
144
- const message = error instanceof Error ? error.message : String(error);
145
- ctx.reply({ type: "credential.unattended.set.error", requestId: msg.requestId, error: message });
146
- }
147
- },
148
124
  // "Test connection": a bounded, non-secret liveness probe for one credential
149
125
  // record (see credentials/api.ts testCredential). The reply carries only
150
126
  // ok/at/reason — the credential's own token never leaves this handler.
@@ -132,18 +132,6 @@ export async function importCredentialRecords(credsDir, records, deletedAt = {})
132
132
  }
133
133
  return createCredentialVault(credsDir).importRecords(list, tombstones);
134
134
  }
135
- /** Apply a hosted custody snapshot as an authoritative filtered set. */
136
- export async function reconcileHostedCredentialRecords(credsDir, records, previous) {
137
- const current = Object.values(records ?? {}).filter((record) => Boolean(record) && typeof record === "object" && typeof record.provider === "string" && typeof record.label === "string");
138
- const currentIds = new Set(current.map((record) => `${record.provider}\u0000${record.label}`));
139
- const store = createCredentialVault(credsDir);
140
- for (const prior of previous) {
141
- if (!currentIds.has(`${prior.provider}\u0000${prior.label}`))
142
- await store.deleteRecord(prior.provider, prior.label);
143
- }
144
- await importCredentialRecords(credsDir, records);
145
- return current.map((record) => ({ provider: record.provider, label: record.label }));
146
- }
147
135
  /** Export provider revocations for cross-node convergence. */
148
136
  export async function exportProviderAuthTombstones(credsDir) {
149
137
  return createCredentialVault(credsDir).exportTombstones();
@@ -225,7 +213,7 @@ export async function listCredentialRecords(credsDir) {
225
213
  const verification = await vault.readVerification(record.provider, record.label).catch(() => undefined);
226
214
  const verified = verification ? { lastVerifiedAt: verification.at, lastVerifiedOk: verification.ok } : {};
227
215
  if (source.kind === "reference") {
228
- return { provider: record.provider, label: record.label, kind: "reference", sync: record.sync, origin: record.origin, unattended: record.unattended === true, ref: source.ref, testable: false, ...verified };
216
+ return { provider: record.provider, label: record.label, kind: "reference", sync: record.sync, origin: record.origin, ref: source.ref, testable: false, ...verified };
229
217
  }
230
218
  const cred = source.cred;
231
219
  const summary = {
@@ -234,7 +222,6 @@ export async function listCredentialRecords(credsDir) {
234
222
  kind: cred.type,
235
223
  sync: record.sync,
236
224
  origin: record.origin,
237
- unattended: record.unattended === true,
238
225
  testable: isTestableProvider(record.provider),
239
226
  ...verified,
240
227
  };
@@ -253,7 +240,6 @@ async function labeledMeta(credsDir, provider, label) {
253
240
  return {
254
241
  sync: existing?.sync ?? defaultSyncFor("bivy"),
255
242
  origin: existing?.origin ?? "bivy",
256
- ...(existing?.unattended === true ? { unattended: true } : {}),
257
243
  };
258
244
  }
259
245
  /** Store an API key under a specific label (multi-account). */
@@ -404,33 +390,6 @@ export function getCredentialIngestPolicy(credsDir) {
404
390
  export function setCredentialIngestPolicy(credsDir, policy) {
405
391
  setIngestPolicyFile(defaultPresetsPath(credsDir), policy);
406
392
  }
407
- /** Explicitly grant/revoke use by separately escrowed unattended runners. */
408
- export async function setCredentialUnattended(credsDir, provider, label, unattended) {
409
- const id = provider.trim().toLowerCase();
410
- if (!id)
411
- throw new Error("Provider is required");
412
- const store = createCredentialVault(credsDir);
413
- const existing = await store.readRecord(id, label);
414
- if (!existing)
415
- throw new Error(`No credential for ${id}:${normalizeLabel(label)}`);
416
- if (existing.sync !== "account" && unattended)
417
- throw new Error("A machine-only credential cannot be granted to unattended runners");
418
- if (existing.source.kind === "reference" && unattended)
419
- throw new Error("Password-manager references cannot be resolved by unattended runners");
420
- await store.putRecord({ ...existing, unattended });
421
- }
422
- /** Credentials included in the explicit hosted custody snapshot. */
423
- export async function exportUnattendedRecords(credsDir) {
424
- const records = await createCredentialVault(credsDir).exportSyncableRecords();
425
- return Object.fromEntries(Object.entries(records).filter(([, record]) => record.unattended === true && record.source.kind === "stored"));
426
- }
427
- /** Monotonic logical revision for the filtered custody projection. */
428
- export async function unattendedCredentialRevision(credsDir) {
429
- const store = createCredentialVault(credsDir);
430
- const records = await store.listRecords();
431
- const deletedAt = await store.exportRecordTombstones();
432
- return Math.max(0, ...records.map((record) => Number(record.updatedAt) || 0), ...Object.values(deletedAt).map((stamp) => Number(stamp) || 0));
433
- }
434
393
  /** Set a labeled credential's sync tier — the per-credential opt-out toggle. */
435
394
  export async function setCredentialSync(credsDir, provider, label, sync) {
436
395
  const id = provider.trim().toLowerCase();
@@ -445,5 +404,5 @@ export async function setCredentialSync(credsDir, provider, label, sync) {
445
404
  if (existing.source.kind === "reference" && existing.source.backend === "command" && sync === "account") {
446
405
  throw new Error("A cmd:// reference is node-local (it runs a command on this machine) and cannot be synced.");
447
406
  }
448
- await store.putRecord({ ...existing, sync, ...(sync === "node" ? { unattended: false } : {}) });
407
+ await store.putRecord({ ...existing, sync });
449
408
  }
@@ -137,12 +137,11 @@ export function preferIncomingRecord(local, incoming) {
137
137
  const incomingOauth = oauthOf(incoming);
138
138
  if (!localOauth || !incomingOauth)
139
139
  return true;
140
- // Identical token content means this is a metadata-only change (an
141
- // unattended-runs grant/revoke, a sync-tier move). Token freshness cannot
142
- // order those, so the newer record write wins — without this, a grant made
143
- // on one machine never reaches its peers, and a peer's next hosted-escrow
144
- // push would read as "nothing granted". Equal stamps fall back to canonical
145
- // record content so both merge directions still converge.
140
+ // Identical token content means this is a metadata-only change (a sync-tier
141
+ // move). Token freshness cannot order those, so the newer record write wins —
142
+ // without this, a change made on one machine never reaches its peers. Equal
143
+ // stamps fall back to canonical record content so both merge directions still
144
+ // converge.
146
145
  if (canonicalCredential(localOauth) === canonicalCredential(incomingOauth)) {
147
146
  const localAt = Number(local.updatedAt) || 0;
148
147
  const incomingAt = Number(incoming.updatedAt) || 0;
@@ -269,14 +268,7 @@ export function mergeDocuments(local, incomingCredentials, incomingDeletedAt = {
269
268
  continue;
270
269
  if (!(key in credentials))
271
270
  imported += 1;
272
- // An incoming record with no opinion on unattended custody (undefined — the
273
- // v2 wire and plaintext ingest never carry the flag) must not strip an
274
- // explicit local grant/revoke: agent-side token refreshes flow through this
275
- // merge, and dropping the flag would make the node's next escrow push
276
- // silently revoke the encrypted cloud copy the user still relies on.
277
- credentials[key] = localRecord && incoming.unattended === undefined && localRecord.unattended !== undefined
278
- ? { ...incoming, unattended: localRecord.unattended }
279
- : incoming;
271
+ credentials[key] = incoming;
280
272
  const incomingUpdatedAt = Number(incoming.updatedAt);
281
273
  if (!Number.isFinite(deletedAt[key]) || incomingUpdatedAt > deletedAt[key])
282
274
  delete deletedAt[key];
@@ -555,11 +555,11 @@ export class BivyCredentialStore {
555
555
  // tombstone-newer-wins — see document.ts).
556
556
  const incoming = migrateToV3({ v: 2, providers: snapshot, deletedAt });
557
557
  // The v2 wire carries no record metadata, so migrateToV3 synthesizes
558
- // defaults (sync: "account", origin: "bivy", no unattended flag). Those
559
- // defaults must not clobber local intent — a machine-only sync tier or an
560
- // unattended-runs custody grant — when only the token content is newer
561
- // (e.g. an agent's own TUI refreshed the OAuth token set). Inherit the
562
- // existing record's metadata; the merge engine still decides freshness.
558
+ // defaults (sync: "account", origin: "bivy"). Those defaults must not
559
+ // clobber local intent — a machine-only sync tier — when only the token
560
+ // content is newer (e.g. an agent's own TUI refreshed the OAuth token
561
+ // set). Inherit the existing record's metadata; the merge engine still
562
+ // decides freshness.
563
563
  for (const [key, record] of Object.entries(incoming.credentials)) {
564
564
  const existing = document.credentials[key];
565
565
  if (!existing)
@@ -568,7 +568,6 @@ export class BivyCredentialStore {
568
568
  ...record,
569
569
  sync: existing.sync,
570
570
  origin: existing.origin,
571
- ...(existing.unattended !== undefined ? { unattended: existing.unattended } : {}),
572
571
  };
573
572
  }
574
573
  const result = mergeDocuments(document, incoming.credentials, incoming.deletedAt);
@@ -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();
Binary file