@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.
- package/README.md +1 -2
- package/dist/agent-instructions.js +14 -4
- package/dist/apps/remote-preview.js +7 -0
- package/dist/apps/service.js +6 -1
- package/dist/apps/styles.css +56 -70
- package/dist/automation-reseal.js +30 -0
- package/dist/config-cli.js +2 -0
- package/dist/controllers/credential-commands.js +3 -27
- package/dist/credentials/api.js +2 -43
- package/dist/credentials/document.js +6 -14
- package/dist/credentials/store.js +5 -6
- package/dist/device-registry.js +27 -0
- package/dist/harness/checkpoint.js +0 -0
- package/dist/node-config.js +4 -3
- package/dist/runtime/model-catalog.js +16 -0
- package/dist/runtime/tool-call-map.js +1 -1
- package/dist/runtime/tool-trace.js +1 -1
- package/dist/server.js +136 -244
- package/dist/session/event-log.js +136 -29
- package/dist/session/inline-image-fetch.js +24 -33
- package/dist/session/message-components.js +429 -0
- package/dist/session/run-terminal.js +9 -7
- package/dist/session/transcript-persistence.js +135 -10
- package/dist/work-instructions.js +50 -0
- package/package.json +2 -2
- package/web/assets/AutomationsView-HnUfeti_.js +51 -0
- package/web/assets/ChangesCard-Dnkguen5.js +4 -0
- package/web/assets/LibraryView-CSKKV8TI.js +2 -0
- package/web/assets/{ProviderConnect-BPwnaFGk.js → ProviderConnect-DNgxGmP9.js} +1 -1
- package/web/assets/SessionList-CYU9IGnd.js +1 -0
- package/web/assets/Settings-HQZtxiMV.js +10 -0
- package/web/assets/{Sheet-DOOJosQY.js → Sheet-DoPtOHBC.js} +1 -1
- package/web/assets/{StatusDot-dpbztK53.js → Spinner-CPdRfDcT.js} +1 -1
- package/web/assets/{Terminal-DVGXS6Yd.js → Terminal-DY6UB5U-.js} +1 -1
- package/web/assets/{VoiceSettings-DX-n0SFl.js → VoiceSettings-BoQ_Mhpm.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-CnJ4astd.js → abnfDiagram-VCTEODGH-n-P11_L4.js} +1 -1
- package/web/assets/arc-BNLueWOY.js +1 -0
- package/web/assets/architecture-7GRP2DOG-DyG-wgaH.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-B26-V_Xd.js → architectureDiagram-5GKGNRK7-C0b9IP5U.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-DuoZkDHs.js → blockDiagram-I7D4REHJ-iVx1FQBD.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-B81PZ-Vw.js → c4Diagram-7LVT6UL2-DIwEaAvJ.js} +1 -1
- package/web/assets/channel-HmCJmS4T.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-DmSrqg9X.js → chunk-4HAMMTFA-DSsyukSz.js} +1 -1
- package/web/assets/chunk-75Z2AOVW-zwLYo-XL.js +2 -0
- package/web/assets/{chunk-DU6HZSFF-jMBIr1yK.js → chunk-DU6HZSFF-PGuLkqeP.js} +15 -15
- package/web/assets/chunk-F27PBJKO-CpkyeGC9.js +1 -0
- package/web/assets/{chunk-GMAD6QVW-C5YdBKWY.js → chunk-GMAD6QVW-CZSmK3Ms.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-DOOAmJLJ.js → chunk-GVQU2GXP-C2XMkY6v.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-CISXYXGE.js → chunk-IMKFNOWR-DAIVz3j5.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-CvWtiEy-.js → chunk-L3NEJ4N5-C0geNTDK.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-yVAn5yky.js → chunk-OSK3NFVY-D-WN2fBr.js} +2 -2
- package/web/assets/{chunk-P2QGCYS3-D-VGoELb.js → chunk-P2QGCYS3-DYGhprKz.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-0Mj4Bp-N.js → chunk-POPQ4Y6H-Dlsu2a2Q.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-Dvvdg6s1.js → chunk-PWAF6VOD-DK0QWYdc.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-DcllnbDq.js → chunk-SHT3W25Y-BpQHIdUi.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-DnuOGeio.js → chunk-SVP7TREG-Cs5YWxpG.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-Dr76NyiT.js → chunk-TICWLB2K-BRsWLPXA.js} +1 -1
- package/web/assets/chunk-XXDRQBXY-CjF14Z7v.js +1 -0
- package/web/assets/classDiagram-ZZMXUADV-BmdAoNdO.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-BmdAoNdO.js +1 -0
- package/web/assets/colors-Cg5Vh3br.js +1 -0
- package/web/assets/{cose-bilkent-JH36ORCC-BIqBF2fY.js → cose-bilkent-JH36ORCC-CEUD3V3W.js} +1 -1
- package/web/assets/{cynefin-OW5HDTMX-DoAt_4E6.js → cynefin-OW5HDTMX-DoHWnu_-.js} +1 -1
- package/web/assets/cynefinDiagram-5FMLGOSQ-CgPeHmV5.js +62 -0
- package/web/assets/{dagre-GXQ25YYZ-DIAXxVMd.js → dagre-GXQ25YYZ-CrIlgwM7.js} +1 -1
- package/web/assets/{defaultLocale-BFoDCU3G.js → defaultLocale-CNPGWpMu.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-DA78JGp6.js → diagram-S7CK7UJ4-BM_l51zV.js} +1 -1
- package/web/assets/diagram-UQ7AKVKN-CfkxqgPI.js +41 -0
- package/web/assets/{diagram-VSXAHHWV-DCkvR_pJ.js → diagram-VSXAHHWV-0y5vCI9g.js} +1 -1
- package/web/assets/diagram-VX7I27RA-BMkMy7jn.js +24 -0
- package/web/assets/diagram-Z3DM3KII--0fxfDqG.js +24 -0
- package/web/assets/dist-kYfyPZP9.js +1 -0
- package/web/assets/{ebnfDiagram-PWID7BFC-CSCD0PAi.js → ebnfDiagram-PWID7BFC-6tPNzbJl.js} +1 -1
- package/web/assets/embed-BXV41wg7.js +201 -0
- package/web/assets/{erDiagram-RLTQ6QDP-M512qtMu.js → erDiagram-RLTQ6QDP-Bjqm_o3R.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-CoCZxN1n.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-CznfWzEq.js +1 -0
- package/web/assets/ganttDiagram-EL5Y4UJY-92X7KOOI.js +292 -0
- package/web/assets/{gitGraph-4MIJSDKK-CwjwFpZb.js → gitGraph-4MIJSDKK-B5F-u7Vq.js} +1 -1
- package/web/assets/gitGraphDiagram-WWUBYQGX-Ct2hVG0b.js +106 -0
- package/web/assets/index-BKSbod42.css +1 -0
- package/web/assets/{index-CNs5i7tU.js → index-CnjJQpgZ.js} +2 -2
- package/web/assets/{info-A6RAGUB7-D6nXu5V2.js → info-A6RAGUB7-D3xmYWyC.js} +1 -1
- package/web/assets/infoDiagram-27XIBGKW-lzqZ7xaA.js +2 -0
- package/web/assets/init-ZlXS9mIS.js +1 -0
- package/web/assets/{ishikawaDiagram-5VMMS53U-4bsiyG2x.js → ishikawaDiagram-5VMMS53U-CsVZHXR0.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-o3U3L2rt.js → journeyDiagram-3NMN7TZE-DN9AdyVy.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-CQOPj0NZ.js → kanban-definition-UXKFOSKX-gvkBaOFe.js} +1 -1
- package/web/assets/linear-CeYXFQza.js +1 -0
- package/web/assets/{mermaid-parser.core-Ba2-DGvb.js → mermaid-parser.core-CPwOm7-a.js} +3 -3
- package/web/assets/{mermaid.core-BxdMVbm9.js → mermaid.core-F09Wr40d.js} +6 -6
- package/web/assets/{mindmap-definition-YA3MSWOX-1Eu4-zLM.js → mindmap-definition-YA3MSWOX-Qzc2J9Pm.js} +1 -1
- package/web/assets/mount-Bvl-yslT.js +36 -0
- package/web/assets/ordinal-BdBDtYTt.js +1 -0
- package/web/assets/{packet-AYTQ26CC-BTZ7CbVd.js → packet-AYTQ26CC-Unxh5mP2.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-DNdK3axo.js → pegDiagram-XKGWAZYB-BIqcRp3m.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-DidjfqgW.js → pie-WAS4IAKB-BipHZKbc.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-Dme68TUA.js → pieDiagram-E7YTZNPT-gaTNuzPD.js} +2 -2
- package/web/assets/{quadrantDiagram-AXDQQJYC-z0xeSJSB.js → quadrantDiagram-AXDQQJYC-3BEmlbuE.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-BV50NO32.js → radar-RG4KPBEZ-Cm1_Q0XJ.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-BOkd4wGS.js → railroad-74A4TZTK-CJ4K8tjT.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-BVKI4cmM.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-UMcDSelb.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-C00zpjQU.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-2eK1mPm_.js → railroadDiagram-O6MQD6OU-Nzyn7Xx6.js} +1 -1
- package/web/assets/range-EplFIxc7.js +1 -0
- package/web/assets/{requirementDiagram-BXWQKSXE-DvlVp433.js → requirementDiagram-BXWQKSXE-9zXuiai_.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-C6CsEyCg.js → sankeyDiagram-P5KCCOFB-Cnw1rQun.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-1wwxxN6P.js → sequenceDiagram-WJ2MYXX4-BSLLRV-U.js} +3 -3
- package/web/assets/{settingsRoute-Dxrvnqih.js → settingsRoute-C2Up46o6.js} +1 -1
- package/web/assets/src-CFHfg5gg.js +1 -0
- package/web/assets/stateDiagram-D77RDMKH-D07b4cJB.js +1 -0
- package/web/assets/stateDiagram-v2-MP3YSRHH-DaG9Yzhk.js +1 -0
- package/web/assets/step-BDqHAzds.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-XLThGmTf.js → swimlanes-42K2YHIH-RpDl0CG9.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-7003ldWX.js +8 -0
- package/web/assets/time-Dzw-YSCF.js +1 -0
- package/web/assets/{timeline-definition-24CTP7MA-BEOV0lDT.js → timeline-definition-24CTP7MA-BgQOP12r.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-Dm4FCZVW.js → treeView-Q6P3EWNA-BiSBsrlo.js} +1 -1
- package/web/assets/treemap-BfjIY-m6.js +1 -0
- package/web/assets/{treemap-WGGIJYW6-DEZGmkE8.js → treemap-WGGIJYW6-CNdjKqUG.js} +1 -1
- package/web/assets/useStore-CxjHbFPA.js +16 -0
- package/web/assets/{vennDiagram-4TSXK5OY-CDRUEJ_c.js → vennDiagram-4TSXK5OY-DLzOr3Yv.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-CjZ5JazT.js → wardley-WFR3VGLG-LCC8FWVb.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-COsxLnU6.js → wardleyDiagram-VM6X3IG4-WrwzM3YW.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-BfwzNIP-.js → xychartDiagram-S5SC5T6Z-B_HuoAnl.js} +3 -3
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/dist/ephemeral-exec.js +0 -108
- package/dist/hosted-custody.js +0 -30
- package/web/assets/AutomationsView-Dk1aHviY.js +0 -51
- package/web/assets/ChangesCard-Bn37dRew.js +0 -4
- package/web/assets/LibraryView-CGkpNOgR.js +0 -2
- package/web/assets/Settings-DJjEVwmu.js +0 -10
- package/web/assets/arc-Pry-7G1_.js +0 -1
- package/web/assets/architecture-7GRP2DOG-kbFZ_7eU.js +0 -1
- package/web/assets/array-BifhSqXX.js +0 -1
- package/web/assets/channel-CpJ3OYta.js +0 -1
- package/web/assets/chunk-75Z2AOVW-CGlA2AB-.js +0 -2
- package/web/assets/chunk-F27PBJKO-Cto7X88Z.js +0 -1
- package/web/assets/chunk-XXDRQBXY-DqAYeljN.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BSqiC-6r.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BSqiC-6r.js +0 -1
- package/web/assets/cynefinDiagram-5FMLGOSQ-B2jZZ_es.js +0 -62
- package/web/assets/diagram-UQ7AKVKN-D7BIW4QU.js +0 -41
- package/web/assets/diagram-VX7I27RA-qmkf9rFQ.js +0 -24
- package/web/assets/diagram-Z3DM3KII-BZf2h-T8.js +0 -24
- package/web/assets/dist-v5Q1xZ2K.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-DHYpZdDY.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-BdNS-1c7.js +0 -1
- package/web/assets/ganttDiagram-EL5Y4UJY-DGMQzJ8U.js +0 -292
- package/web/assets/gitGraphDiagram-WWUBYQGX-DNfbdhPa.js +0 -106
- package/web/assets/index-kWXUrhG8.css +0 -1
- package/web/assets/infoDiagram-27XIBGKW-QcpEFiaQ.js +0 -2
- package/web/assets/init-C-OQMol4.js +0 -1
- package/web/assets/line-Cq9nN3X2.js +0 -1
- package/web/assets/linear-SzVmGSw4.js +0 -1
- package/web/assets/mount-C9EzYXqA.js +0 -31
- package/web/assets/ordinal-BDEzSJ7C.js +0 -1
- package/web/assets/path-fybaL0A-.js +0 -1
- package/web/assets/railroad-abnf-HS5TGJTU-DiQLjR_J.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-D4bSTzXb.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-DDhl4AMh.js +0 -1
- package/web/assets/src-B6xuSHsQ.js +0 -1
- package/web/assets/stateDiagram-D77RDMKH-_fkXrLZe.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-Cuo-j5c1.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-DOpCt5KW.js +0 -8
- 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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3597
|
-
|
|
3598
|
-
//
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
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
|
|
3796
|
-
let
|
|
3797
|
-
if (
|
|
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
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
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
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
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
|
-
|
|
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.
|
|
3875
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
3911
|
-
//
|
|
3912
|
-
//
|
|
3913
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
8134
|
-
// (``)
|
|
8135
|
-
//
|
|
8136
|
-
//
|
|
8137
|
-
//
|
|
8138
|
-
//
|
|
8040
|
+
// A finalized assistant message may reference an image via markdown — a
|
|
8041
|
+
// remote URL (``) to fetch, or a file the agent just
|
|
8042
|
+
// produced in its workspace (``) 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
|
-
|
|
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
|
-
|
|
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
|
});
|