@bivy/bivy 0.20.9-staging.9 → 0.21.0

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 +2 -2
  26. package/web/assets/AutomationsView-HnUfeti_.js +51 -0
  27. package/web/assets/ChangesCard-Dnkguen5.js +4 -0
  28. package/web/assets/LibraryView-CSKKV8TI.js +2 -0
  29. package/web/assets/{ProviderConnect-BPwnaFGk.js → ProviderConnect-DNgxGmP9.js} +1 -1
  30. package/web/assets/SessionList-CYU9IGnd.js +1 -0
  31. package/web/assets/Settings-HQZtxiMV.js +10 -0
  32. package/web/assets/{Sheet-DOOJosQY.js → Sheet-DoPtOHBC.js} +1 -1
  33. package/web/assets/{StatusDot-dpbztK53.js → Spinner-CPdRfDcT.js} +1 -1
  34. package/web/assets/{Terminal-DVGXS6Yd.js → Terminal-DY6UB5U-.js} +1 -1
  35. package/web/assets/{VoiceSettings-DX-n0SFl.js → VoiceSettings-BoQ_Mhpm.js} +1 -1
  36. package/web/assets/{abnfDiagram-VCTEODGH-CnJ4astd.js → abnfDiagram-VCTEODGH-n-P11_L4.js} +1 -1
  37. package/web/assets/arc-BNLueWOY.js +1 -0
  38. package/web/assets/architecture-7GRP2DOG-DyG-wgaH.js +1 -0
  39. package/web/assets/{architectureDiagram-5GKGNRK7-B26-V_Xd.js → architectureDiagram-5GKGNRK7-C0b9IP5U.js} +1 -1
  40. package/web/assets/{blockDiagram-I7D4REHJ-DuoZkDHs.js → blockDiagram-I7D4REHJ-iVx1FQBD.js} +1 -1
  41. package/web/assets/{c4Diagram-7LVT6UL2-B81PZ-Vw.js → c4Diagram-7LVT6UL2-DIwEaAvJ.js} +1 -1
  42. package/web/assets/channel-HmCJmS4T.js +1 -0
  43. package/web/assets/{chunk-4HAMMTFA-DmSrqg9X.js → chunk-4HAMMTFA-DSsyukSz.js} +1 -1
  44. package/web/assets/chunk-75Z2AOVW-zwLYo-XL.js +2 -0
  45. package/web/assets/{chunk-DU6HZSFF-jMBIr1yK.js → chunk-DU6HZSFF-PGuLkqeP.js} +15 -15
  46. package/web/assets/chunk-F27PBJKO-CpkyeGC9.js +1 -0
  47. package/web/assets/{chunk-GMAD6QVW-C5YdBKWY.js → chunk-GMAD6QVW-CZSmK3Ms.js} +1 -1
  48. package/web/assets/{chunk-GVQU2GXP-DOOAmJLJ.js → chunk-GVQU2GXP-C2XMkY6v.js} +1 -1
  49. package/web/assets/{chunk-IMKFNOWR-CISXYXGE.js → chunk-IMKFNOWR-DAIVz3j5.js} +1 -1
  50. package/web/assets/{chunk-L3NEJ4N5-CvWtiEy-.js → chunk-L3NEJ4N5-C0geNTDK.js} +1 -1
  51. package/web/assets/{chunk-OSK3NFVY-yVAn5yky.js → chunk-OSK3NFVY-D-WN2fBr.js} +2 -2
  52. package/web/assets/{chunk-P2QGCYS3-D-VGoELb.js → chunk-P2QGCYS3-DYGhprKz.js} +1 -1
  53. package/web/assets/{chunk-POPQ4Y6H-0Mj4Bp-N.js → chunk-POPQ4Y6H-Dlsu2a2Q.js} +1 -1
  54. package/web/assets/{chunk-PWAF6VOD-Dvvdg6s1.js → chunk-PWAF6VOD-DK0QWYdc.js} +1 -1
  55. package/web/assets/{chunk-SHT3W25Y-DcllnbDq.js → chunk-SHT3W25Y-BpQHIdUi.js} +1 -1
  56. package/web/assets/{chunk-SVP7TREG-DnuOGeio.js → chunk-SVP7TREG-Cs5YWxpG.js} +1 -1
  57. package/web/assets/{chunk-TICWLB2K-Dr76NyiT.js → chunk-TICWLB2K-BRsWLPXA.js} +1 -1
  58. package/web/assets/chunk-XXDRQBXY-CjF14Z7v.js +1 -0
  59. package/web/assets/classDiagram-ZZMXUADV-BmdAoNdO.js +1 -0
  60. package/web/assets/classDiagram-v2-VYDZK3BY-BmdAoNdO.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-DoAt_4E6.js → cynefin-OW5HDTMX-DoHWnu_-.js} +1 -1
  64. package/web/assets/cynefinDiagram-5FMLGOSQ-CgPeHmV5.js +62 -0
  65. package/web/assets/{dagre-GXQ25YYZ-DIAXxVMd.js → dagre-GXQ25YYZ-CrIlgwM7.js} +1 -1
  66. package/web/assets/{defaultLocale-BFoDCU3G.js → defaultLocale-CNPGWpMu.js} +1 -1
  67. package/web/assets/{diagram-S7CK7UJ4-DA78JGp6.js → diagram-S7CK7UJ4-BM_l51zV.js} +1 -1
  68. package/web/assets/diagram-UQ7AKVKN-CfkxqgPI.js +41 -0
  69. package/web/assets/{diagram-VSXAHHWV-DCkvR_pJ.js → diagram-VSXAHHWV-0y5vCI9g.js} +1 -1
  70. package/web/assets/diagram-VX7I27RA-BMkMy7jn.js +24 -0
  71. package/web/assets/diagram-Z3DM3KII--0fxfDqG.js +24 -0
  72. package/web/assets/dist-kYfyPZP9.js +1 -0
  73. package/web/assets/{ebnfDiagram-PWID7BFC-CSCD0PAi.js → ebnfDiagram-PWID7BFC-6tPNzbJl.js} +1 -1
  74. package/web/assets/embed-BXV41wg7.js +201 -0
  75. package/web/assets/{erDiagram-RLTQ6QDP-M512qtMu.js → erDiagram-RLTQ6QDP-Bjqm_o3R.js} +1 -1
  76. package/web/assets/eventmodeling-NTZA5JFV-CoCZxN1n.js +1 -0
  77. package/web/assets/flowDiagram-HODETNUW-CznfWzEq.js +1 -0
  78. package/web/assets/ganttDiagram-EL5Y4UJY-92X7KOOI.js +292 -0
  79. package/web/assets/{gitGraph-4MIJSDKK-CwjwFpZb.js → gitGraph-4MIJSDKK-B5F-u7Vq.js} +1 -1
  80. package/web/assets/gitGraphDiagram-WWUBYQGX-Ct2hVG0b.js +106 -0
  81. package/web/assets/index-BKSbod42.css +1 -0
  82. package/web/assets/{index-CNs5i7tU.js → index-CnjJQpgZ.js} +2 -2
  83. package/web/assets/{info-A6RAGUB7-D6nXu5V2.js → info-A6RAGUB7-D3xmYWyC.js} +1 -1
  84. package/web/assets/infoDiagram-27XIBGKW-lzqZ7xaA.js +2 -0
  85. package/web/assets/init-ZlXS9mIS.js +1 -0
  86. package/web/assets/{ishikawaDiagram-5VMMS53U-4bsiyG2x.js → ishikawaDiagram-5VMMS53U-CsVZHXR0.js} +1 -1
  87. package/web/assets/{journeyDiagram-3NMN7TZE-o3U3L2rt.js → journeyDiagram-3NMN7TZE-DN9AdyVy.js} +1 -1
  88. package/web/assets/{kanban-definition-UXKFOSKX-CQOPj0NZ.js → kanban-definition-UXKFOSKX-gvkBaOFe.js} +1 -1
  89. package/web/assets/linear-CeYXFQza.js +1 -0
  90. package/web/assets/{mermaid-parser.core-Ba2-DGvb.js → mermaid-parser.core-CPwOm7-a.js} +3 -3
  91. package/web/assets/{mermaid.core-BxdMVbm9.js → mermaid.core-F09Wr40d.js} +6 -6
  92. package/web/assets/{mindmap-definition-YA3MSWOX-1Eu4-zLM.js → mindmap-definition-YA3MSWOX-Qzc2J9Pm.js} +1 -1
  93. package/web/assets/mount-Bvl-yslT.js +36 -0
  94. package/web/assets/ordinal-BdBDtYTt.js +1 -0
  95. package/web/assets/{packet-AYTQ26CC-BTZ7CbVd.js → packet-AYTQ26CC-Unxh5mP2.js} +1 -1
  96. package/web/assets/{pegDiagram-XKGWAZYB-DNdK3axo.js → pegDiagram-XKGWAZYB-BIqcRp3m.js} +1 -1
  97. package/web/assets/{pie-WAS4IAKB-DidjfqgW.js → pie-WAS4IAKB-BipHZKbc.js} +1 -1
  98. package/web/assets/{pieDiagram-E7YTZNPT-Dme68TUA.js → pieDiagram-E7YTZNPT-gaTNuzPD.js} +2 -2
  99. package/web/assets/{quadrantDiagram-AXDQQJYC-z0xeSJSB.js → quadrantDiagram-AXDQQJYC-3BEmlbuE.js} +1 -1
  100. package/web/assets/{radar-RG4KPBEZ-BV50NO32.js → radar-RG4KPBEZ-Cm1_Q0XJ.js} +1 -1
  101. package/web/assets/{railroad-74A4TZTK-BOkd4wGS.js → railroad-74A4TZTK-CJ4K8tjT.js} +1 -1
  102. package/web/assets/railroad-abnf-HS5TGJTU-BVKI4cmM.js +1 -0
  103. package/web/assets/railroad-ebnf-LZEXJU2U-UMcDSelb.js +1 -0
  104. package/web/assets/railroad-peg-WCYAUIDC-C00zpjQU.js +1 -0
  105. package/web/assets/{railroadDiagram-O6MQD6OU-2eK1mPm_.js → railroadDiagram-O6MQD6OU-Nzyn7Xx6.js} +1 -1
  106. package/web/assets/range-EplFIxc7.js +1 -0
  107. package/web/assets/{requirementDiagram-BXWQKSXE-DvlVp433.js → requirementDiagram-BXWQKSXE-9zXuiai_.js} +1 -1
  108. package/web/assets/{sankeyDiagram-P5KCCOFB-C6CsEyCg.js → sankeyDiagram-P5KCCOFB-Cnw1rQun.js} +1 -1
  109. package/web/assets/{sequenceDiagram-WJ2MYXX4-1wwxxN6P.js → sequenceDiagram-WJ2MYXX4-BSLLRV-U.js} +3 -3
  110. package/web/assets/{settingsRoute-Dxrvnqih.js → settingsRoute-C2Up46o6.js} +1 -1
  111. package/web/assets/src-CFHfg5gg.js +1 -0
  112. package/web/assets/stateDiagram-D77RDMKH-D07b4cJB.js +1 -0
  113. package/web/assets/stateDiagram-v2-MP3YSRHH-DaG9Yzhk.js +1 -0
  114. package/web/assets/step-BDqHAzds.js +1 -0
  115. package/web/assets/{swimlanes-42K2YHIH-XLThGmTf.js → swimlanes-42K2YHIH-RpDl0CG9.js} +1 -1
  116. package/web/assets/swimlanesDiagram-VR7AAH4N-7003ldWX.js +8 -0
  117. package/web/assets/time-Dzw-YSCF.js +1 -0
  118. package/web/assets/{timeline-definition-24CTP7MA-BEOV0lDT.js → timeline-definition-24CTP7MA-BgQOP12r.js} +1 -1
  119. package/web/assets/{treeView-Q6P3EWNA-Dm4FCZVW.js → treeView-Q6P3EWNA-BiSBsrlo.js} +1 -1
  120. package/web/assets/treemap-BfjIY-m6.js +1 -0
  121. package/web/assets/{treemap-WGGIJYW6-DEZGmkE8.js → treemap-WGGIJYW6-CNdjKqUG.js} +1 -1
  122. package/web/assets/useStore-CxjHbFPA.js +16 -0
  123. package/web/assets/{vennDiagram-4TSXK5OY-CDRUEJ_c.js → vennDiagram-4TSXK5OY-DLzOr3Yv.js} +1 -1
  124. package/web/assets/{wardley-WFR3VGLG-CjZ5JazT.js → wardley-WFR3VGLG-LCC8FWVb.js} +1 -1
  125. package/web/assets/{wardleyDiagram-VM6X3IG4-COsxLnU6.js → wardleyDiagram-VM6X3IG4-WrwzM3YW.js} +1 -1
  126. package/web/assets/{xychartDiagram-S5SC5T6Z-BfwzNIP-.js → xychartDiagram-S5SC5T6Z-B_HuoAnl.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-Dk1aHviY.js +0 -51
  132. package/web/assets/ChangesCard-Bn37dRew.js +0 -4
  133. package/web/assets/LibraryView-CGkpNOgR.js +0 -2
  134. package/web/assets/Settings-DJjEVwmu.js +0 -10
  135. package/web/assets/arc-Pry-7G1_.js +0 -1
  136. package/web/assets/architecture-7GRP2DOG-kbFZ_7eU.js +0 -1
  137. package/web/assets/array-BifhSqXX.js +0 -1
  138. package/web/assets/channel-CpJ3OYta.js +0 -1
  139. package/web/assets/chunk-75Z2AOVW-CGlA2AB-.js +0 -2
  140. package/web/assets/chunk-F27PBJKO-Cto7X88Z.js +0 -1
  141. package/web/assets/chunk-XXDRQBXY-DqAYeljN.js +0 -1
  142. package/web/assets/classDiagram-ZZMXUADV-BSqiC-6r.js +0 -1
  143. package/web/assets/classDiagram-v2-VYDZK3BY-BSqiC-6r.js +0 -1
  144. package/web/assets/cynefinDiagram-5FMLGOSQ-B2jZZ_es.js +0 -62
  145. package/web/assets/diagram-UQ7AKVKN-D7BIW4QU.js +0 -41
  146. package/web/assets/diagram-VX7I27RA-qmkf9rFQ.js +0 -24
  147. package/web/assets/diagram-Z3DM3KII-BZf2h-T8.js +0 -24
  148. package/web/assets/dist-v5Q1xZ2K.js +0 -1
  149. package/web/assets/eventmodeling-NTZA5JFV-DHYpZdDY.js +0 -1
  150. package/web/assets/flowDiagram-HODETNUW-BdNS-1c7.js +0 -1
  151. package/web/assets/ganttDiagram-EL5Y4UJY-DGMQzJ8U.js +0 -292
  152. package/web/assets/gitGraphDiagram-WWUBYQGX-DNfbdhPa.js +0 -106
  153. package/web/assets/index-kWXUrhG8.css +0 -1
  154. package/web/assets/infoDiagram-27XIBGKW-QcpEFiaQ.js +0 -2
  155. package/web/assets/init-C-OQMol4.js +0 -1
  156. package/web/assets/line-Cq9nN3X2.js +0 -1
  157. package/web/assets/linear-SzVmGSw4.js +0 -1
  158. package/web/assets/mount-C9EzYXqA.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-DiQLjR_J.js +0 -1
  162. package/web/assets/railroad-ebnf-LZEXJU2U-D4bSTzXb.js +0 -1
  163. package/web/assets/railroad-peg-WCYAUIDC-DDhl4AMh.js +0 -1
  164. package/web/assets/src-B6xuSHsQ.js +0 -1
  165. package/web/assets/stateDiagram-D77RDMKH-_fkXrLZe.js +0 -1
  166. package/web/assets/stateDiagram-v2-MP3YSRHH-Cuo-j5c1.js +0 -1
  167. package/web/assets/swimlanesDiagram-VR7AAH4N-DOpCt5KW.js +0 -8
  168. package/web/assets/useStore-C1E8iwAQ.js +0 -29
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";
@@ -76,12 +77,11 @@ import { listNativePiSessions, piAgentDir } from "./agents/pi/integration.js";
76
77
  import { deriveSessionState } from "./session/session-state.js";
77
78
  import { resolveStreamingBehavior } from "./session/record.js";
78
79
  import { createSessionEngine } from "./session/engine.js";
79
- import { exportProviderAuth, exportAccountApiKeys, exportAccountOAuthCredentials, importAccountOAuthCredentials, exportSyncableProviderAuth, exportProviderAuthTombstones, importProviderAuth, removeProvider, setProviderApiKey, listCredentialRecords, setProviderApiKeyLabeled, setProviderReferenceLabeled, removeProviderCredential, setCredentialSync, setCredentialUnattended, exportUnattendedRecords, unattendedCredentialRevision, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, exportSyncableRecords, exportRecordTombstones, importCredentialRecords, reconcileHostedCredentialRecords } from "./credentials/api.js";
80
+ import { exportProviderAuth, exportAccountApiKeys, exportAccountOAuthCredentials, importAccountOAuthCredentials, exportSyncableProviderAuth, exportProviderAuthTombstones, importProviderAuth, removeProvider, setProviderApiKey, listCredentialRecords, setProviderApiKeyLabeled, setProviderReferenceLabeled, removeProviderCredential, setCredentialSync, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, exportSyncableRecords, exportRecordTombstones, importCredentialRecords } from "./credentials/api.js";
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
84
  import { BIVY_AGENT_NOTE, mergeSyncedAgentInstructions, readAgentInstructions, sessionInstructions, writeAgentInstructions, MAX_AGENT_INSTRUCTIONS_BYTES } from "./agent-instructions.js";
84
- import { execEphemeralRequest } from "./ephemeral-exec.js";
85
85
  import { ApprovalManager } from "./approval.js";
86
86
  import { QuestionManager, validQuestions, isAskUserQuestionTool, formatQuestionResult } from "./question.js";
87
87
  import { NodeIdentity } from "./identity.js";
@@ -109,7 +109,6 @@ import { forceAbortTurn } from "./session/abort-recovery.js";
109
109
  import { runRequiredAutomationChecks } from "./automation-checks.js";
110
110
  import { configToLegacySettings, mergeLegacyIntoNodeConfig, readNodeConfig, writeNodeConfig } from "./node-config.js";
111
111
  import { loadProjectPolicy, resolveProjectSafety } from "./project-policy.js";
112
- import { hostedCustodyNode } from "./hosted-custody.js";
113
112
  import { PolicyEngine } from "./policy/policy-engine.js";
114
113
  import { SessionAllowRules } from "./policy/session-allow.js";
115
114
  import { TerminalManager } from "./terminal.js";
@@ -177,6 +176,7 @@ import { getSttConfig, isSttProvider, removeSttKey, setSttKey, setSttProvider, t
177
176
  import { synthesizeOpenAiSpeech } from "./tts.js";
178
177
  import { seal, open } from "./e2e.js";
179
178
  import { recoverModelAuthKey } from "./model-auth-key-recovery.js";
179
+ import { runPrompt, trustedInstructions } from "./work-instructions.js";
180
180
  import { RunDelegationService, parseDelegationSource } from "./run-tools.js";
181
181
  import { delegationCard } from "./session/delegation-cards.js";
182
182
  import { TERMINAL_DELEGATION, isDelegatedFrom } from "./session/delegations.js";
@@ -1081,6 +1081,7 @@ const linkedDevices = createLinkedDeviceController({
1081
1081
  syncPairingMetadata();
1082
1082
  relay?.pushRotate(deliveries);
1083
1083
  broadcast({ type: "devices.updated", devices });
1084
+ void resealRetiredAutomationTemplates();
1084
1085
  },
1085
1086
  });
1086
1087
  const accessDevices = createAccessDeviceController({
@@ -1562,6 +1563,15 @@ function nodeConfiguredDefaultAgent() {
1562
1563
  }
1563
1564
  /** How interactive sessions recover after a restart interrupted them mid-turn.
1564
1565
  * Defaults to "auto" (re-drive the turn); "manual" waits for a user tap. */
1566
+ /** This machine runs Slack prompts, which arrive unsealed: the node setting, or BIVY_SLACK_PROMPTS. Off by default. */
1567
+ function slackPromptsEnabled() {
1568
+ const env = process.env.BIVY_SLACK_PROMPTS;
1569
+ if (env === "1" || env === "true")
1570
+ return true;
1571
+ if (env === "0" || env === "false")
1572
+ return false;
1573
+ return readNodeConfig(appDir)?.automation?.slackPrompts === true;
1574
+ }
1565
1575
  /** Agents may screenshot app previews: the node setting, or BIVY_APP_SCREENSHOTS. */
1566
1576
  function appScreenshotsEnabled() {
1567
1577
  const env = process.env.BIVY_APP_SCREENSHOTS;
@@ -2267,7 +2277,7 @@ const notAgentServers = (offers) => offers.filter((offer) => !agentCommands.has(
2267
2277
  const appService = new AppService(appRegistry, appGateway ?? remotePreview, {
2268
2278
  start: async (spec) => {
2269
2279
  let failure = "Could not start the app terminal.";
2270
- const id = await runTerms.openRunTerminal({ ...spec, agent: "app", label: spec.name }, (event) => {
2280
+ const id = await runTerms.openRunTerminal({ ...spec, agent: "app", label: spec.name, background: true }, (event) => {
2271
2281
  const e = event;
2272
2282
  if (e.type === "terminal.error" && e.error)
2273
2283
  failure = e.error;
@@ -2441,7 +2451,7 @@ const RELAY_COMMANDS = {
2441
2451
  credsDir,
2442
2452
  sendEvent: (event) => relay?.sendEvent(event),
2443
2453
  broadcast,
2444
- pushModelAuthToControlPlane: (opts) => pushModelAuthToControlPlane(false, opts?.throwOnFailure === true, opts?.allowEmptyHostedEscrow === true),
2454
+ pushModelAuthToControlPlane: (opts) => pushModelAuthToControlPlane(false, opts?.throwOnFailure === true),
2445
2455
  refreshSessionAfterAuth,
2446
2456
  listProvidersUnified,
2447
2457
  }),
@@ -2897,6 +2907,13 @@ const RELAY_COMMANDS = {
2897
2907
  if (!requestedSessionId && wantedRuntimeId && record?.runtimeId !== wantedRuntimeId)
2898
2908
  record = null;
2899
2909
  record ??= await sessionForModelQuery(wantedRuntimeId);
2910
+ // A session whose agent hasn't started yet answers from the runtime's
2911
+ // placeholder catalog, which can be missing the account's current models —
2912
+ // a freshly launched machine then rejected a saved model it actually has.
2913
+ // Warm the real catalog first (a no-op re-read once the agent is running),
2914
+ // bounded so a slow spawn still answers with what is known.
2915
+ if (requestedSessionId)
2916
+ await warmSessionCatalog(record.session, SESSION_MODELS_WARM_MS);
2900
2917
  // Tag the list with the runtime it was resolved for so the client can
2901
2918
  // tell whether it belongs to the agent it currently has selected. Without
2902
2919
  // this a models.list answered for one agent (e.g. Codex's models) could
@@ -3069,8 +3086,7 @@ const RELAY_COMMANDS = {
3069
3086
  }
3070
3087
  }
3071
3088
  await removeProvider(credsDir, id);
3072
- // Deleting a granted credential must also remove its escrowed cloud copy.
3073
- await pushModelAuthToControlPlane(false, false, true);
3089
+ await pushModelAuthToControlPlane();
3074
3090
  await refreshSessionAfterAuth();
3075
3091
  broadcast({ type: "provider.oauth.reset", provider: id, ok: true, providers: await listProvidersUnified() });
3076
3092
  broadcast({ type: "providers.list", providers: await listProvidersUnified() });
@@ -3082,8 +3098,7 @@ const RELAY_COMMANDS = {
3082
3098
  async "provider.remove"(msg) {
3083
3099
  try {
3084
3100
  await removeProvider(credsDir, String(msg.provider ?? msg.id ?? ""));
3085
- // Deleting a granted credential must also remove its escrowed cloud copy.
3086
- await pushModelAuthToControlPlane(false, false, true);
3101
+ await pushModelAuthToControlPlane();
3087
3102
  await refreshSessionAfterAuth();
3088
3103
  broadcast({ type: "providers.list", providers: await listProvidersUnified() });
3089
3104
  }
@@ -3187,22 +3202,6 @@ const RELAY_COMMANDS = {
3187
3202
  return ctx.reply({ type: "session.promote.result", requestId, ok: false, error: "Promotion lost the epoch race" });
3188
3203
  ctx.reply({ type: "session.promote.result", requestId, ok: true, sessionId, epoch });
3189
3204
  },
3190
- // Ephemeral provisioning transport (node-broker path). A remote device that
3191
- // holds the user's cloud credentials asks this node to make ONE allowlisted
3192
- // HTTPS request to a provider (Fly/Hetzner/AWS/...) on its behalf. The
3193
- // token/credentials ride in the request headers and are used transiently —
3194
- // never persisted here — so the provisioning stays end-to-end (the control
3195
- // plane never sees it). The host allowlist is the SSRF guard.
3196
- async "ephemeral.exec"(msg) {
3197
- const requestId = String(msg.requestId ?? "");
3198
- try {
3199
- const result = await execEphemeralRequest(msg.request);
3200
- relay?.sendEvent({ type: "ephemeral.exec.result", requestId, ...result });
3201
- }
3202
- catch (error) {
3203
- relay?.sendEvent({ type: "ephemeral.exec.result", requestId, error: error instanceof Error ? error.message : String(error) });
3204
- }
3205
- },
3206
3205
  // Subscription / OAuth login driven from a remote device. The node runs the
3207
3206
  // provider's device-code (or paste-back) flow and reports the verification
3208
3207
  // link + code over the relay; the phone never needs the node's localhost.
@@ -3547,6 +3546,7 @@ function startRelayIfConfigured() {
3547
3546
  if (config.controlPlaneUrl && config.enrollmentToken) {
3548
3547
  sessionAdvertiseTarget = { controlPlaneUrl: config.controlPlaneUrl, enrollmentToken: config.enrollmentToken };
3549
3548
  void syncModelAuthFromControlPlane();
3549
+ void resealRetiredAutomationTemplates();
3550
3550
  if (remoteRuntimeEnabled()) {
3551
3551
  // Stage 3: layer a control-plane-backed location registry UNDER the in-memory
3552
3552
  // one (so a lookup surviving a restart resolves from durable state), then
@@ -3590,16 +3590,16 @@ function startRelayIfConfigured() {
3590
3590
  return true;
3591
3591
  }
3592
3592
  const modelAuthVaultKeyPath = path.join(appDir, "model-auth-vault.json");
3593
- const hostedModelAuthVaultKeyPath = path.join(appDir, "model-auth-hosted-vault.json");
3594
- const hostedImportedRecordsPath = path.join(appDir, "model-auth-hosted-records.json");
3595
3593
  let lastPushedModelAuthCiphertext = "";
3596
- let lastPushedHostedModelAuthCiphertext = "";
3597
- let lastPushedHostedModelAuthRevision = -1;
3598
- // Custody semantics are keyed on the dedicated flag (plus an ephemeral
3599
- // back-compat case) — see src/hosted-custody.ts. A persistent personal node
3600
- // that polls the hosted GitHub queue is NOT a custody guest: it owns its
3601
- // logins and must keep account sync and unattended-escrow publishing.
3602
- const isHostedCustodyNode = () => hostedCustodyNode();
3594
+ // Every node, the account's cloud computer included, owns its vault key and
3595
+ // gets it wrapped node-to-node. Remove state left by the retired hosted
3596
+ // custody mode, whose key the control plane could also decrypt.
3597
+ for (const retired of ["model-auth-hosted-vault.json", "model-auth-hosted-records.json"]) {
3598
+ try {
3599
+ fs.rmSync(path.join(appDir, retired), { force: true });
3600
+ }
3601
+ catch { /* best effort */ }
3602
+ }
3603
3603
  function readLocalModelAuthVaultKey() {
3604
3604
  try {
3605
3605
  const parsed = JSON.parse(fs.readFileSync(modelAuthVaultKeyPath, "utf8"));
@@ -3625,40 +3625,6 @@ function forgetLocalModelAuthVaultKey() {
3625
3625
  }
3626
3626
  catch { /* best effort */ }
3627
3627
  }
3628
- function readHostedModelAuthVaultKey() {
3629
- try {
3630
- const parsed = JSON.parse(fs.readFileSync(hostedModelAuthVaultKeyPath, "utf8"));
3631
- if (parsed.vaultKeyB64 && Buffer.from(parsed.vaultKeyB64, "base64").length === 32)
3632
- return parsed.vaultKeyB64;
3633
- }
3634
- catch { /* no hosted key */ }
3635
- return undefined;
3636
- }
3637
- function writeHostedModelAuthVaultKey(vaultKeyB64) {
3638
- fs.writeFileSync(hostedModelAuthVaultKeyPath, `${JSON.stringify({ vaultKeyB64, createdAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
3639
- }
3640
- function readHostedImportedRecords() {
3641
- try {
3642
- const parsed = JSON.parse(fs.readFileSync(hostedImportedRecordsPath, "utf8"));
3643
- return Array.isArray(parsed?.records)
3644
- ? parsed.records.filter((record) => Boolean(record) && typeof record === "object" && typeof record.provider === "string" && typeof record.label === "string")
3645
- : [];
3646
- }
3647
- catch {
3648
- return [];
3649
- }
3650
- }
3651
- function writeHostedImportedRecords(records) {
3652
- fs.writeFileSync(hostedImportedRecordsPath, `${JSON.stringify({ records }, null, 2)}\n`, { mode: 0o600 });
3653
- }
3654
- function ensureHostedModelAuthVaultKey() {
3655
- const existing = readHostedModelAuthVaultKey();
3656
- if (existing)
3657
- return existing;
3658
- const created = randomBytes(32).toString("base64");
3659
- writeHostedModelAuthVaultKey(created);
3660
- return created;
3661
- }
3662
3628
  function ensureLocalModelAuthVaultKey() {
3663
3629
  const existing = readLocalModelAuthVaultKey();
3664
3630
  if (existing)
@@ -3708,6 +3674,43 @@ async function modelAuthFetch(pathname, init = {}) {
3708
3674
  headers.set("content-type", "application/json");
3709
3675
  return fetch(`${sessionAdvertiseTarget.controlPlaneUrl.replace(/\/$/, "")}${pathname}`, { ...init, headers });
3710
3676
  }
3677
+ // A device revoke rotates the room key. Re-seal this node's stored automation
3678
+ // instructions under the new key so the node and its devices can still read
3679
+ // them, then drop the retired keys. Retried at startup until it completes.
3680
+ async function resealRetiredAutomationTemplates() {
3681
+ const retired = pairingStore.retiredRoomKeys();
3682
+ if (!retired.length || !sessionAdvertiseTarget)
3683
+ return;
3684
+ const current = pairingStore.roomKey();
3685
+ try {
3686
+ const res = await modelAuthFetch("/node/automation-templates");
3687
+ if (!res?.ok)
3688
+ throw new Error(`control plane returned ${res?.status}`);
3689
+ const { templates } = await res.json();
3690
+ const result = await resealAutomationTemplates({
3691
+ nodeId: identity.nodeId,
3692
+ current,
3693
+ retired,
3694
+ templates,
3695
+ save: async (id, templateCiphertext) => {
3696
+ const saved = await modelAuthFetch(`/node/automation-templates/${encodeURIComponent(id)}`, { method: "PUT", body: JSON.stringify({ templateCiphertext }) });
3697
+ if (!saved?.ok)
3698
+ throw new Error(`control plane returned ${saved?.status}`);
3699
+ },
3700
+ });
3701
+ if (result.resealed)
3702
+ console.log(`[automation-reseal] re-sealed ${result.resealed} automation template(s) under the rotated room key`);
3703
+ if (result.unreadable.length)
3704
+ console.warn(`[automation-reseal] ${result.unreadable.length} automation template(s) were sealed under a key this node no longer has: ${result.unreadable.join(", ")}`);
3705
+ // Another rotation during this pass retired `current`; run again for it.
3706
+ if (!pairingStore.roomKey().equals(current))
3707
+ return void resealRetiredAutomationTemplates();
3708
+ pairingStore.forgetRetiredRoomKeys();
3709
+ }
3710
+ catch (error) {
3711
+ console.warn("[automation-reseal] will retry at next start:", error.message);
3712
+ }
3713
+ }
3711
3714
  const remoteSessionAdmission = createRemoteSessionAdmission(async (idempotencyKey) => {
3712
3715
  // Only remote scopes reach this callback. Self-hosted control planes answer
3713
3716
  // allowed without an extension; losing enrollment mid-launch must fail closed.
@@ -3772,6 +3775,11 @@ function ensureModelAuthColdStart() {
3772
3775
  return;
3773
3776
  // A concurrent sync may have already landed the key — stop as soon as we have it.
3774
3777
  if (readLocalModelAuthVaultKey() || modelAuthColdStartAttempts >= MODEL_AUTH_COLDSTART_MAX_ATTEMPTS) {
3778
+ // No online peer answered: credential sync has settled without the
3779
+ // vault. A launch waiting on this milestone proceeds and asks the user
3780
+ // to sign in on this machine instead of waiting forever.
3781
+ if (!readLocalModelAuthVaultKey())
3782
+ void reportEphemeralMilestone("credentialsReadyAt");
3775
3783
  stopModelAuthColdStart();
3776
3784
  return;
3777
3785
  }
@@ -3792,61 +3800,34 @@ async function syncModelAuthFromControlPlane() {
3792
3800
  if (!res?.ok)
3793
3801
  return;
3794
3802
  const data = (await res.json().catch(() => ({})));
3795
- const hostedCustody = isHostedCustodyNode();
3796
- let hostedData = {};
3797
- if (hostedCustody) {
3798
- const hostedResponse = await modelAuthFetch("/node/model-auth-hosted-vault");
3799
- if (hostedResponse?.ok)
3800
- hostedData = (await hostedResponse.json().catch(() => ({})));
3801
- }
3802
- const targetVault = hostedCustody ? hostedData.hostedVault : data.vault;
3803
- let vaultKeyB64 = hostedCustody ? readHostedModelAuthVaultKey() : readLocalModelAuthVaultKey();
3804
- // Hosted runners may decrypt ONLY the separately encrypted snapshot of
3805
- // records that the user explicitly granted to unattended execution.
3806
- if (hostedCustody && !vaultKeyB64 && hostedData.hostedKey && hostedData.hostedVault && Buffer.from(hostedData.hostedKey, "base64").length === 32) {
3807
- vaultKeyB64 = hostedData.hostedKey;
3808
- writeHostedModelAuthVaultKey(vaultKeyB64);
3809
- }
3810
- if (targetVault?.ciphertext && (vaultKeyB64 || (!hostedCustody && data.wrappedKey))) {
3803
+ const targetVault = data.vault;
3804
+ let vaultKeyB64 = readLocalModelAuthVaultKey();
3805
+ if (targetVault?.ciphertext && (vaultKeyB64 || data.wrappedKey)) {
3811
3806
  let decrypted;
3812
3807
  try {
3813
- if (hostedCustody) {
3814
- decrypted = decryptModelAuthEnvelope(targetVault.ciphertext, vaultKeyB64);
3815
- }
3816
- else {
3817
- const wrapped = data.wrappedKey;
3818
- const recovered = recoverModelAuthKey({
3819
- localKey: vaultKeyB64,
3820
- unwrap: wrapped ? () => pairingStore.unwrapFromNodePublicKey(wrapped.wrappedByPublicKey, wrapped.wrappedKey) : undefined,
3821
- decrypt: (key) => decryptModelAuthEnvelope(targetVault.ciphertext, key),
3822
- });
3823
- if (!recovered)
3824
- throw new Error("No available key can decrypt the account vault");
3825
- vaultKeyB64 = recovered.key;
3826
- decrypted = recovered.value;
3827
- writeLocalModelAuthVaultKey(vaultKeyB64);
3828
- }
3808
+ const wrapped = data.wrappedKey;
3809
+ const recovered = recoverModelAuthKey({
3810
+ localKey: vaultKeyB64,
3811
+ unwrap: wrapped ? () => pairingStore.unwrapFromNodePublicKey(wrapped.wrappedByPublicKey, wrapped.wrappedKey) : undefined,
3812
+ decrypt: (key) => decryptModelAuthEnvelope(targetVault.ciphertext, key),
3813
+ });
3814
+ if (!recovered)
3815
+ throw new Error("No available key can decrypt the account vault");
3816
+ vaultKeyB64 = recovered.key;
3817
+ decrypted = recovered.value;
3818
+ writeLocalModelAuthVaultKey(vaultKeyB64);
3829
3819
  }
3830
3820
  catch (error) {
3831
3821
  // Most commonly this node cached the previous generation while another
3832
3822
  // survivor completed a revoke-triggered re-key. Forget it and request a
3833
3823
  // wrap of the current key; retaining it would make every poll fail forever.
3834
- if (hostedCustody) {
3835
- try {
3836
- fs.rmSync(hostedModelAuthVaultKeyPath, { force: true });
3837
- }
3838
- catch { /* best effort */ }
3839
- lastPushedHostedModelAuthCiphertext = "";
3840
- }
3841
- else {
3842
- forgetLocalModelAuthVaultKey();
3843
- lastPushedModelAuthCiphertext = "";
3844
- await modelAuthFetch("/node/model-auth-key/request", {
3845
- method: "POST",
3846
- body: JSON.stringify({ publicKey: pairingStore.nodePublicKeyB64(), rejectedWrappedKey: data.wrappedKey?.wrappedKey }),
3847
- });
3848
- ensureModelAuthColdStart();
3849
- }
3824
+ forgetLocalModelAuthVaultKey();
3825
+ lastPushedModelAuthCiphertext = "";
3826
+ await modelAuthFetch("/node/model-auth-key/request", {
3827
+ method: "POST",
3828
+ body: JSON.stringify({ publicKey: pairingStore.nodePublicKeyB64(), rejectedWrappedKey: data.wrappedKey?.wrappedKey }),
3829
+ });
3830
+ ensureModelAuthColdStart();
3850
3831
  console.warn("[auth-sync] cached vault key is stale; requested the rotated key:", error.message);
3851
3832
  return;
3852
3833
  }
@@ -3856,45 +3837,31 @@ async function syncModelAuthFromControlPlane() {
3856
3837
  // so importing records alone (plus its record-keyed tombstones) is complete.
3857
3838
  // Fall back to the provider-keyed fields for a v2 peer.
3858
3839
  if (v >= 3) {
3859
- if (hostedCustody) {
3860
- // Hosted snapshots are authoritative filtered sets, not additive peer
3861
- // merges. Remove custody-derived records omitted by a later snapshot
3862
- // so a revoke takes effect on already-running hosted nodes.
3863
- const current = await reconcileHostedCredentialRecords(credsDir, records, readHostedImportedRecords());
3864
- writeHostedImportedRecords(current);
3865
- }
3866
- else {
3867
- await importCredentialRecords(credsDir, records, recordsDeletedAt);
3868
- }
3840
+ await importCredentialRecords(credsDir, records, recordsDeletedAt);
3869
3841
  }
3870
3842
  else {
3871
3843
  await importProviderAuth(credsDir, providers, deletedAt);
3872
3844
  }
3873
3845
  importLocalModels(localModelsDir, localModels);
3874
- // Account-wide agent instructions reconcile last-writer-wins. Hosted
3875
- // runners hold only the filtered credential snapshot, which never carries
3876
- // them, so they neither import nor republish.
3877
- const instructionsSync = hostedCustody ? "unchanged" : mergeSyncedAgentInstructions(appDir, decrypted.agentInstructions);
3846
+ // Account-wide agent instructions reconcile last-writer-wins.
3847
+ const instructionsSync = mergeSyncedAgentInstructions(appDir, decrypted.agentInstructions);
3878
3848
  if (instructionsSync === "imported")
3879
3849
  broadcast({ type: "node.settings", settings: nodeSettingsSnapshot() });
3880
3850
  // A synced key or config change can both alter the projection, so always
3881
3851
  // regenerate it (and refresh the panel) after importing the vault.
3882
3852
  await writePiModelsProjection();
3883
3853
  await broadcastLocalModels();
3884
- if (hostedCustody)
3885
- lastPushedHostedModelAuthCiphertext = targetVault.ciphertext;
3886
- else
3887
- lastPushedModelAuthCiphertext = targetVault.ciphertext;
3854
+ lastPushedModelAuthCiphertext = targetVault.ciphertext;
3888
3855
  // Got the key and imported the vault (incl. any subscription-OAuth logins) —
3889
3856
  // the cold-start race is over.
3890
3857
  stopModelAuthColdStart();
3891
3858
  broadcast({ type: "providers.list", providers: await listProvidersUnified() });
3892
- if (!hostedCustody && data.vault?.needsRotation)
3859
+ if (data.vault?.needsRotation)
3893
3860
  await pushModelAuthToControlPlane(true);
3894
3861
  else if (instructionsSync === "local-newer")
3895
3862
  await pushModelAuthToControlPlane();
3896
3863
  }
3897
- else if (targetVault?.ciphertext && !vaultKeyB64 && !hostedCustody) {
3864
+ else if (targetVault?.ciphertext && !vaultKeyB64) {
3898
3865
  await modelAuthFetch("/node/model-auth-key/request", { method: "POST", body: JSON.stringify({ publicKey: pairingStore.nodePublicKeyB64() }) });
3899
3866
  // No peer has wrapped our key yet. Fast-retry (bounded) so a short-lived
3900
3867
  // ephemeral runner picks up the key within seconds of a peer answering,
@@ -3907,13 +3874,10 @@ async function syncModelAuthFromControlPlane() {
3907
3874
  await pushModelAuthToControlPlane();
3908
3875
  }
3909
3876
  await processModelAuthKeyRequests(data.requests ?? []);
3910
- // A personal node with no vault has nothing to hydrate and is ready. A
3911
- // hosted-custody guest is different: an absent filtered snapshot means it
3912
- // has no model credential at all, not that hydration succeeded.
3913
- const credentialsReady = hostedCustody
3914
- ? Boolean(targetVault?.ciphertext && readHostedModelAuthVaultKey())
3915
- : Boolean(!targetVault?.ciphertext || readLocalModelAuthVaultKey());
3916
- if (credentialsReady)
3877
+ // With no account vault there is nothing to hydrate; with one, ready once
3878
+ // this node holds its key. (The cold-start retry reports it too when no
3879
+ // peer answers, so a waiting launch can move on.)
3880
+ if (!targetVault?.ciphertext || readLocalModelAuthVaultKey())
3917
3881
  void reportEphemeralMilestone("credentialsReadyAt");
3918
3882
  }
3919
3883
  catch (error) {
@@ -3934,51 +3898,7 @@ async function processModelAuthKeyRequests(requests) {
3934
3898
  });
3935
3899
  }
3936
3900
  }
3937
- async function pushHostedModelAuthToControlPlane(allowEmpty = false) {
3938
- const [records, revision] = await Promise.all([exportUnattendedRecords(credsDir), unattendedCredentialRevision(credsDir)]);
3939
- // A setup guest must not establish an empty snapshot when the credential is
3940
- // first saved (before the explicit grant command follows). Otherwise its
3941
- // one allowed initial write would be consumed by an unusable vault.
3942
- //
3943
- // And an IMPLICIT empty push must never revoke the escrow: an empty snapshot
3944
- // is how "disable unattended runs" removes the cloud copy, but every vault
3945
- // change funnels through here — so a node whose copy simply lacks the grant
3946
- // (not yet synced, or granted elsewhere) would otherwise wipe the encrypted
3947
- // cloud copy the user's other machines just published. Only an explicit
3948
- // local revoke/delete (allowEmpty) may publish an empty snapshot.
3949
- if (Object.keys(records).length === 0 && (!allowEmpty || isHostedCustodyNode()))
3950
- return;
3951
- if (revision === lastPushedHostedModelAuthRevision)
3952
- return;
3953
- const key = ensureHostedModelAuthVaultKey();
3954
- const ciphertext = encryptModelAuthProviders({}, {}, {}, key, records, {});
3955
- const publish = async (expectedGeneration) => modelAuthFetch("/node/model-auth-hosted-vault", {
3956
- method: "PUT",
3957
- body: JSON.stringify({ ciphertext, vaultKeyB64: key, expectedGeneration, revision }),
3958
- });
3959
- const currentResponse = await modelAuthFetch("/node/model-auth-hosted-vault");
3960
- if (currentResponse?.status === 403)
3961
- throw new Error("hosted credential custody is not enabled for this account");
3962
- const current = currentResponse?.ok
3963
- ? (await currentResponse.json().catch(() => ({})))
3964
- : {};
3965
- let response = await publish(current.hostedVault?.generation ?? 0);
3966
- if (response?.status === 409) {
3967
- const conflict = await response.json().catch(() => ({}));
3968
- // Retry only when our logical state is at least as new; the store also
3969
- // enforces this revision check atomically with the generation CAS.
3970
- if (revision >= Number(conflict.revision ?? 0))
3971
- response = await publish(Number(conflict.generation ?? 0));
3972
- }
3973
- if (response?.ok) {
3974
- lastPushedHostedModelAuthCiphertext = ciphertext;
3975
- lastPushedHostedModelAuthRevision = revision;
3976
- }
3977
- else {
3978
- throw new Error(`hosted model-auth push failed (${response?.status ?? "offline"})`);
3979
- }
3980
- }
3981
- async function pushModelAuthToControlPlane(rotateKey = false, throwOnFailure = false, allowEmptyHostedEscrow = false) {
3901
+ async function pushModelAuthToControlPlane(rotateKey = false, throwOnFailure = false) {
3982
3902
  if (!sessionAdvertiseTarget)
3983
3903
  return;
3984
3904
  // Piggyback the (plaintext, non-secret) provider status summary on every
@@ -3988,16 +3908,6 @@ async function pushModelAuthToControlPlane(rotateKey = false, throwOnFailure = f
3988
3908
  // versa.
3989
3909
  await pushProviderSummaryToControlPlane();
3990
3910
  try {
3991
- // A hosted runner holds only the explicitly granted snapshot and must never
3992
- // overwrite the peer-to-peer account vault with that filtered subset.
3993
- if (isHostedCustodyNode()) {
3994
- // A credential-setup guest may establish the initial filtered snapshot.
3995
- // The control plane refuses managed-guest replacement after that first
3996
- // write, so normal hosted runners remain recipients rather than authorities.
3997
- if (process.env.BIVY_HOSTED_CREDENTIAL_PUBLISH === "1" && !lastPushedHostedModelAuthCiphertext)
3998
- await pushHostedModelAuthToControlPlane();
3999
- return;
4000
- }
4001
3911
  // Only push credentials on the account-sync tier; a `sync: "node"` credential
4002
3912
  // stays local (per-credential opt-out). Tombstones still converge for all.
4003
3913
  const providers = await exportSyncableProviderAuth(credsDir);
@@ -4013,10 +3923,8 @@ async function pushModelAuthToControlPlane(rotateKey = false, throwOnFailure = f
4013
3923
  writeLocalModelAuthVaultKey(vaultKeyB64);
4014
3924
  const instructions = readAgentInstructions(appDir);
4015
3925
  const ciphertext = encryptModelAuthProviders(providers, deletedAt, localModels, vaultKeyB64, records, recordsDeletedAt, instructions.updatedAt > 0 ? instructions : undefined);
4016
- if (!rotateKey && ciphertext === lastPushedModelAuthCiphertext) {
4017
- await pushHostedModelAuthToControlPlane(allowEmptyHostedEscrow);
3926
+ if (!rotateKey && ciphertext === lastPushedModelAuthCiphertext)
4018
3927
  return;
4019
- }
4020
3928
  const push = await modelAuthFetch("/node/model-auth-vault", { method: "PUT", body: JSON.stringify({ ciphertext, rotated: rotateKey }) });
4021
3929
  if (!push?.ok) {
4022
3930
  if (rotateKey) {
@@ -4032,10 +3940,6 @@ async function pushModelAuthToControlPlane(rotateKey = false, throwOnFailure = f
4032
3940
  body: JSON.stringify({ targetNodeId: identity.nodeId, wrappedByPublicKey: pairingStore.nodePublicKeyB64(), wrappedKey: pairingStore.wrapForNodePublicKey(pairingStore.nodePublicKeyB64(), vaultKeyB64) }),
4033
3941
  });
4034
3942
  lastPushedModelAuthCiphertext = ciphertext;
4035
- // Publish a DIFFERENT ciphertext under a DIFFERENT key containing only
4036
- // records with `unattended:true`. Escrowing this key cannot decrypt the E2E
4037
- // account vault, which is the critical custody separation.
4038
- await pushHostedModelAuthToControlPlane(allowEmptyHostedEscrow);
4039
3943
  }
4040
3944
  catch (error) {
4041
3945
  console.warn("[auth-sync] could not push model auth:", error.message);
@@ -5558,13 +5462,14 @@ async function executeWorkItem(item, report, signal) {
5558
5462
  // source.
5559
5463
  let credentialLabels;
5560
5464
  let filter;
5561
- if (item.body?.startsWith("bivy-room-v1:")) {
5465
+ const sealed = Boolean(item.body?.startsWith("bivy-room-v1:"));
5466
+ if (sealed) {
5562
5467
  const [, nodeId, ...payload] = item.body.split(":");
5563
5468
  if (nodeId !== identity.nodeId || payload.length === 0) {
5564
5469
  throw new Error("automation instructions were encrypted for a different node");
5565
5470
  }
5566
5471
  try {
5567
- const template = decodeAutomationTemplate(open(pairingStore.roomKey(), payload.join(":")));
5472
+ const template = decodeAutomationTemplate(pairingStore.openSealed(payload.join(":")));
5568
5473
  credentialLabels = template.credentialLabels;
5569
5474
  filter = template.filter;
5570
5475
  item = { ...item, body: template.instructions };
@@ -5573,6 +5478,12 @@ async function executeWorkItem(item, report, signal) {
5573
5478
  throw new Error("could not decrypt automation instructions on this node");
5574
5479
  }
5575
5480
  }
5481
+ // Only sealed, fetched, built-in or opted-in Slack instructions reach the agent
5482
+ // (src/work-instructions.ts): the control plane can't author a run.
5483
+ const trusted = trustedInstructions(item, { sealed, slackPrompts: slackPromptsEnabled() });
5484
+ if (!trusted.ok)
5485
+ throw new Error(trusted.reason);
5486
+ item = { ...item, body: trusted.body };
5576
5487
  // Gate before any repository fetch, session continuation, or agent creation.
5577
5488
  // A skip finishes this selected automation; it does not fall through to another.
5578
5489
  if (filter && !await passWebhookFilter(filter, item, report, signal))
@@ -5709,9 +5620,7 @@ async function executeWorkItem(item, report, signal) {
5709
5620
  // into the prompt. A scheduled chat message must arrive verbatim; the name is
5710
5621
  // only the run/session label.
5711
5622
  const delegatedProvenance = parseDelegationSource(item.source);
5712
- const request = item.source === "schedule" || delegatedProvenance
5713
- ? (item.body || item.title)
5714
- : item.body ? `${item.title}\n\n${item.body}` : item.title;
5623
+ const request = runPrompt(item, trusted.titleInPrompt);
5715
5624
  // Plain chat messages (scheduled "message me later" reminders) skip
5716
5625
  // auto-push and required checks — even when a workspace target happens to be
5717
5626
  // a git checkout.
@@ -7619,7 +7528,7 @@ function evaluateEphemeralTeardown() {
7619
7528
  const records = new Set(openSessions.values());
7620
7529
  const anyWorking = [...records].some((r) => r.isWorking);
7621
7530
  const anyRemoteActive = clients.size > 0 || (relay?.clientCount ?? 0) > 0
7622
- || [...records].some((r) => r.remoteActive);
7531
+ || [...records].some((r) => r.remoteActive) || remotePreview.inUse();
7623
7532
  const inFlightWork = controlPlanePoller?.inFlightCount() ?? 0;
7624
7533
  if (anyWorking || anyRemoteActive || inFlightWork > 0) {
7625
7534
  ephemeralEverBusy = true;
@@ -7973,9 +7882,7 @@ function actionableAgentError(runtimeId, error) {
7973
7882
  if (id.startsWith("codex"))
7974
7883
  return "Codex is not signed in. Run `codex login`, then retry; the same login works from Bivy and the PWA.";
7975
7884
  if (id === "pi" || id === "aider")
7976
- return isHostedCustodyNode()
7977
- ? "No model credential is available to this Bivy Cloud Machine. Connect a provider and enable it for Bivy Cloud, then retry."
7978
- : "No model credential is configured. Run `bivy provider login`, then retry. This is only required once and compatible credentials sync E2E-encrypted to your other Bivy nodes.";
7885
+ return "No model credential is configured. Run `bivy provider login`, then retry. This is only required once and compatible credentials sync E2E-encrypted to your other Bivy nodes.";
7979
7886
  return "The selected agent needs model authentication. Sign in through its native CLI, then retry.";
7980
7887
  }
7981
7888
  return raw;
@@ -8130,14 +8037,17 @@ function attachSessionListeners(record) {
8130
8037
  if (event.type === "turn_start" || event.type === "message_boundary" || event.type === "message_end" || event.type === "turn_end") {
8131
8038
  transcripts.persistTranscriptSnapshot(record);
8132
8039
  }
8133
- // A finalized assistant message may reference a remote image via markdown
8134
- // (`![alt](https://…)`) — fetch and store it now so the chat can render it
8135
- // (see resolveInlineImages). Checked on both events: message_end is the
8136
- // precise "this assistant message is done" signal most runtimes emit, but
8137
- // turn_end is a safety net for one that only surfaces the final text there.
8138
- // Fire-and-forget and internally deduped, so checking on both costs nothing.
8040
+ // A finalized assistant message may reference an image via markdown — a
8041
+ // remote URL (`![alt](https://…)`) to fetch, or a file the agent just
8042
+ // produced in its workspace (`![alt](out/chart.png)`) to read. Resolve and
8043
+ // store either now, so the chat can render it and history keeps it after the
8044
+ // workspace is gone. Checked on both events: message_end is the precise
8045
+ // "this assistant message is done" signal most runtimes emit, but turn_end
8046
+ // is a safety net for one that only surfaces the final text there. Both
8047
+ // resolvers are internally deduped, so checking twice costs nothing.
8139
8048
  if (event.type === "message_end" || event.type === "turn_end") {
8140
8049
  transcripts.resolveInlineImages(record);
8050
+ transcripts.resolveWorkspaceRefs(record, harnessDirFor(record));
8141
8051
  }
8142
8052
  // Durably persist the throttled sidecars at the turn boundary so a crash
8143
8053
  // loses at most the in-flight turn's UI detail, not the whole turn. A
@@ -9069,6 +8979,9 @@ async function sessionForModelQuery(runtimeId) {
9069
8979
  modelQueryScratchPending.set(wanted, build);
9070
8980
  return build;
9071
8981
  }
8982
+ /** How long a session's models.list waits for its agent's real catalog before
8983
+ * answering with what is known (below the client's 15 s launch timeout). */
8984
+ const SESSION_MODELS_WARM_MS = 8_000;
9072
8985
  // A fresh scratch answers its first models.list from the runtime's placeholder
9073
8986
  // list (no live agent yet). Warm the real catalog in the background — for Claude
9074
8987
  // Code this spins up the agent subprocess just far enough for supportedModels()
@@ -10447,8 +10360,7 @@ app.post("/api/auth/credentials", async (req, res, next) => {
10447
10360
  app.delete("/api/auth/credentials/:provider/:label", async (req, res, next) => {
10448
10361
  try {
10449
10362
  await removeProviderCredential(credsDir, String(req.params.provider), String(req.params.label));
10450
- // Deleting a granted credential must also remove its escrowed cloud copy.
10451
- await pushModelAuthToControlPlane(false, false, true);
10363
+ await pushModelAuthToControlPlane();
10452
10364
  await refreshSessionAfterAuth();
10453
10365
  res.json({ ok: true, records: await listCredentialRecords(credsDir), providers: await listProvidersUnified() });
10454
10366
  }
@@ -10460,30 +10372,10 @@ app.post("/api/auth/credentials/:provider/:label/availability", async (req, res,
10460
10372
  try {
10461
10373
  const sync = req.body?.sync === "node" ? "node" : "account";
10462
10374
  await setCredentialSync(credsDir, String(req.params.provider), String(req.params.label), sync);
10463
- // Demoting to machine-only revokes any custody grant; let that clear the escrow.
10464
- await pushModelAuthToControlPlane(false, false, sync === "node");
10465
- res.json({ ok: true, records: await listCredentialRecords(credsDir) });
10466
- }
10467
- catch (error) {
10468
- next(error);
10469
- }
10470
- });
10471
- app.post("/api/auth/credentials/:provider/:label/unattended", async (req, res, next) => {
10472
- const provider = String(req.params.provider);
10473
- const label = String(req.params.label);
10474
- const enable = req.body?.unattended === true;
10475
- const previous = (await listCredentialRecords(credsDir).catch(() => []))
10476
- .find((record) => record.provider === provider.trim().toLowerCase() && record.label === label)?.unattended === true;
10477
- try {
10478
- await setCredentialUnattended(credsDir, provider, label, enable);
10479
- // Fail loud (mirrors the relay command): the UI must never claim an
10480
- // encrypted cloud copy exists when custody publication failed. An explicit
10481
- // disable is the one caller allowed to publish an empty escrow snapshot.
10482
- await pushModelAuthToControlPlane(false, true, !enable);
10375
+ await pushModelAuthToControlPlane();
10483
10376
  res.json({ ok: true, records: await listCredentialRecords(credsDir) });
10484
10377
  }
10485
10378
  catch (error) {
10486
- await setCredentialUnattended(credsDir, provider, label, previous).catch(() => { });
10487
10379
  next(error);
10488
10380
  }
10489
10381
  });