borgmcp 1.1.15 → 2.0.2
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/CONTRIBUTING.md +15 -0
- package/LICENSE +201 -33
- package/NOTICE +2 -0
- package/README.md +85 -36
- package/SECURITY.md +7 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +63 -1
- package/dist/agent-runtime.js.map +1 -0
- package/dist/assimilate-cmd.d.ts +96 -17
- package/dist/assimilate-cmd.d.ts.map +1 -0
- package/dist/assimilate-cmd.js +1396 -48
- package/dist/assimilate-cmd.js.map +1 -0
- package/dist/assimilate-deps.d.ts.map +1 -0
- package/dist/assimilate-deps.js +384 -3
- package/dist/assimilate-deps.js.map +1 -0
- package/dist/assimilate-guard.d.ts +5 -7
- package/dist/assimilate-guard.d.ts.map +1 -0
- package/dist/assimilate-guard.js +60 -2
- package/dist/assimilate-guard.js.map +1 -0
- package/dist/assimilate-welcome.d.ts +1 -1
- package/dist/assimilate-welcome.d.ts.map +1 -0
- package/dist/assimilate-welcome.js +32 -2
- package/dist/assimilate-welcome.js.map +1 -0
- package/dist/auth-env.d.ts +7 -50
- package/dist/auth-env.d.ts.map +1 -0
- package/dist/auth-env.js +22 -1
- package/dist/auth-env.js.map +1 -0
- package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
- package/dist/backends/launch-all-pastelist.js +11 -4
- package/dist/backends/launch-all-pastelist.js.map +1 -0
- package/dist/backends/launch-all-tmux.d.ts.map +1 -0
- package/dist/backends/launch-all-tmux.js +49 -1
- package/dist/backends/launch-all-tmux.js.map +1 -0
- package/dist/backends/launch-all-windows.d.ts.map +1 -0
- package/dist/backends/launch-all-windows.js +86 -12
- package/dist/backends/launch-all-windows.js.map +1 -0
- package/dist/bare-launch-menu.d.ts.map +1 -0
- package/dist/bare-launch-menu.js +83 -6
- package/dist/bare-launch-menu.js.map +1 -0
- package/dist/claude-launch-args.d.ts.map +1 -0
- package/dist/claude-launch-args.js +34 -1
- package/dist/claude-launch-args.js.map +1 -0
- package/dist/claude.d.ts.map +1 -0
- package/dist/claude.js +486 -14
- package/dist/claude.js.map +1 -0
- package/dist/cleanup-cmd.d.ts +5 -5
- package/dist/cleanup-cmd.d.ts.map +1 -0
- package/dist/cleanup-cmd.js +453 -14
- package/dist/cleanup-cmd.js.map +1 -0
- package/dist/clear-rewake-core.d.ts.map +1 -0
- package/dist/clear-rewake-core.js +23 -2
- package/dist/clear-rewake-core.js.map +1 -0
- package/dist/clear-rewake.d.ts.map +1 -0
- package/dist/clear-rewake.js +19 -1
- package/dist/clear-rewake.js.map +1 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +104 -56
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-platform.d.ts.map +1 -0
- package/dist/cli-platform.js +106 -4
- package/dist/cli-platform.js.map +1 -0
- package/dist/cli-tool-approval.d.ts +84 -0
- package/dist/cli-tool-approval.d.ts.map +1 -0
- package/dist/cli-tool-approval.js +583 -0
- package/dist/cli-tool-approval.js.map +1 -0
- package/dist/codex-app-server.d.ts.map +1 -0
- package/dist/codex-app-server.js +228 -4
- package/dist/codex-app-server.js.map +1 -0
- package/dist/codex-app-wake.d.ts +9 -11
- package/dist/codex-app-wake.d.ts.map +1 -0
- package/dist/codex-app-wake.js +436 -2
- package/dist/codex-app-wake.js.map +1 -0
- package/dist/codex-launch.d.ts.map +1 -0
- package/dist/codex-launch.js +102 -2
- package/dist/codex-launch.js.map +1 -0
- package/dist/codex-remote.d.ts +6 -1
- package/dist/codex-remote.d.ts.map +1 -0
- package/dist/codex-remote.js +394 -1
- package/dist/codex-remote.js.map +1 -0
- package/dist/codex-wake-resolve.d.ts +4 -4
- package/dist/codex-wake-resolve.d.ts.map +1 -0
- package/dist/codex-wake-resolve.js +127 -1
- package/dist/codex-wake-resolve.js.map +1 -0
- package/dist/config-utils.d.ts +2 -2
- package/dist/config-utils.d.ts.map +1 -0
- package/dist/config-utils.js +766 -3
- package/dist/config-utils.js.map +1 -0
- package/dist/config.d.ts +98 -61
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +426 -1
- package/dist/config.js.map +1 -0
- package/dist/console-prefix.d.ts.map +1 -0
- package/dist/console-prefix.js +96 -1
- package/dist/console-prefix.js.map +1 -0
- package/dist/cube-name.d.ts.map +1 -0
- package/dist/cube-name.js +67 -1
- package/dist/cube-name.js.map +1 -0
- package/dist/cubes.d.ts +126 -18
- package/dist/cubes.d.ts.map +1 -0
- package/dist/cubes.js +412 -4
- package/dist/cubes.js.map +1 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +71 -1
- package/dist/debug.js.map +1 -0
- package/dist/direct-log.d.ts.map +1 -0
- package/dist/direct-log.js +11 -1
- package/dist/direct-log.js.map +1 -0
- package/dist/docs-sections.d.ts +11 -10
- package/dist/docs-sections.d.ts.map +1 -0
- package/dist/docs-sections.js +113 -3
- package/dist/docs-sections.js.map +1 -0
- package/dist/drone-lifecycle.d.ts +5 -21
- package/dist/drone-lifecycle.d.ts.map +1 -0
- package/dist/drone-lifecycle.js +57 -8
- package/dist/drone-lifecycle.js.map +1 -0
- package/dist/drone-management.d.ts +29 -0
- package/dist/drone-management.d.ts.map +1 -0
- package/dist/drone-management.js +109 -0
- package/dist/drone-management.js.map +1 -0
- package/dist/ensure-mcp-config.d.ts.map +1 -0
- package/dist/ensure-mcp-config.js +36 -1
- package/dist/ensure-mcp-config.js.map +1 -0
- package/dist/evict-drone.d.ts +10 -2
- package/dist/evict-drone.d.ts.map +1 -0
- package/dist/evict-drone.js +60 -1
- package/dist/evict-drone.js.map +1 -0
- package/dist/gc-orphan-inboxes.d.ts.map +1 -0
- package/dist/gc-orphan-inboxes.js +177 -1
- package/dist/gc-orphan-inboxes.js.map +1 -0
- package/dist/inbox-monitor.d.ts +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -0
- package/dist/inbox-monitor.js +856 -3
- package/dist/inbox-monitor.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1034 -37
- package/dist/index.js.map +1 -0
- package/dist/launch-all-cmd.d.ts.map +1 -0
- package/dist/launch-all-cmd.js +377 -37
- package/dist/launch-all-cmd.js.map +1 -0
- package/dist/launch-all-command.d.ts.map +1 -0
- package/dist/launch-all-command.js +24 -1
- package/dist/launch-all-command.js.map +1 -0
- package/dist/launch-all-deps.d.ts +3 -8
- package/dist/launch-all-deps.d.ts.map +1 -0
- package/dist/launch-all-deps.js +99 -1
- package/dist/launch-all-deps.js.map +1 -0
- package/dist/launch-all-discovery.d.ts +1 -0
- package/dist/launch-all-discovery.d.ts.map +1 -0
- package/dist/launch-all-discovery.js +90 -4
- package/dist/launch-all-discovery.js.map +1 -0
- package/dist/launch-all-locks.d.ts.map +1 -0
- package/dist/launch-all-locks.js +54 -1
- package/dist/launch-all-locks.js.map +1 -0
- package/dist/launch-gate.d.ts +2 -2
- package/dist/launch-gate.d.ts.map +1 -0
- package/dist/launch-gate.js +73 -1
- package/dist/launch-gate.js.map +1 -0
- package/dist/lifecycle-log-guard.d.ts.map +1 -0
- package/dist/lifecycle-log-guard.js +93 -2
- package/dist/lifecycle-log-guard.js.map +1 -0
- package/dist/list-roles-render.d.ts.map +1 -0
- package/dist/list-roles-render.js +40 -6
- package/dist/list-roles-render.js.map +1 -0
- package/dist/local-log-routing.d.ts +19 -0
- package/dist/local-log-routing.d.ts.map +1 -0
- package/dist/local-log-routing.js +67 -0
- package/dist/local-log-routing.js.map +1 -0
- package/dist/local-manage-tool-result.d.ts +8 -0
- package/dist/local-manage-tool-result.d.ts.map +1 -0
- package/dist/local-manage-tool-result.js +10 -0
- package/dist/local-manage-tool-result.js.map +1 -0
- package/dist/local-server-cursor.d.ts +34 -0
- package/dist/local-server-cursor.d.ts.map +1 -0
- package/dist/local-server-cursor.js +151 -0
- package/dist/local-server-cursor.js.map +1 -0
- package/dist/log-audit.d.ts.map +1 -0
- package/dist/log-audit.js +194 -3
- package/dist/log-audit.js.map +1 -0
- package/dist/log-stream.d.ts +35 -18
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +1270 -12
- package/dist/log-stream.js.map +1 -0
- package/dist/model-presets.d.ts.map +1 -0
- package/dist/model-presets.js +19 -1
- package/dist/model-presets.js.map +1 -0
- package/dist/name-validator.d.ts.map +1 -0
- package/dist/name-validator.js +22 -1
- package/dist/name-validator.js.map +1 -0
- package/dist/opencode-drone.d.ts.map +1 -0
- package/dist/opencode-drone.js +438 -4
- package/dist/opencode-drone.js.map +1 -0
- package/dist/opencode-plugin.d.ts +1 -1
- package/dist/opencode-plugin.d.ts.map +1 -0
- package/dist/opencode-plugin.js +29 -4
- package/dist/opencode-plugin.js.map +1 -0
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -0
- package/dist/parse-assimilate-args.js +125 -1
- package/dist/parse-assimilate-args.js.map +1 -0
- package/dist/parse-launch-all-args.d.ts.map +1 -0
- package/dist/parse-launch-all-args.js +70 -1
- package/dist/parse-launch-all-args.js.map +1 -0
- package/dist/postinstall-banner.d.ts.map +1 -0
- package/dist/postinstall-banner.js +27 -2
- package/dist/postinstall-banner.js.map +1 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +32 -5
- package/dist/postinstall.js.map +1 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +9 -1
- package/dist/readiness-probe.js.map +1 -0
- package/dist/regen-format.d.ts.map +1 -0
- package/dist/regen-format.js +545 -13
- package/dist/regen-format.js.map +1 -0
- package/dist/regen.d.ts.map +1 -0
- package/dist/regen.js +141 -5
- package/dist/regen.js.map +1 -0
- package/dist/remote-client.d.ts +88 -173
- package/dist/remote-client.d.ts.map +1 -0
- package/dist/remote-client.js +997 -1
- package/dist/remote-client.js.map +1 -0
- package/dist/reset-local-seat-cmd.d.ts +50 -0
- package/dist/reset-local-seat-cmd.d.ts.map +1 -0
- package/dist/reset-local-seat-cmd.js +169 -0
- package/dist/reset-local-seat-cmd.js.map +1 -0
- package/dist/role-match.d.ts.map +1 -0
- package/dist/role-match.js +23 -1
- package/dist/role-match.js.map +1 -0
- package/dist/role-resolver.d.ts.map +1 -0
- package/dist/role-resolver.js +60 -1
- package/dist/role-resolver.js.map +1 -0
- package/dist/roster-render.d.ts.map +1 -0
- package/dist/roster-render.js +121 -3
- package/dist/roster-render.js.map +1 -0
- package/dist/seat-probe.d.ts +32 -13
- package/dist/seat-probe.d.ts.map +1 -0
- package/dist/seat-probe.js +78 -1
- package/dist/seat-probe.js.map +1 -0
- package/dist/seat-store.d.ts +76 -0
- package/dist/seat-store.d.ts.map +1 -0
- package/dist/seat-store.js +309 -0
- package/dist/seat-store.js.map +1 -0
- package/dist/seats.d.ts +375 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +670 -0
- package/dist/seats.js.map +1 -0
- package/dist/self-path.d.ts +27 -0
- package/dist/self-path.d.ts.map +1 -0
- package/dist/self-path.js +43 -0
- package/dist/self-path.js.map +1 -0
- package/dist/server-endpoint.d.ts +9 -0
- package/dist/server-endpoint.d.ts.map +1 -0
- package/dist/server-endpoint.js +38 -0
- package/dist/server-endpoint.js.map +1 -0
- package/dist/server-errors.d.ts +50 -0
- package/dist/server-errors.d.ts.map +1 -0
- package/dist/server-errors.js +79 -0
- package/dist/server-errors.js.map +1 -0
- package/dist/server-handshake.d.ts +197 -0
- package/dist/server-handshake.d.ts.map +1 -0
- package/dist/server-handshake.js +533 -0
- package/dist/server-handshake.js.map +1 -0
- package/dist/server-response.d.ts +3 -0
- package/dist/server-response.d.ts.map +1 -0
- package/dist/server-response.js +67 -0
- package/dist/server-response.js.map +1 -0
- package/dist/server-trust.d.ts +14 -0
- package/dist/server-trust.d.ts.map +1 -0
- package/dist/server-trust.js +225 -0
- package/dist/server-trust.js.map +1 -0
- package/dist/session-continuity.d.ts +33 -0
- package/dist/session-continuity.d.ts.map +1 -0
- package/dist/session-continuity.js +220 -0
- package/dist/session-continuity.js.map +1 -0
- package/dist/setup-confirm.d.ts +3 -3
- package/dist/setup-confirm.d.ts.map +1 -0
- package/dist/setup-confirm.js +126 -2
- package/dist/setup-confirm.js.map +1 -0
- package/dist/setup.d.ts +5 -3
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +169 -46
- package/dist/setup.js.map +1 -0
- package/dist/shell-escape.d.ts.map +1 -0
- package/dist/shell-escape.js +22 -1
- package/dist/shell-escape.js.map +1 -0
- package/dist/spawn.d.ts +2 -2
- package/dist/spawn.d.ts.map +1 -0
- package/dist/spawn.js +29 -10
- package/dist/spawn.js.map +1 -0
- package/dist/startup-services.d.ts +0 -1
- package/dist/startup-services.d.ts.map +1 -0
- package/dist/startup-services.js +26 -1
- package/dist/startup-services.js.map +1 -0
- package/dist/stream-owner.d.ts +6 -2
- package/dist/stream-owner.d.ts.map +1 -0
- package/dist/stream-owner.js +547 -2
- package/dist/stream-owner.js.map +1 -0
- package/dist/stream-status.d.ts +1 -1
- package/dist/stream-status.d.ts.map +1 -0
- package/dist/stream-status.js +268 -3
- package/dist/stream-status.js.map +1 -0
- package/dist/sync-roles-render.d.ts.map +1 -0
- package/dist/sync-roles-render.js +118 -3
- package/dist/sync-roles-render.js.map +1 -0
- package/dist/sync.d.ts +1 -1
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +286 -22
- package/dist/sync.js.map +1 -0
- package/dist/terminal-title.d.ts.map +1 -0
- package/dist/terminal-title.js +68 -1
- package/dist/terminal-title.js.map +1 -0
- package/dist/token-store.d.ts +17 -111
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +81 -1
- package/dist/token-store.js.map +1 -0
- package/dist/tool-manifest.d.ts +4 -8
- package/dist/tool-manifest.d.ts.map +1 -0
- package/dist/tool-manifest.js +554 -1
- package/dist/tool-manifest.js.map +1 -0
- package/dist/tool-scope.d.ts +8 -12
- package/dist/tool-scope.d.ts.map +1 -0
- package/dist/tool-scope.js +97 -1
- package/dist/tool-scope.js.map +1 -0
- package/dist/types.d.ts +1 -5
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -0
- package/dist/unknown-subcommand.js +37 -1
- package/dist/unknown-subcommand.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +78 -2
- package/dist/version.js.map +1 -0
- package/dist/working-repo.d.ts.map +1 -0
- package/dist/working-repo.js +81 -1
- package/dist/working-repo.js.map +1 -0
- package/dist/worktree-lifecycle.d.ts.map +1 -0
- package/dist/worktree-lifecycle.js +205 -2
- package/dist/worktree-lifecycle.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +42 -0
- package/docs/LOCAL_SERVER.md +103 -0
- package/docs/RELEASING.md +129 -0
- package/package.json +52 -34
- package/src/agent-runtime.ts +69 -0
- package/src/assimilate-cmd.ts +1864 -0
- package/src/assimilate-deps.ts +444 -0
- package/src/assimilate-guard.ts +84 -0
- package/src/assimilate-welcome.ts +39 -0
- package/src/auth-env.ts +21 -0
- package/src/backends/launch-all-pastelist.ts +18 -0
- package/src/backends/launch-all-tmux.ts +75 -0
- package/src/backends/launch-all-windows.ts +110 -0
- package/src/bare-launch-menu.ts +148 -0
- package/src/claude-launch-args.ts +35 -0
- package/src/claude.ts +561 -0
- package/src/cleanup-cmd.ts +581 -0
- package/src/clear-rewake-core.ts +34 -0
- package/src/clear-rewake.ts +18 -0
- package/src/cli-help.ts +116 -0
- package/src/cli-platform.ts +127 -0
- package/src/cli-tool-approval.ts +716 -0
- package/src/codex-app-server.ts +238 -0
- package/src/codex-app-wake.ts +524 -0
- package/src/codex-launch.ts +140 -0
- package/src/codex-remote.ts +500 -0
- package/src/codex-wake-resolve.ts +155 -0
- package/src/config-utils.ts +811 -0
- package/src/config.ts +595 -0
- package/src/console-prefix.ts +99 -0
- package/src/cube-name.ts +69 -0
- package/src/cubes.ts +583 -0
- package/src/debug.ts +73 -0
- package/src/direct-log.ts +9 -0
- package/src/docs-sections.ts +129 -0
- package/src/drone-lifecycle.ts +62 -0
- package/src/drone-management.ts +162 -0
- package/src/ensure-mcp-config.ts +54 -0
- package/src/evict-drone.ts +82 -0
- package/src/gc-orphan-inboxes.ts +229 -0
- package/src/inbox-monitor.ts +1005 -0
- package/src/index.ts +1201 -0
- package/src/launch-all-cmd.ts +450 -0
- package/src/launch-all-command.ts +30 -0
- package/src/launch-all-deps.ts +188 -0
- package/src/launch-all-discovery.ts +118 -0
- package/src/launch-all-locks.ts +74 -0
- package/src/launch-gate.ts +80 -0
- package/src/lifecycle-log-guard.ts +144 -0
- package/src/list-roles-render.ts +52 -0
- package/src/local-log-routing.ts +106 -0
- package/src/local-manage-tool-result.ts +10 -0
- package/src/local-server-cursor.ts +188 -0
- package/src/log-audit.ts +207 -0
- package/src/log-stream.ts +1567 -0
- package/src/model-presets.ts +22 -0
- package/src/name-validator.ts +23 -0
- package/src/opencode-drone.ts +528 -0
- package/src/opencode-plugin.ts +38 -0
- package/src/parse-assimilate-args.ts +125 -0
- package/src/parse-launch-all-args.ts +100 -0
- package/src/postinstall-banner.ts +37 -0
- package/src/postinstall.ts +36 -0
- package/src/readiness-probe.ts +12 -0
- package/src/regen-format.ts +651 -0
- package/src/regen.ts +150 -0
- package/src/remote-client.ts +1457 -0
- package/src/reset-local-seat-cmd.ts +227 -0
- package/src/role-match.ts +29 -0
- package/src/role-resolver.ts +93 -0
- package/src/roster-render.ts +196 -0
- package/src/seat-probe.ts +115 -0
- package/src/seat-store.ts +333 -0
- package/src/seats.ts +911 -0
- package/src/self-path.ts +51 -0
- package/src/server-endpoint.ts +38 -0
- package/src/server-errors.ts +95 -0
- package/src/server-handshake.ts +816 -0
- package/src/server-response.ts +76 -0
- package/src/server-trust.ts +245 -0
- package/src/session-continuity.ts +280 -0
- package/src/setup-confirm.ts +171 -0
- package/src/setup.ts +186 -0
- package/src/shell-escape.ts +22 -0
- package/src/spawn.ts +31 -0
- package/src/startup-services.ts +36 -0
- package/src/stream-owner.ts +686 -0
- package/src/stream-status.ts +337 -0
- package/src/sync-roles-render.ts +163 -0
- package/src/sync.ts +363 -0
- package/src/terminal-title.ts +76 -0
- package/src/token-store.ts +102 -0
- package/src/tool-manifest.ts +610 -0
- package/src/tool-scope.ts +110 -0
- package/src/types.ts +5 -0
- package/src/unknown-subcommand.ts +35 -0
- package/src/version.ts +82 -0
- package/src/working-repo.ts +99 -0
- package/src/worktree-lifecycle.ts +254 -0
- package/dist/auth-recovery.d.ts +0 -33
- package/dist/auth-recovery.js +0 -1
- package/dist/auth.d.ts +0 -103
- package/dist/auth.js +0 -39
- package/dist/device-auth.d.ts +0 -75
- package/dist/device-auth.js +0 -1
- package/dist/drone-address.d.ts +0 -26
- package/dist/drone-address.js +0 -1
- package/dist/get-started.d.ts +0 -20
- package/dist/get-started.js +0 -2
- package/dist/health-beat.d.ts +0 -134
- package/dist/health-beat.js +0 -1
- package/dist/role-section.d.ts +0 -110
- package/dist/role-section.js +0 -8
- package/dist/setup-action.d.ts +0 -20
- package/dist/setup-action.js +0 -1
- package/dist/stale-version-check.d.ts +0 -40
- package/dist/stale-version-check.js +0 -1
- package/dist/subscription-retry.d.ts +0 -40
- package/dist/subscription-retry.js +0 -1
- package/dist/templates.d.ts +0 -107
- package/dist/templates.js +0 -359
- package/dist/token-crypto.d.ts +0 -50
- package/dist/token-crypto.js +0 -1
package/dist/remote-client.js
CHANGED
|
@@ -1 +1,997 @@
|
|
|
1
|
-
import{getIdToken as f,getRefreshToken as m,clearTokens as h}from"./config.js";import{refreshIdToken as S,RefreshTokenInvalidError as g,RefreshTransientError as T}from"./auth.js";import{consolePrefix as j}from"./console-prefix.js";import{debugLog as E}from"./debug.js";import{assertUuidShape as x}from"./evict-drone.js";import{DroneEvictedError as k,DroneFrozenError as C,DRONE_EVICTED_CODE as P,DRONE_FROZEN_CODE as O,errorCodeFromBody as I}from"./drone-lifecycle.js";import{canonicalizeWorkingRepoIdentity as D}from"./working-repo.js";const L=/^(claude|ollama):[A-Za-z0-9._:\/-]+$/,A=process.env.BORG_API_URL||"https://api.borgmcp.ai",G=3,J=6e4;let l=null;function _(e){return l||(l=S(e).finally(()=>{l=null}),l)}function M(e){if(e==null)return null;const t=e.trim();return/^\d+$/.test(t)?parseInt(t,10)*1e3:null}function v(e,t,n=J,o=()=>Math.random()*500){const r=e??1e3*(t+1);return Math.min(r,n)+o()}function U(e){const t=(n,o)=>`${n}${/[.:!?]$/.test(n)?"":":"} ${o}`;try{const n=JSON.parse(e);if(typeof n?.error=="string")return typeof n.details=="string"?t(n.error,n.details):n.error;if(n?.error&&typeof n.error=="object"){const o=n.error.message,r=n.error.details??n.details;if(typeof o=="string"&&typeof r=="string")return t(o,r);if(typeof o=="string")return o}if(typeof n?.message=="string"&&typeof n?.details=="string")return t(n.message,n.details);if(typeof n?.message=="string")return n.message}catch{}return e}async function N(e,t,n){const o=n.maxRetries??G;let r=e,i=0;for(;r.status===429&&i<o;){const p=v(M(r.headers.get("Retry-After")),i,n.capMs,n.jitter);n.log?.(`rate limited (429); retrying in ${Math.round(p)}ms (attempt ${i+1}/${o})`),await n.sleep(p),i++,r=await t()}return r}function q(e){return new Promise(t=>setTimeout(t,e))}async function B(){let e=await f();if(!e){const t=await m(),n=t!=null;if(t)try{await _(t),e=await f()}catch(o){if(o instanceof g)await h();else throw o instanceof T?o:new T(`Token refresh failed unexpectedly (your saved login was NOT cleared \u2014 retry; if it persists, restart the borg session): ${o?.message??"unknown"}`)}if(!e)throw new Error(n?"Authentication expired \u2014 your saved login has expired. Run: borg setup":"Authentication required \u2014 you are not signed in. Run: borg setup")}return e}async function F(){const e=await m();if(!e)return null;try{return await _(e),await f()}catch(t){if(t instanceof g&&await h(),t instanceof T)throw t;return null}}async function Q(){if(await f())return"valid";const t=await m();if(!t)return"dead";try{return await _(t),await f()?"valid":"transient"}catch(n){return n instanceof g?(await h(),"dead"):"transient"}}async function s(e,t={}){let n=await B();const{droneSession:o,apiUrl:r,headers:i,...p}=t,$=r??A,y=(p.method??"GET").toUpperCase(),w=async c=>{const d={Authorization:`Bearer ${c}`,...i};o&&(d["X-Drone-Session"]=o),E(`\u2192 ${y} ${e}`);const u=await fetch(`${$}${e}`,{...p,headers:d});return E(`\u2190 ${u.status} ${y} ${e}`),u};let a=await w(n);if(a.status===401){const c=await F();c&&(n=c,a=await w(n))}if(a.status===401)throw new Error("Authentication required. Run: borg setup");if(a.status===429&&(a=await N(a,()=>w(n),{sleep:q,log:c=>console.error(`${j()}${c}`)})),!a.ok){const c=await a.text();E(`\u2717 ${a.status} ${y} ${e}: ${c}`);const d=U(c),u=I(c);if(a.status===410&&u===P)throw new k(d);if(a.status===423&&u===O)throw new C(d);if(a.status===429){const b=a.headers.get("Retry-After"),R=b?` (retry after ${b}s)`:"";throw new Error(`HTTP 429: rate limited${R}: ${d}`)}throw new Error(`HTTP ${a.status}: ${d}`)}return a}async function K(e,t,n,o){const r={hostname:n??null};if((o==="claude"||o==="codex"||o==="opencode")&&(r.agent_kind=o),typeof e=="string")r.cube_name=e;else if(e.cube_id&&(r.cube_id=e.cube_id),e.cube_name&&(r.cube_name=e.cube_name),e.role_id&&(r.role_id=e.role_id),e.role_name&&(r.role_name=e.role_name),e.prior_drone_id&&(r.prior_drone_id=e.prior_drone_id),e.model!=null){if(!L.test(e.model))throw new Error(`Invalid model descriptor: "${e.model}" (expected "<claude|ollama>:<model>").`);r.model=e.model}return await(await s("/api/assimilate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r),apiUrl:t})).json()}async function ee(e,t){return await(await s("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:t})).json()}async function ne(e,t){return await(await s("/api/drone/role",{method:"GET",droneSession:e,apiUrl:t})).json()}async function te(e,t,n){const o=new URLSearchParams({role:n});return await(await s(`/api/drone/role?${o.toString()}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function oe(e,t){return await(await s("/api/drone/whoami",{method:"GET",droneSession:e,apiUrl:t})).json()}async function re(e,t,n){const o=n?`?since=${encodeURIComponent(n)}`:"";return await(await s(`/api/drone/roster${o}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function se(e,t,n={}){const o=new URLSearchParams;n.since&&o.set("since",n.since),n.limit!==void 0&&o.set("limit",String(n.limit)),n.unreadOnly&&o.set("unread_only","true");const r=o.toString();return await(await s(`/api/drone/log${r?`?${r}`:""}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function ie(e,t,n,o="ack"){await s(`/api/drone/log/${n}/ack`,{method:"POST",body:JSON.stringify({kind:o}),droneSession:e,apiUrl:t})}async function ae(e,t,n){return await(await s("/api/drone/decide",{method:"POST",body:JSON.stringify(n),droneSession:e,apiUrl:t})).json()}async function ce(e,t,n){const o=n?`?topic=${encodeURIComponent(n)}`:"";return await(await s(`/api/drone/decisions${o}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function pe(e,t,n){return await(await s("/api/drone/decisions",{method:"DELETE",body:JSON.stringify(n),droneSession:e,apiUrl:t})).json()}async function de(e,t,n={}){const o=new URLSearchParams;if(n.since&&o.set("since",n.since),n.reportedModel&&o.set("reported_model",n.reportedModel),n.workingRepo){o.set("working_repo_reported","1");const p=n.workingRepo.origin?D(n.workingRepo.origin):null;p&&o.set("working_repo_origin",p)}const r=o.toString();return await(await s(`/api/drone/regen${r?`?${r}`:""}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function ue(e,t,n,o){const r=new URLSearchParams({role:n,section:o});return await(await s(`/api/drone/role-rationale?${r.toString()}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function fe(e,t,n,o={}){const r={message:n,...o.visibility?{visibility:o.visibility}:{},...o.recipientDroneIds?{recipientDroneIds:o.recipientDroneIds}:{},...o.class?{class:o.class}:{},...o.to?{to:o.to}:{}};return await(await s("/api/drone/log",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:t,body:JSON.stringify(r)})).json()}async function le(e,t,n){const o={kind:n.kind??"friction",message:n.message,...n.metadata?{metadata:n.metadata}:{}};return await(await s("/api/drone/report",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:t,body:JSON.stringify(o)})).json()}async function ye(){const e=await s("/api/reports",{method:"GET"});if(e.status===403)return{forbidden:!0};if(!e.ok)throw new Error(`Failed to fetch reports: ${e.status}`);return{forbidden:!1,reports:(await e.json()).reports}}async function we(){return await(await s("/api/cubes",{method:"GET"})).json()}async function me(){return await(await s("/api/templates",{method:"GET"})).json()}async function he(e,t,n){const o={cube_directive:t};e&&(o.name=e),n?.template&&(o.template=n.template),n&&Object.prototype.hasOwnProperty.call(n,"message_taxonomy")&&(o.message_taxonomy=n.message_taxonomy??null);const i=await(await s("/api/cubes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();return i.cube?{...i.cube,roles:i.roles??[],drones:i.drones??[]}:i}async function ge(e,t){return await(await s(`/api/cubes/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Te(e,t){return await(await s(`/api/cubes/${e}/taxonomy-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Ee(e){await s(`/api/cubes/${e}`,{method:"DELETE"})}async function _e(e,t){return await(await s(`/api/cubes/${e}/roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function be(e,t){return await(await s(`/api/roles/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function xe(e,t){return await(await s(`/api/roles/${e}/section-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function $e(e){await s(`/api/roles/${e}`,{method:"DELETE"})}async function Re(e,t){return x(e,"drone_id"),await(await s(`/api/drones/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role_id:t})})).json()}async function Se(e){x(e,"drone_id"),await s(`/api/drones/${e}`,{method:"DELETE"})}async function je(e){const n=await(await s(`/api/cubes/${e}`,{method:"GET"})).json();return n.cube?{...n.cube,roles:n.roles??[],drones:n.drones??[]}:n}async function ke(e,t){return await(await s(`/api/cubes/${e}/apply-template`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:t})})).json()}async function Ce(){return await(await s("/api/subscription/status",{method:"GET"})).json()}async function Pe(e,t="software-dev",n=!1,o){return await(await s(`/api/cubes/${e}/sync-roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:t,apply:n,...o?{decisions:o}:{}})})).json()}async function Oe(){const t=await(await s("/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!t.checkout_url)throw new Error("No checkout URL in response");return t.checkout_url}async function Ie(){const t=await(await s("/api/subscription/portal",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!t.portal_url)throw new Error(t.message||"No portal URL in response");return t.portal_url}export{A as API_URL,ie as ackLogEntry,fe as appendLog,ke as applyTemplate,K as assimilate,Ce as checkSubscriptionStatus,Ie as createBillingPortalSession,he as createCube,_e as createRole,Oe as createSubscription,Ee as deleteCube,$e as deleteRole,Se as evictDrone,U as extractHttpErrorMessage,ye as fetchReports,je as getCube,ee as getCubeInfo,ne as getRoleInfo,te as getRoleInfoByName,re as getRoster,B as getValidToken,we as listCubes,ce as listDecisions,me as listTemplates,M as parseRetryAfterMs,xe as patchRoleSection,Te as patchTaxonomyClass,Q as probeSession,v as rateLimitWaitMs,se as readLog,Re as reassignDrone,ae as recordDecision,de as regen,pe as removeDecision,N as retryOn429,ue as roleRationale,le as submitReport,Pe as syncRoles,ge as updateCube,be as updateRole,oe as whoami};
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for a verified local (self-hosted) Borg server.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Pinned-TLS requests to the selected local server
|
|
6
|
+
* - Drone-session / enrollment-credential injection
|
|
7
|
+
* - Network failure handling with retry + exponential backoff
|
|
8
|
+
*
|
|
9
|
+
* There is no hosted-authority path: every request must carry verified local
|
|
10
|
+
* server trust or it fails closed before any network or credential use.
|
|
11
|
+
*/
|
|
12
|
+
import { getServerCredential, } from './config.js';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { createProtocolEnvelope, decodeEvictDroneResult, decodeProtocolEnvelope, decodeProtocolErrorEnvelope, decodeReassignDroneResult, ErrorCode, } from 'borgmcp-shared/protocol';
|
|
15
|
+
import { debugLog } from './debug.js';
|
|
16
|
+
import { assertUuidShape } from './evict-drone.js';
|
|
17
|
+
import { DroneEvictedError, DRONE_EVICTED_CODE } from './drone-lifecycle.js';
|
|
18
|
+
import { loadBorgServerTrust } from './server-trust.js';
|
|
19
|
+
import { BorgServerError, BorgServerHttpError, BorgServerTrustError, BorgServerUnreachableError, LocalManageCredentialUnavailableError, LocalManageRequiredError, } from './server-errors.js';
|
|
20
|
+
import { getActiveCube } from './cubes.js';
|
|
21
|
+
import { advanceLocalServerCursor, getLocalServerCursor, } from './local-server-cursor.js';
|
|
22
|
+
import { readBoundedResponseBody } from './server-response.js';
|
|
23
|
+
import { resolveLocalLogRecipients } from './local-log-routing.js';
|
|
24
|
+
import { ensureLocalSessionFresh, recoverExpiredLocalSession } from './session-continuity.js';
|
|
25
|
+
// gh#330: honor the server's Retry-After on 429 instead of failing the
|
|
26
|
+
// (often required) coordination signal outright. Bounded so a CLI call
|
|
27
|
+
// never blocks unboundedly; capped per attempt so a large window-reset
|
|
28
|
+
// retryAfter can't wedge the call.
|
|
29
|
+
const RATE_LIMIT_MAX_RETRIES = 3;
|
|
30
|
+
const RATE_LIMIT_MAX_WAIT_MS = 60_000; // cap a single Retry-After honor
|
|
31
|
+
export const LOCAL_SERVER_RESPONSE_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
32
|
+
// A typed auth-error envelope is tiny; anything larger is hostile and the
|
|
33
|
+
// bounded read throws → the 401 fails closed to non-destructive CREDENTIAL_REJECTED.
|
|
34
|
+
const AUTH_ERROR_ENVELOPE_LIMIT_BYTES = 64 * 1024;
|
|
35
|
+
export const LOCAL_SERVER_REQUEST_TIMEOUT_MS = 5_000;
|
|
36
|
+
const LOCAL_SERVER_RESPONSE_LIMIT_MESSAGE = 'Local Borg server response exceeded the response limit';
|
|
37
|
+
/**
|
|
38
|
+
* Parse a `Retry-After` header (delta-seconds form, which the worker
|
|
39
|
+
* emits — mcp-server.ts:382/583) into milliseconds. Returns null when
|
|
40
|
+
* absent or not a non-negative integer count of seconds. (The HTTP-date
|
|
41
|
+
* form is not emitted by the worker, so it is intentionally unhandled.)
|
|
42
|
+
*/
|
|
43
|
+
export function parseRetryAfterMs(headerValue) {
|
|
44
|
+
if (headerValue == null)
|
|
45
|
+
return null;
|
|
46
|
+
const trimmed = headerValue.trim();
|
|
47
|
+
if (!/^\d+$/.test(trimmed))
|
|
48
|
+
return null;
|
|
49
|
+
return parseInt(trimmed, 10) * 1000;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How long to wait before the next 429 retry. Honors the server's
|
|
53
|
+
* Retry-After when present (capped at `capMs` so a full-window reset
|
|
54
|
+
* can't wedge a CLI call); falls back to an escalating 1s·(attempt+1)
|
|
55
|
+
* when absent. Adds jitter (injected for tests) so co-located sibling
|
|
56
|
+
* drones sharing one per-IP bucket don't retry in lockstep.
|
|
57
|
+
*/
|
|
58
|
+
export function rateLimitWaitMs(retryAfterMs, attempt, capMs = RATE_LIMIT_MAX_WAIT_MS, jitter = () => Math.random() * 500) {
|
|
59
|
+
const base = retryAfterMs != null ? retryAfterMs : 1000 * (attempt + 1);
|
|
60
|
+
return Math.min(base, capMs) + jitter();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Given an ALREADY-OBTAINED response, while it is a 429 and retries
|
|
64
|
+
* remain, wait per `rateLimitWaitMs` (honoring the CURRENT response's
|
|
65
|
+
* Retry-After) and THEN re-run `doRequest`. Takes `initialResponse`
|
|
66
|
+
* (not a first request) because the caller has already made the request
|
|
67
|
+
* and read its status — re-fetching first would ignore the first 429's
|
|
68
|
+
* Retry-After and double-fire an immediate extra request (CR blocker
|
|
69
|
+
* d3a564f5). Returns the last Response (200-class on success, or a final
|
|
70
|
+
* 429 if retries exhaust — the caller surfaces that). `sleep` is
|
|
71
|
+
* injected for deterministic tests; no fetch-global mocking required.
|
|
72
|
+
*/
|
|
73
|
+
export async function retryOn429(initialResponse, doRequest, opts) {
|
|
74
|
+
const maxRetries = opts.maxRetries ?? RATE_LIMIT_MAX_RETRIES;
|
|
75
|
+
let response = initialResponse;
|
|
76
|
+
let attempt = 0;
|
|
77
|
+
while (response.status === 429 && attempt < maxRetries) {
|
|
78
|
+
// Honor THIS 429's Retry-After BEFORE issuing the next request.
|
|
79
|
+
const waitMs = rateLimitWaitMs(parseRetryAfterMs(response.headers.get('Retry-After')), attempt, opts.capMs, opts.jitter);
|
|
80
|
+
opts.log?.(`rate limited (429); retrying in ${Math.round(waitMs)}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
81
|
+
await opts.sleep(waitMs);
|
|
82
|
+
attempt++;
|
|
83
|
+
response = await doRequest();
|
|
84
|
+
}
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
async function localAuthorityContext(sessionToken, apiUrl, expectedServerTrustIdentity) {
|
|
88
|
+
const active = await getActiveCube();
|
|
89
|
+
const matched = active?.serverTrustIdentity !== undefined &&
|
|
90
|
+
active.apiUrl === apiUrl &&
|
|
91
|
+
active.sessionToken === sessionToken
|
|
92
|
+
? active
|
|
93
|
+
: null;
|
|
94
|
+
if (!matched) {
|
|
95
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
96
|
+
}
|
|
97
|
+
assertUuidShape(matched.cubeId, 'cube_id');
|
|
98
|
+
assertUuidShape(matched.droneId, 'drone_id');
|
|
99
|
+
if (expectedServerTrustIdentity !== undefined) {
|
|
100
|
+
if (matched.serverTrustIdentity !== expectedServerTrustIdentity) {
|
|
101
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
102
|
+
}
|
|
103
|
+
return matched;
|
|
104
|
+
}
|
|
105
|
+
// Only a hydrated local ActiveCube carrying the verified trust anchor
|
|
106
|
+
// authorizes the request. cubes.json is mutable local state and a
|
|
107
|
+
// legacy-looking sessionToken proves nothing — fail closed before any
|
|
108
|
+
// network use when no verified local authority is present.
|
|
109
|
+
return matched;
|
|
110
|
+
}
|
|
111
|
+
function localUnsupported(capability) {
|
|
112
|
+
throw new Error(`Local Borg server does not support ${capability}`);
|
|
113
|
+
}
|
|
114
|
+
function waitForLocalRequest(promise, signal) {
|
|
115
|
+
if (signal.aborted)
|
|
116
|
+
return Promise.reject(signal.reason);
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const onAbort = () => reject(signal.reason);
|
|
119
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
120
|
+
promise.then(resolve, reject).finally(() => {
|
|
121
|
+
signal.removeEventListener('abort', onAbort);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function decodeLocalProtocolResponse(request, allowNoContent, decodePayload = (value) => value) {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timeout = setTimeout(() => {
|
|
128
|
+
controller.abort(new Error('Local Borg server request timed out'));
|
|
129
|
+
}, LOCAL_SERVER_REQUEST_TIMEOUT_MS);
|
|
130
|
+
try {
|
|
131
|
+
const response = await waitForLocalRequest(request(controller.signal), controller.signal);
|
|
132
|
+
if (response.status === 204 && allowNoContent)
|
|
133
|
+
return null;
|
|
134
|
+
const encoded = await readBoundedResponseBody(response, LOCAL_SERVER_RESPONSE_LIMIT_BYTES, LOCAL_SERVER_RESPONSE_LIMIT_MESSAGE, controller.signal);
|
|
135
|
+
let body;
|
|
136
|
+
try {
|
|
137
|
+
body = JSON.parse(encoded);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
throw new Error('Local Borg server returned an invalid protocol envelope');
|
|
141
|
+
}
|
|
142
|
+
return decodeProtocolEnvelope(body, decodePayload).payload;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (controller.signal.aborted) {
|
|
146
|
+
// CR5: a TYPED transport-timeout verdict (message kept for call-site parity).
|
|
147
|
+
throw new BorgServerUnreachableError('Local Borg server request timed out');
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function localServerRequest(active, path, method, payload) {
|
|
156
|
+
let current = active.localSessionCredentialRef && active.localSessionExpiresAt
|
|
157
|
+
? await ensureLocalSessionFresh(active)
|
|
158
|
+
: active;
|
|
159
|
+
const request = (candidate) => decodeLocalProtocolResponse((signal) => authedFetch(path, {
|
|
160
|
+
method,
|
|
161
|
+
signal,
|
|
162
|
+
droneSession: candidate.sessionToken,
|
|
163
|
+
apiUrl: candidate.apiUrl,
|
|
164
|
+
serverTrustIdentity: candidate.serverTrustIdentity,
|
|
165
|
+
redirect: 'error',
|
|
166
|
+
...(payload === undefined
|
|
167
|
+
? { headers: { Accept: 'application/json' } }
|
|
168
|
+
: {
|
|
169
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
170
|
+
body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
|
|
171
|
+
}),
|
|
172
|
+
}), true);
|
|
173
|
+
try {
|
|
174
|
+
return await request(current);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (!(error instanceof BorgServerError) || error.code !== 'AUTH_EXPIRED')
|
|
178
|
+
throw error;
|
|
179
|
+
if (current.sessionToken !== active.sessionToken)
|
|
180
|
+
throw error;
|
|
181
|
+
current = await recoverExpiredLocalSession(current);
|
|
182
|
+
return request(current);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function manageCopyValue(value) {
|
|
186
|
+
return JSON.stringify(value);
|
|
187
|
+
}
|
|
188
|
+
async function localManageConnection(active, operation) {
|
|
189
|
+
const trustIdentity = active.serverTrustIdentity;
|
|
190
|
+
if (!trustIdentity)
|
|
191
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
192
|
+
let authToken;
|
|
193
|
+
try {
|
|
194
|
+
authToken = await getServerCredential(active.apiUrl, trustIdentity);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new LocalManageCredentialUnavailableError(operation.operation, operation.cubeName, operation.noMutation);
|
|
198
|
+
}
|
|
199
|
+
if (!authToken) {
|
|
200
|
+
throw new LocalManageCredentialUnavailableError(operation.operation, operation.cubeName, operation.noMutation);
|
|
201
|
+
}
|
|
202
|
+
return { apiUrl: active.apiUrl, authToken, serverTrustIdentity: trustIdentity };
|
|
203
|
+
}
|
|
204
|
+
async function localManageRequest(active, path, method, operation, payload, decodePayload) {
|
|
205
|
+
const connection = await localManageConnection(active, operation);
|
|
206
|
+
try {
|
|
207
|
+
return await decodeLocalProtocolResponse((signal) => authedFetch(path, {
|
|
208
|
+
method,
|
|
209
|
+
signal,
|
|
210
|
+
apiUrl: connection.apiUrl,
|
|
211
|
+
authToken: connection.authToken,
|
|
212
|
+
serverTrustIdentity: connection.serverTrustIdentity,
|
|
213
|
+
redirect: 'error',
|
|
214
|
+
...(payload === undefined
|
|
215
|
+
? { headers: { Accept: 'application/json' } }
|
|
216
|
+
: {
|
|
217
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
218
|
+
body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
|
|
219
|
+
}),
|
|
220
|
+
}), true, decodePayload);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof BorgServerHttpError &&
|
|
224
|
+
error.status === 403 &&
|
|
225
|
+
error.code === ErrorCode.ACCESS_DENIED) {
|
|
226
|
+
throw new LocalManageRequiredError(operation.operation, operation.cubeName, operation.noMutation);
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function localConnectionRequest(connection, path) {
|
|
232
|
+
return decodeLocalProtocolResponse((signal) => authedFetch(path, {
|
|
233
|
+
method: 'GET',
|
|
234
|
+
signal,
|
|
235
|
+
apiUrl: connection.apiUrl,
|
|
236
|
+
authToken: connection.authToken,
|
|
237
|
+
serverTrustIdentity: connection.serverTrustIdentity,
|
|
238
|
+
redirect: 'error',
|
|
239
|
+
headers: { Accept: 'application/json' },
|
|
240
|
+
}), false);
|
|
241
|
+
}
|
|
242
|
+
async function localOwnerConnection(connection) {
|
|
243
|
+
if (connection)
|
|
244
|
+
return connection;
|
|
245
|
+
const active = await getActiveCube();
|
|
246
|
+
if (!active?.serverTrustIdentity) {
|
|
247
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
248
|
+
}
|
|
249
|
+
const authToken = await getServerCredential(active.apiUrl, active.serverTrustIdentity);
|
|
250
|
+
if (!authToken)
|
|
251
|
+
throw new Error('No credential is stored for the selected Borg server identity');
|
|
252
|
+
return {
|
|
253
|
+
apiUrl: active.apiUrl,
|
|
254
|
+
authToken,
|
|
255
|
+
serverTrustIdentity: active.serverTrustIdentity,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function localCubeComposition(active) {
|
|
259
|
+
const base = `/api/cubes/${active.cubeId}`;
|
|
260
|
+
const [cubePayload, rolePayload, dronePayload] = await Promise.all([
|
|
261
|
+
localServerRequest(active, base, 'GET'),
|
|
262
|
+
localServerRequest(active, `${base}/roles`, 'GET'),
|
|
263
|
+
localServerRequest(active, `${base}/drones`, 'GET'),
|
|
264
|
+
]);
|
|
265
|
+
if (!cubePayload || !rolePayload || !dronePayload) {
|
|
266
|
+
throw new Error('Local Borg server returned an incomplete cube response');
|
|
267
|
+
}
|
|
268
|
+
const drone = dronePayload.drones.find((candidate) => candidate.id === active.droneId);
|
|
269
|
+
const role = rolePayload.roles.find((candidate) => candidate.id === drone?.role_id);
|
|
270
|
+
if (!drone || !role)
|
|
271
|
+
throw new Error('Local Borg server no longer recognizes this drone seat');
|
|
272
|
+
return {
|
|
273
|
+
cube: cubePayload.cube,
|
|
274
|
+
roles: rolePayload.roles,
|
|
275
|
+
drones: dronePayload.drones,
|
|
276
|
+
role,
|
|
277
|
+
drone,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function localCursorBinding(active) {
|
|
281
|
+
return {
|
|
282
|
+
origin: active.apiUrl,
|
|
283
|
+
trustIdentity: active.serverTrustIdentity,
|
|
284
|
+
cubeId: active.cubeId,
|
|
285
|
+
droneId: active.droneId,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async function localReadLogPage(active, opts = {}) {
|
|
289
|
+
const payload = await localServerRequest(active, `/api/cubes/${active.cubeId}/logs`, 'PUT', {
|
|
290
|
+
cursor: opts.cursor ?? null,
|
|
291
|
+
...(opts.limit === undefined ? {} : { limit: opts.limit }),
|
|
292
|
+
});
|
|
293
|
+
if (!payload)
|
|
294
|
+
throw new Error('Local Borg server returned an empty log response');
|
|
295
|
+
return payload;
|
|
296
|
+
}
|
|
297
|
+
function isPendingWakeEntry(entry, droneId) {
|
|
298
|
+
if (entry.visibility === 'direct') {
|
|
299
|
+
const recipients = Array.isArray(entry.recipient_drone_ids)
|
|
300
|
+
? entry.recipient_drone_ids.filter((recipient) => typeof recipient === 'string')
|
|
301
|
+
: [];
|
|
302
|
+
if (!recipients.includes(droneId))
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
const isHeartbeatPing = typeof entry.message === 'string' && entry.message.startsWith('[HEARTBEAT-PING]');
|
|
306
|
+
return entry.drone_id !== droneId || isHeartbeatPing;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* client#76: inspect authoritative unread log state without advancing the
|
|
310
|
+
* agent-owned unread cursor. The scan mirrors the SSE wake filters: unaddressed
|
|
311
|
+
* direct entries and ordinary own posts are not work for this seat. A full
|
|
312
|
+
* paginated scan prevents a run of skipped entries from hiding later real work.
|
|
313
|
+
*/
|
|
314
|
+
export async function hasPendingWakeActivity(active, deps = {}) {
|
|
315
|
+
if (!active.serverTrustIdentity) {
|
|
316
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
317
|
+
}
|
|
318
|
+
const getCursor = deps.getCursor ?? getLocalServerCursor;
|
|
319
|
+
const readPage = deps.readPage ?? localReadLogPage;
|
|
320
|
+
let cursor = await getCursor(localCursorBinding(active));
|
|
321
|
+
for (;;) {
|
|
322
|
+
const page = await readPage(active, { cursor, limit: 500 });
|
|
323
|
+
if (page.entries.some((entry) => isPendingWakeEntry(entry, active.droneId)))
|
|
324
|
+
return true;
|
|
325
|
+
if (!page.has_more)
|
|
326
|
+
return false;
|
|
327
|
+
if (!page.cursor ||
|
|
328
|
+
(cursor && page.cursor.id === cursor.id && page.cursor.created_at === cursor.created_at)) {
|
|
329
|
+
throw new Error('Local Borg server returned a non-advancing log cursor');
|
|
330
|
+
}
|
|
331
|
+
cursor = page.cursor;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function resolveLocalLogCursor(active, since) {
|
|
335
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
336
|
+
.test(since);
|
|
337
|
+
const timestamp = isUuid ? null : Date.parse(since);
|
|
338
|
+
if (!isUuid && (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== since)) {
|
|
339
|
+
throw new Error('Invalid local Borg server log cursor');
|
|
340
|
+
}
|
|
341
|
+
let scanCursor = null;
|
|
342
|
+
let timestampCursor = null;
|
|
343
|
+
for (;;) {
|
|
344
|
+
const page = await localReadLogPage(active, { cursor: scanCursor, limit: 500 });
|
|
345
|
+
for (const entry of page.entries) {
|
|
346
|
+
if (isUuid && entry.id === since) {
|
|
347
|
+
return { id: entry.id, created_at: entry.created_at };
|
|
348
|
+
}
|
|
349
|
+
if (!isUuid) {
|
|
350
|
+
if (entry.created_at > since)
|
|
351
|
+
return timestampCursor;
|
|
352
|
+
timestampCursor = { id: entry.id, created_at: entry.created_at };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!page.has_more || !page.cursor) {
|
|
356
|
+
if (isUuid)
|
|
357
|
+
throw new Error('Local Borg server log cursor was not found');
|
|
358
|
+
return timestampCursor;
|
|
359
|
+
}
|
|
360
|
+
scanCursor = page.cursor;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Sleep for specified milliseconds
|
|
365
|
+
*/
|
|
366
|
+
function sleep(ms) {
|
|
367
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Authenticated fetch helper.
|
|
371
|
+
*
|
|
372
|
+
* Adds the Bearer token + optional drone-session header, parses errors
|
|
373
|
+
* consistently, and surfaces a re-auth recovery message ("Run: borg setup")
|
|
374
|
+
* on auth failure (gh#780/#794 — never `borg assimilate`, which rides the same
|
|
375
|
+
* broken Bearer).
|
|
376
|
+
*
|
|
377
|
+
* Accepts an optional `apiUrl` override so already-assimilated callers can
|
|
378
|
+
* route to the worker that issued their drone session token, regardless of
|
|
379
|
+
* what BORG_API_URL was set to when this process started.
|
|
380
|
+
*/
|
|
381
|
+
async function authedFetch(path, init = {}) {
|
|
382
|
+
const { droneSession, apiUrl, authToken, serverTrustIdentity: suppliedTrustIdentity, headers, ...rest } = init;
|
|
383
|
+
if (apiUrl === undefined) {
|
|
384
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
385
|
+
}
|
|
386
|
+
const baseUrl = apiUrl;
|
|
387
|
+
if (suppliedTrustIdentity === undefined) {
|
|
388
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
389
|
+
}
|
|
390
|
+
const serverTrustIdentity = suppliedTrustIdentity;
|
|
391
|
+
if (!/^\/api\/cubes(?:\/|$)/.test(path)) {
|
|
392
|
+
localUnsupported(`the ${path} capability`);
|
|
393
|
+
}
|
|
394
|
+
let requestFetch;
|
|
395
|
+
let token;
|
|
396
|
+
{
|
|
397
|
+
const trust = await loadBorgServerTrust(baseUrl);
|
|
398
|
+
if (trust.identity !== serverTrustIdentity) {
|
|
399
|
+
// CR5: a TYPED terminal trust verdict — never inferred from error text.
|
|
400
|
+
throw new BorgServerTrustError('Borg server trust identity changed; refusing the connection');
|
|
401
|
+
}
|
|
402
|
+
requestFetch = trust.fetchImpl;
|
|
403
|
+
if (droneSession !== undefined) {
|
|
404
|
+
// Local attach credentials are already cube/drone-scoped Bearers. The
|
|
405
|
+
// server authenticates this single narrower principal directly.
|
|
406
|
+
token = droneSession;
|
|
407
|
+
}
|
|
408
|
+
else if (authToken !== undefined) {
|
|
409
|
+
token = authToken;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const stored = await getServerCredential(baseUrl, serverTrustIdentity);
|
|
413
|
+
if (!stored) {
|
|
414
|
+
throw new Error('No credential is stored for the selected Borg server identity');
|
|
415
|
+
}
|
|
416
|
+
token = stored;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const method = (rest.method ?? 'GET').toUpperCase();
|
|
420
|
+
const buildRequest = async (tok) => {
|
|
421
|
+
const finalHeaders = {
|
|
422
|
+
'Authorization': `Bearer ${tok}`,
|
|
423
|
+
...headers,
|
|
424
|
+
};
|
|
425
|
+
// --debug / BORG_DEBUG: trace every HTTP attempt. Logs method/path/status
|
|
426
|
+
// ONLY — never the Authorization header or any token material.
|
|
427
|
+
debugLog(`→ ${method} ${path}`);
|
|
428
|
+
const res = await requestFetch(`${baseUrl}${path}`, {
|
|
429
|
+
...rest,
|
|
430
|
+
headers: finalHeaders,
|
|
431
|
+
});
|
|
432
|
+
debugLog(`← ${res.status} ${method} ${path}`);
|
|
433
|
+
return res;
|
|
434
|
+
};
|
|
435
|
+
const response = await buildRequest(token);
|
|
436
|
+
if (response.status === 401) {
|
|
437
|
+
// Reached only after pinned-TLS trust is verified (localAuthorityContext
|
|
438
|
+
// fails closed otherwise). The DESTRUCTIVE worktree-seat reset is permitted
|
|
439
|
+
// ONLY when BOTH hold: (a) this request used the drone SESSION bearer, and
|
|
440
|
+
// (b) the server's bounded-decoded shared-v2 error envelope carries the EXACT
|
|
441
|
+
// typed code SESSION_REJECTED. A bare 401 is never sufficient. Any other or
|
|
442
|
+
// absent/malformed/oversized code — or a parent enrollment/client credential
|
|
443
|
+
// (authToken/stored) — is CREDENTIAL_REJECTED → non-destructive re-enroll
|
|
444
|
+
// recovery, never a seat reset. The body is read (and thus consumed) here,
|
|
445
|
+
// bounded to reject oversized hostile payloads (fail closed to non-reset).
|
|
446
|
+
let rejectedCode;
|
|
447
|
+
try {
|
|
448
|
+
const body = await readBoundedResponseBody(response, AUTH_ERROR_ENVELOPE_LIMIT_BYTES, 'Local Borg server auth error response exceeded the response limit');
|
|
449
|
+
rejectedCode = decodeProtocolErrorEnvelope(JSON.parse(body)).error.code;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
rejectedCode = undefined;
|
|
453
|
+
}
|
|
454
|
+
if (droneSession !== undefined && rejectedCode === ErrorCode.AUTH_EXPIRED) {
|
|
455
|
+
throw new BorgServerError('AUTH_EXPIRED', 'the selected Borg server session expired');
|
|
456
|
+
}
|
|
457
|
+
if (droneSession !== undefined && rejectedCode === ErrorCode.SESSION_REJECTED) {
|
|
458
|
+
throw new BorgServerError('SESSION_REJECTED', 'the selected Borg server rejected this worktree session (revoked or taken over)');
|
|
459
|
+
}
|
|
460
|
+
throw new BorgServerError('CREDENTIAL_REJECTED', 'the selected Borg server rejected the credential');
|
|
461
|
+
}
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
// Do not copy a server response body into errors or debug output: a malicious or
|
|
464
|
+
// misconfigured server could reflect bearer/invitation material or inject
|
|
465
|
+
// terminal controls. Decode only the bounded protocol error code for typed
|
|
466
|
+
// branching; never surface the server-provided message or details.
|
|
467
|
+
let code;
|
|
468
|
+
try {
|
|
469
|
+
const body = await readBoundedResponseBody(response, AUTH_ERROR_ENVELOPE_LIMIT_BYTES, 'Local Borg server error response exceeded the response limit');
|
|
470
|
+
code = decodeProtocolErrorEnvelope(JSON.parse(body)).error.code;
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
code = undefined;
|
|
474
|
+
}
|
|
475
|
+
debugLog(`✗ ${response.status} ${method} ${path}`);
|
|
476
|
+
if (droneSession !== undefined && response.status === 410 && code === DRONE_EVICTED_CODE) {
|
|
477
|
+
throw new DroneEvictedError();
|
|
478
|
+
}
|
|
479
|
+
throw new BorgServerHttpError(response.status, `Borg server request failed (HTTP ${response.status})`, code);
|
|
480
|
+
}
|
|
481
|
+
return response;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get the active cube's directive + role registry.
|
|
485
|
+
*/
|
|
486
|
+
export async function getCubeInfo(sessionToken, apiUrl, serverTrustIdentity) {
|
|
487
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
488
|
+
const composed = await localCubeComposition(local);
|
|
489
|
+
return { cube: composed.cube, roles: composed.roles };
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Get this drone's assigned role (with detailed_description).
|
|
493
|
+
*/
|
|
494
|
+
export async function getRoleInfo(sessionToken, apiUrl, serverTrustIdentity) {
|
|
495
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
496
|
+
return { role: (await localCubeComposition(local)).role };
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Get a named role's full playbook (detailed_description). Any drone in
|
|
500
|
+
* the cube may read any role. `role` is a role name (case-insensitive)
|
|
501
|
+
* or role id.
|
|
502
|
+
*/
|
|
503
|
+
export async function getRoleInfoByName(sessionToken, apiUrl, role, serverTrustIdentity) {
|
|
504
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
505
|
+
const roles = (await localCubeComposition(local)).roles;
|
|
506
|
+
const matched = roles.find((candidate) => candidate.id === role || candidate.name.toLowerCase() === role.toLowerCase());
|
|
507
|
+
if (!matched)
|
|
508
|
+
throw new Error(`Local Borg server has no role named ${JSON.stringify(role)}`);
|
|
509
|
+
return { role: matched };
|
|
510
|
+
}
|
|
511
|
+
export async function whoami(sessionToken, apiUrl, serverTrustIdentity) {
|
|
512
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
513
|
+
const composed = await localCubeComposition(local);
|
|
514
|
+
return {
|
|
515
|
+
cube_id: composed.cube.id,
|
|
516
|
+
cube_name: composed.cube.name,
|
|
517
|
+
drone_id: composed.drone.id,
|
|
518
|
+
drone_label: composed.drone.label,
|
|
519
|
+
role_id: composed.role.id,
|
|
520
|
+
role_name: composed.role.name,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* List all currently-connected drones in this cube.
|
|
525
|
+
*
|
|
526
|
+
* Optional `since` is the T2.1 sender-side liveness probe — pass either
|
|
527
|
+
* an activity_log entry id (UUID; server resolves to its `created_at`)
|
|
528
|
+
* OR an ISO-8601 timestamp. When provided, the response includes:
|
|
529
|
+
* - per-drone `seen_since: boolean` — true iff that drone's
|
|
530
|
+
* `last_seen` is strictly after the resolved timestamp
|
|
531
|
+
* - top-level `since: ISO-string | null` — the resolved timestamp
|
|
532
|
+
* (echoed back so the renderer can label the column accurately
|
|
533
|
+
* even when the caller passed an entry-id)
|
|
534
|
+
*/
|
|
535
|
+
export async function getRoster(sessionToken, apiUrl, since, serverTrustIdentity) {
|
|
536
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
537
|
+
if (since !== undefined)
|
|
538
|
+
localUnsupported('roster liveness filtering');
|
|
539
|
+
const composed = await localCubeComposition(local);
|
|
540
|
+
return { drones: composed.drones, roles: composed.roles, message_taxonomy: null };
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Read recent log entries for the cube.
|
|
544
|
+
*/
|
|
545
|
+
export async function readLog(sessionToken, apiUrl, opts = {}) {
|
|
546
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, opts.serverTrustIdentity);
|
|
547
|
+
let cursor = null;
|
|
548
|
+
if (opts.unreadOnly)
|
|
549
|
+
cursor = await getLocalServerCursor(localCursorBinding(local));
|
|
550
|
+
if (opts.since !== undefined)
|
|
551
|
+
cursor = await resolveLocalLogCursor(local, opts.since);
|
|
552
|
+
const page = await localReadLogPage(local, { cursor, limit: opts.limit });
|
|
553
|
+
if (opts.unreadOnly && page.cursor) {
|
|
554
|
+
await advanceLocalServerCursor(localCursorBinding(local), page.cursor);
|
|
555
|
+
}
|
|
556
|
+
const composed = await localCubeComposition(local);
|
|
557
|
+
return {
|
|
558
|
+
entries: page.entries,
|
|
559
|
+
drones: composed.drones,
|
|
560
|
+
roles: composed.roles,
|
|
561
|
+
behind_by: page.behind_by,
|
|
562
|
+
has_more: page.has_more,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Sprint 25 log substrate refactor: explicit ack on a log entry.
|
|
567
|
+
*
|
|
568
|
+
* Replaces in-band `ACK: <dispatch-id>` log entries with a DB-backed
|
|
569
|
+
* flag on activity_log_acks. Idempotent — the server INSERT uses ON
|
|
570
|
+
* CONFLICT DO NOTHING. 204 No Content on success.
|
|
571
|
+
*/
|
|
572
|
+
// 'claim' is advisory review-gate ownership; 'ack' preserves the original wire default.
|
|
573
|
+
export async function ackLogEntry(sessionToken, apiUrl, entryId, kind = 'ack', serverTrustIdentity) {
|
|
574
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
575
|
+
await localServerRequest(local, `/api/cubes/${local.cubeId}/acks`, 'POST', { entry_id: entryId, kind });
|
|
576
|
+
}
|
|
577
|
+
/** Record a ratified cube decision using the local client's cube-manage grant. */
|
|
578
|
+
export async function recordDecision(sessionToken, apiUrl, input, serverTrustIdentity) {
|
|
579
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
580
|
+
const payload = await localManageRequest(local, `/api/cubes/${local.cubeId}/decisions`, 'POST', {
|
|
581
|
+
operation: `record a decision in cube ${manageCopyValue(local.name)}`,
|
|
582
|
+
cubeName: local.name,
|
|
583
|
+
noMutation: 'Nothing was recorded.',
|
|
584
|
+
}, input);
|
|
585
|
+
if (!payload)
|
|
586
|
+
throw new Error('Local Borg server returned an empty decision response');
|
|
587
|
+
return payload;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* gh#740: list active ratified decisions for the cube (any member). With
|
|
591
|
+
* `topic`, returns that topic's active decision.
|
|
592
|
+
*/
|
|
593
|
+
export async function listDecisions(sessionToken, apiUrl, topic, serverTrustIdentity) {
|
|
594
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
595
|
+
const payload = await localServerRequest(local, `/api/cubes/${local.cubeId}/decisions`, 'PUT', {});
|
|
596
|
+
if (!payload)
|
|
597
|
+
throw new Error('Local Borg server returned an empty decisions response');
|
|
598
|
+
return {
|
|
599
|
+
decisions: topic === undefined
|
|
600
|
+
? payload.decisions
|
|
601
|
+
: payload.decisions.filter((decision) => decision.topic === topic),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/** Remove one active ratified decision. The worker enforces the seat gate. */
|
|
605
|
+
export async function removeDecision(sessionToken, apiUrl, selector, serverTrustIdentity) {
|
|
606
|
+
void sessionToken;
|
|
607
|
+
void apiUrl;
|
|
608
|
+
void selector;
|
|
609
|
+
void serverTrustIdentity;
|
|
610
|
+
localUnsupported('decision removal');
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Regen: one-shot composite of everything a drone needs to be oriented.
|
|
614
|
+
*
|
|
615
|
+
* Returns the active cube's directive, the drone's own role with full
|
|
616
|
+
* detailed_description, the public role registry (no detailed_description
|
|
617
|
+
* leakage for OTHER roles), the drone roster, and the caller's unread-log
|
|
618
|
+
* COUNT (behind_by). gh#886: the recent-log PAYLOAD is no longer rendered
|
|
619
|
+
* client-side — the drone gets the count and drains via borg_read-log. Use
|
|
620
|
+
* on session start and before each new task to stay in sync.
|
|
621
|
+
*
|
|
622
|
+
* gh#29 Sprint C / Q3a: optional `since` cursor (entry-id UUID or
|
|
623
|
+
* ISO-8601 timestamp). The worker still ships `recentLog` for rollout-compat
|
|
624
|
+
* (a pre-gh#886 client renders it; `since` trims it to entries strictly after
|
|
625
|
+
* the anchor) — but the current client renders the unread COUNT, not the
|
|
626
|
+
* payload, so `since` no longer affects what this client shows.
|
|
627
|
+
*/
|
|
628
|
+
export async function regen(sessionToken, apiUrl, opts = {}) {
|
|
629
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, opts.serverTrustIdentity);
|
|
630
|
+
const composed = await localCubeComposition(local);
|
|
631
|
+
const cursor = opts.since === undefined
|
|
632
|
+
? await getLocalServerCursor(localCursorBinding(local))
|
|
633
|
+
: await resolveLocalLogCursor(local, opts.since);
|
|
634
|
+
const page = await localReadLogPage(local, { cursor, limit: 1 });
|
|
635
|
+
let decisions = [];
|
|
636
|
+
try {
|
|
637
|
+
decisions = (await listDecisions(sessionToken, apiUrl, undefined, opts.serverTrustIdentity)).decisions;
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
console.warn(`Local regen: failed to fetch ratified decisions (${error instanceof Error ? error.message : String(error)}); continuing without them.`);
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
cube: composed.cube,
|
|
644
|
+
role: composed.role,
|
|
645
|
+
drone: composed.drone,
|
|
646
|
+
roles: composed.roles,
|
|
647
|
+
drones: composed.drones,
|
|
648
|
+
recentLog: [],
|
|
649
|
+
behind_by: page.entries.length + page.behind_by,
|
|
650
|
+
decisions,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
export async function roleRationale(sessionToken, apiUrl, role, section, serverTrustIdentity) {
|
|
654
|
+
void sessionToken;
|
|
655
|
+
void apiUrl;
|
|
656
|
+
void role;
|
|
657
|
+
void section;
|
|
658
|
+
void serverTrustIdentity;
|
|
659
|
+
localUnsupported('role rationale sections');
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Append a message to the cube's shared activity log.
|
|
663
|
+
*/
|
|
664
|
+
export async function appendLog(sessionToken, apiUrl, message, opts = {}) {
|
|
665
|
+
if (opts.visibility === 'broadcast' && (opts.to?.length ?? 0) > 0) {
|
|
666
|
+
throw new Error("Invalid input: visibility:'broadcast' cannot be combined with non-empty to:. " +
|
|
667
|
+
'Remove visibility to direct to recipients, or remove to: to broadcast.');
|
|
668
|
+
}
|
|
669
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, opts.serverTrustIdentity);
|
|
670
|
+
let visibility = opts.visibility;
|
|
671
|
+
let recipientDroneIds = opts.recipientDroneIds;
|
|
672
|
+
if (visibility !== 'broadcast' &&
|
|
673
|
+
(!recipientDroneIds || recipientDroneIds.length === 0) &&
|
|
674
|
+
opts.to !== undefined) {
|
|
675
|
+
const base = `/api/cubes/${local.cubeId}`;
|
|
676
|
+
const [rolePayload, dronePayload] = await Promise.all([
|
|
677
|
+
localServerRequest(local, `${base}/roles`, 'GET'),
|
|
678
|
+
localServerRequest(local, `${base}/drones`, 'GET'),
|
|
679
|
+
]);
|
|
680
|
+
if (!rolePayload || !dronePayload) {
|
|
681
|
+
throw new Error('Local Borg server returned an incomplete cube roster');
|
|
682
|
+
}
|
|
683
|
+
recipientDroneIds = resolveLocalLogRecipients(opts.to, dronePayload.drones, rolePayload.roles);
|
|
684
|
+
visibility = 'direct';
|
|
685
|
+
}
|
|
686
|
+
else if (visibility === undefined && recipientDroneIds !== undefined) {
|
|
687
|
+
visibility = 'direct';
|
|
688
|
+
}
|
|
689
|
+
const payload = await localServerRequest(local, `/api/cubes/${local.cubeId}/logs`, 'POST', {
|
|
690
|
+
message,
|
|
691
|
+
...(visibility ? { visibility } : {}),
|
|
692
|
+
...(visibility === 'direct' && recipientDroneIds
|
|
693
|
+
? { recipientDroneIds }
|
|
694
|
+
: {}),
|
|
695
|
+
// server#48 append-time taxonomy routing: forward the requested class
|
|
696
|
+
// so the server can classify/route. It is honored only when no explicit
|
|
697
|
+
// visibility/recipients override it (server resolveMessageRouting).
|
|
698
|
+
...(opts.class ? { class: opts.class } : {}),
|
|
699
|
+
});
|
|
700
|
+
if (!payload)
|
|
701
|
+
throw new Error('Local Borg server returned an empty log response');
|
|
702
|
+
return { entry: payload.entry };
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* List cubes readable by the local client's live grants.
|
|
706
|
+
*/
|
|
707
|
+
export async function listCubes(connection) {
|
|
708
|
+
return localConnectionRequest(await localOwnerConnection(connection), '/api/cubes');
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* List bundled cube templates. Used by the `borg assimilate` orchestrator
|
|
712
|
+
* to surface the interactive template prompt on first-drone bootstrap.
|
|
713
|
+
*/
|
|
714
|
+
/**
|
|
715
|
+
* Create a new cube. Server-side seeds a default "Drone" role atomically
|
|
716
|
+
* so the cube is assimilatable immediately, OR applies the named template
|
|
717
|
+
* atomically when `opts.template` is set (single-withUserId transaction —
|
|
718
|
+
* skips the auto-Drone insert to avoid is_default partial-index conflict).
|
|
719
|
+
*
|
|
720
|
+
* Returns `{ cube, roles }` — the roles array lets the assimilate
|
|
721
|
+
* orchestrator pick a default role without a follow-up `getCube` call.
|
|
722
|
+
* Existing callers that read `body.cube` keep working (forward-compat).
|
|
723
|
+
*/
|
|
724
|
+
export async function createCube(name, cubeDirective, opts, connection) {
|
|
725
|
+
void name;
|
|
726
|
+
void cubeDirective;
|
|
727
|
+
void opts;
|
|
728
|
+
void connection;
|
|
729
|
+
localUnsupported('cube creation');
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Update a cube's name and/or cube_directive. Both fields are optional;
|
|
733
|
+
* pass only what changes.
|
|
734
|
+
*/
|
|
735
|
+
export async function updateCube(cubeId, updates) {
|
|
736
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
737
|
+
const active = await getActiveCube();
|
|
738
|
+
if (!active?.serverTrustIdentity)
|
|
739
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
740
|
+
if (updates.name !== undefined)
|
|
741
|
+
localUnsupported('cube rename');
|
|
742
|
+
const payload = {};
|
|
743
|
+
if (updates.cube_directive !== undefined)
|
|
744
|
+
payload.cube_directive = updates.cube_directive;
|
|
745
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'message_taxonomy')) {
|
|
746
|
+
payload.message_taxonomy = updates.message_taxonomy ?? null;
|
|
747
|
+
}
|
|
748
|
+
const result = await localManageRequest(active, `/api/cubes/${cubeId}`, 'PATCH', {
|
|
749
|
+
operation: `update cube settings in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
750
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
751
|
+
noMutation: 'No cube settings were changed.',
|
|
752
|
+
}, payload);
|
|
753
|
+
if (!result)
|
|
754
|
+
throw new Error('Local Borg server returned an empty cube response');
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* gh#473 PR1 — granular per-class taxonomy patch. Add / replace-by-name
|
|
759
|
+
* / remove a single class within the cube's message_taxonomy, leaving
|
|
760
|
+
* other classes unchanged. The server re-validates the full resulting
|
|
761
|
+
* array and requires the selected local client's live cube-manage grant.
|
|
762
|
+
*/
|
|
763
|
+
export async function patchTaxonomyClass(cubeId, op) {
|
|
764
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
765
|
+
const active = await getActiveCube();
|
|
766
|
+
if (!active?.serverTrustIdentity)
|
|
767
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
768
|
+
const className = op.action === 'remove' ? op.class : op.class_def.class;
|
|
769
|
+
const preposition = op.action === 'add' ? 'to' : op.action === 'replace' ? 'in' : 'from';
|
|
770
|
+
const pastTense = op.action === 'add' ? 'added' : op.action === 'replace' ? 'replaced' : 'removed';
|
|
771
|
+
const cubeName = cubeId === active.cubeId ? active.name : cubeId;
|
|
772
|
+
const result = await localManageRequest(active, `/api/cubes/${cubeId}/taxonomy-patch`, 'POST', {
|
|
773
|
+
operation: `${op.action} message class ${manageCopyValue(className)} ${preposition} cube ${manageCopyValue(cubeName)}`,
|
|
774
|
+
cubeName,
|
|
775
|
+
noMutation: `No message class was ${pastTense}.`,
|
|
776
|
+
}, op);
|
|
777
|
+
if (!result)
|
|
778
|
+
throw new Error('Local Borg server returned an empty taxonomy response');
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Delete a cube. Cascade-deletes all roles, drones, and log entries.
|
|
783
|
+
* Requires a live cube-manage grant on the selected local client.
|
|
784
|
+
*/
|
|
785
|
+
export async function deleteCube(cubeId) {
|
|
786
|
+
void cubeId;
|
|
787
|
+
localUnsupported('cube deletion');
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Create a role inside a cube. is_default=true demotes the previous
|
|
791
|
+
* default role; the cube always has exactly one default.
|
|
792
|
+
*/
|
|
793
|
+
export async function createRole(cubeId, data) {
|
|
794
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
795
|
+
const active = await getActiveCube();
|
|
796
|
+
if (!active?.serverTrustIdentity)
|
|
797
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
798
|
+
if (data.default_model !== undefined)
|
|
799
|
+
localUnsupported('per-role default model');
|
|
800
|
+
const result = await localManageRequest(active, `/api/cubes/${cubeId}/roles`, 'POST', {
|
|
801
|
+
operation: `create role ${manageCopyValue(data.name)} in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
802
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
803
|
+
noMutation: 'No role was created.',
|
|
804
|
+
}, buildLocalRoleFields(data));
|
|
805
|
+
if (!result)
|
|
806
|
+
throw new Error('Local Borg server returned an empty role response');
|
|
807
|
+
return result;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Update a role. All fields optional; pass only what changes.
|
|
811
|
+
*/
|
|
812
|
+
export async function updateRole(roleId, updates) {
|
|
813
|
+
assertUuidShape(roleId, 'role_id');
|
|
814
|
+
const active = await getActiveCube();
|
|
815
|
+
if (!active?.serverTrustIdentity)
|
|
816
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
817
|
+
assertUuidShape(active.cubeId, 'cube_id');
|
|
818
|
+
if (updates.default_model !== undefined)
|
|
819
|
+
localUnsupported('per-role default model');
|
|
820
|
+
const result = await localManageRequest(active, `/api/cubes/${active.cubeId}/roles/${roleId}`, 'PATCH', {
|
|
821
|
+
operation: `update role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
|
|
822
|
+
cubeName: active.name,
|
|
823
|
+
noMutation: 'No role was updated.',
|
|
824
|
+
}, buildLocalRoleFields(updates));
|
|
825
|
+
if (!result)
|
|
826
|
+
throw new Error('Local Borg server returned an empty role response');
|
|
827
|
+
return result;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Project a create/update-role field bag onto the exact snake_case keys the
|
|
831
|
+
* self-hosted coordination API accepts, dropping undefined entries and the
|
|
832
|
+
* unsupported default_model (rejected before this call). name is included only
|
|
833
|
+
* when present so a partial update PATCHes just the supplied fields.
|
|
834
|
+
*/
|
|
835
|
+
function buildLocalRoleFields(fields) {
|
|
836
|
+
const payload = {};
|
|
837
|
+
for (const key of [
|
|
838
|
+
'name',
|
|
839
|
+
'short_description',
|
|
840
|
+
'detailed_description',
|
|
841
|
+
'is_default',
|
|
842
|
+
'is_mandatory',
|
|
843
|
+
'is_human_seat',
|
|
844
|
+
'can_broadcast',
|
|
845
|
+
'receives_all_direct',
|
|
846
|
+
'role_class',
|
|
847
|
+
]) {
|
|
848
|
+
if (fields[key] !== undefined)
|
|
849
|
+
payload[key] = fields[key];
|
|
850
|
+
}
|
|
851
|
+
return payload;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* gh#473 PR1 — granular role-text section patch. Replace / insert /
|
|
855
|
+
* delete a single named section of a role's detailed_description,
|
|
856
|
+
* leaving the rest of the field byte-identical. Requires the selected local
|
|
857
|
+
* client's live cube-manage grant. Sections are delimited by plain-label lines (e.g.
|
|
858
|
+
* `Workflow:`), NOT markdown headings.
|
|
859
|
+
*/
|
|
860
|
+
export async function patchRoleSection(roleId, op) {
|
|
861
|
+
assertUuidShape(roleId, 'role_id');
|
|
862
|
+
const active = await getActiveCube();
|
|
863
|
+
if (!active?.serverTrustIdentity)
|
|
864
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
865
|
+
assertUuidShape(active.cubeId, 'cube_id');
|
|
866
|
+
const result = await localManageRequest(active, `/api/cubes/${active.cubeId}/roles/${roleId}/section-patch`, 'POST', {
|
|
867
|
+
operation: `${op.action} section ${manageCopyValue(op.heading)} ${op.action === 'delete' ? 'from' : 'in'} role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
|
|
868
|
+
cubeName: active.name,
|
|
869
|
+
noMutation: `No role section was ${op.action === 'insert' ? 'inserted' : op.action === 'replace' ? 'replaced' : 'deleted'}.`,
|
|
870
|
+
}, { ...op });
|
|
871
|
+
if (!result)
|
|
872
|
+
throw new Error('Local Borg server returned an empty role response');
|
|
873
|
+
return result;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Delete a role. Worker refuses if any drone is still assigned to it
|
|
877
|
+
* (reassign or evict those drones first).
|
|
878
|
+
*/
|
|
879
|
+
export async function deleteRole(roleId) {
|
|
880
|
+
void roleId;
|
|
881
|
+
localUnsupported('role deletion');
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Reassign a drone to a different role within the same cube.
|
|
885
|
+
* Queen-class seat cardinality is enforced server-side — attempting
|
|
886
|
+
* to assign to a queen-class role when another drone already holds
|
|
887
|
+
* the seat returns an error. The class-hierarchy guard also rejects
|
|
888
|
+
* direct promotion from non-human-seat roles.
|
|
889
|
+
*/
|
|
890
|
+
export async function reassignDrone(droneId, roleId, activeOverride) {
|
|
891
|
+
// Validate both identifiers before credential lookup or network access.
|
|
892
|
+
assertUuidShape(droneId, 'drone_id');
|
|
893
|
+
assertUuidShape(roleId, 'role_id');
|
|
894
|
+
const active = activeOverride ?? await getActiveCube();
|
|
895
|
+
if (!active?.serverTrustIdentity)
|
|
896
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
897
|
+
assertUuidShape(active.cubeId, 'cube_id');
|
|
898
|
+
const result = await localManageRequest(active, `/api/cubes/${active.cubeId}/drones/${droneId}`, 'PATCH', {
|
|
899
|
+
operation: `reassign drone ${manageCopyValue(droneId)} to role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
|
|
900
|
+
cubeName: active.name,
|
|
901
|
+
noMutation: 'No drone was reassigned.',
|
|
902
|
+
}, { role_id: roleId }, decodeReassignDroneResult);
|
|
903
|
+
if (!result)
|
|
904
|
+
throw new Error('Local Borg server returned an empty drone reassignment response');
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
907
|
+
export async function evictDrone(droneId, options = {}) {
|
|
908
|
+
assertUuidShape(droneId, 'drone_id');
|
|
909
|
+
if (options.cubeId !== undefined)
|
|
910
|
+
assertUuidShape(options.cubeId, 'cube_id');
|
|
911
|
+
const active = options.active ?? await getActiveCube();
|
|
912
|
+
if (!active?.serverTrustIdentity)
|
|
913
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
914
|
+
const cubeId = options.cubeId ?? active.cubeId;
|
|
915
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
916
|
+
const cubeName = options.cubeName ?? (cubeId === active.cubeId ? active.name : cubeId);
|
|
917
|
+
const targetReference = options.targetReference ?? droneId;
|
|
918
|
+
const result = await localManageRequest(active, `/api/cubes/${cubeId}/drones/${droneId}`, 'DELETE', {
|
|
919
|
+
operation: `remove ${manageCopyValue(targetReference)} from cube ${manageCopyValue(cubeName)}`,
|
|
920
|
+
cubeName,
|
|
921
|
+
noMutation: 'No drone was removed.',
|
|
922
|
+
}, {}, decodeEvictDroneResult);
|
|
923
|
+
if (!result)
|
|
924
|
+
throw new Error('Local Borg server returned an empty drone eviction response');
|
|
925
|
+
return result;
|
|
926
|
+
}
|
|
927
|
+
export async function listRoles(cubeId) {
|
|
928
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
929
|
+
const active = await getActiveCube();
|
|
930
|
+
if (!active?.serverTrustIdentity)
|
|
931
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
932
|
+
const result = await localServerRequest(active, `/api/cubes/${cubeId}/roles`, 'GET');
|
|
933
|
+
if (!result || !Array.isArray(result.roles)) {
|
|
934
|
+
throw new Error('Local Borg server returned an invalid roles response');
|
|
935
|
+
}
|
|
936
|
+
return result.roles;
|
|
937
|
+
}
|
|
938
|
+
export async function getCubeForManagement(cubeId, operation, activeOverride) {
|
|
939
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
940
|
+
const active = activeOverride ?? await getActiveCube();
|
|
941
|
+
if (!active?.serverTrustIdentity)
|
|
942
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
943
|
+
return getCube(cubeId, await localManageConnection(active, operation));
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Fetch a cube's full detail: directive, roles (with detailed
|
|
947
|
+
* descriptions), and drones. Access is enforced by the local client's live
|
|
948
|
+
* per-cube grant.
|
|
949
|
+
*/
|
|
950
|
+
export async function getCube(cubeId, connection) {
|
|
951
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
952
|
+
const resolved = await localOwnerConnection(connection);
|
|
953
|
+
const base = `/api/cubes/${cubeId}`;
|
|
954
|
+
const [cubePayload, rolePayload, dronePayload] = await Promise.all([
|
|
955
|
+
localConnectionRequest(resolved, base),
|
|
956
|
+
localConnectionRequest(resolved, `${base}/roles`),
|
|
957
|
+
localConnectionRequest(resolved, `${base}/drones`),
|
|
958
|
+
]);
|
|
959
|
+
return {
|
|
960
|
+
...cubePayload.cube,
|
|
961
|
+
roles: rolePayload.roles,
|
|
962
|
+
drones: dronePayload.drones,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* gh#473 PR2 — apply a named template to an existing cube via the
|
|
967
|
+
* NON-CLOBBERING server route. New roles are inserted; existing
|
|
968
|
+
* template-named roles get ADD fragments auto-applied (template
|
|
969
|
+
* sections/classes the cube lacks) but their EVOLVED (conflicting)
|
|
970
|
+
* fragments are surfaced server-side and KEPT, never overwritten. Returns
|
|
971
|
+
* `{ created, updated }` counts. To selectively take template versions of
|
|
972
|
+
* conflicting fragments, use `syncRoles` with a `decisions` map instead.
|
|
973
|
+
*/
|
|
974
|
+
export async function applyTemplate(cubeId, templateName) {
|
|
975
|
+
void cubeId;
|
|
976
|
+
void templateName;
|
|
977
|
+
localUnsupported('template application');
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* gh#473 PR2 — NON-CLOBBERING sync of a cube's roles + message_taxonomy
|
|
981
|
+
* against the current built-in template. Dry-run by default classifies
|
|
982
|
+
* each fragment (role-text SECTION / short_description / flags / taxonomy
|
|
983
|
+
* CLASS) as ADD / UNCHANGED / CONFLICT. Pass apply=true to commit:
|
|
984
|
+
* ADD fragments auto-apply (zero clobber risk); CONFLICT fragments apply
|
|
985
|
+
* ONLY when their stable key appears in `decisions` as 'accept'.
|
|
986
|
+
* Unspecified conflicts DEFAULT TO REJECT — the cube's evolved text is
|
|
987
|
+
* never silently overwritten. Custom roles (names not in template) are
|
|
988
|
+
* never touched. Returns a NonClobberSyncResult.
|
|
989
|
+
*/
|
|
990
|
+
export async function syncRoles(cubeId, templateName = 'software-dev', apply = false, decisions) {
|
|
991
|
+
void cubeId;
|
|
992
|
+
void templateName;
|
|
993
|
+
void apply;
|
|
994
|
+
void decisions;
|
|
995
|
+
localUnsupported('role synchronization');
|
|
996
|
+
}
|
|
997
|
+
//# sourceMappingURL=remote-client.js.map
|