@gajae-code/coding-agent 0.13.2 → 0.14.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/CHANGELOG.md +215 -8
- package/README.md +2 -0
- package/bin/gajaessi.js +4 -0
- package/dist/types/async/job-manager.d.ts +55 -2
- package/dist/types/async/support.d.ts +14 -0
- package/dist/types/capability/mcp.d.ts +6 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/accounts-cli.d.ts +19 -0
- package/dist/types/cli/args.d.ts +3 -0
- package/dist/types/cli/config-cli.d.ts +6 -0
- package/dist/types/cli/crash-cli.d.ts +10 -0
- package/dist/types/cli/customize-doctor.d.ts +145 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/mcp-cli.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/root-flags.d.ts +12 -0
- package/dist/types/cli/setup-cli.d.ts +3 -1
- package/dist/types/cli/skills-cli.d.ts +2 -1
- package/dist/types/cli/update-cli.d.ts +19 -1
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/accounts.d.ts +40 -0
- package/dist/types/commands/crash.d.ts +24 -0
- package/dist/types/commands/customize.d.ts +22 -0
- package/dist/types/commands/daemon.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/commands/skills.d.ts +6 -1
- package/dist/types/commands/update.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
- package/dist/types/config/config-schema-version.d.ts +10 -0
- package/dist/types/config/file-lock.d.ts +18 -0
- package/dist/types/config/model-profile-activation.d.ts +3 -1
- package/dist/types/config/model-registry.d.ts +10 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +73 -9
- package/dist/types/config/settings.d.ts +63 -0
- package/dist/types/config/skill-settings-defaults.d.ts +26 -0
- package/dist/types/config/task-delegation.d.ts +39 -0
- package/dist/types/config/update-channel.d.ts +10 -0
- package/dist/types/coordinator-mcp/server.d.ts +5 -1
- package/dist/types/crash/index-store.d.ts +87 -0
- package/dist/types/crash/nudge.d.ts +50 -0
- package/dist/types/crash/record-loader.d.ts +17 -0
- package/dist/types/crash/report.d.ts +134 -0
- package/dist/types/crash/sanitize.d.ts +25 -0
- package/dist/types/cursor.d.ts +13 -2
- package/dist/types/customization/import.d.ts +37 -0
- package/dist/types/customization/index.d.ts +4 -0
- package/dist/types/customization/inventory.d.ts +25 -0
- package/dist/types/customization/mutations.d.ts +32 -0
- package/dist/types/customization/types.d.ts +115 -0
- package/dist/types/daemon/control-types.d.ts +8 -1
- package/dist/types/daemon/operator-contract.d.ts +4 -4
- package/dist/types/discovery/claude.d.ts +39 -1
- package/dist/types/discovery/codex.d.ts +39 -1
- package/dist/types/discovery/index.d.ts +2 -0
- package/dist/types/discovery/mcp-compat.d.ts +30 -0
- package/dist/types/edit/renderer.d.ts +23 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -4
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
- package/dist/types/extensibility/hooks/loader.d.ts +19 -5
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
- package/dist/types/extensibility/skill-management.d.ts +107 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
- package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
- package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
- package/dist/types/hooks/events.d.ts +60 -0
- package/dist/types/hooks/normalize.d.ts +83 -0
- package/dist/types/hooks/skill-state.d.ts +9 -2
- package/dist/types/main.d.ts +2 -1
- package/dist/types/modes/acp/abort-scope.d.ts +13 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -4
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
- package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
- package/dist/types/modes/components/customization/index.d.ts +1 -0
- package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
- package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
- package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -4
- package/dist/types/modes/components/pet-capability.d.ts +7 -21
- package/dist/types/modes/components/settings-selector.d.ts +5 -0
- package/dist/types/modes/components/status-line/gh.d.ts +6 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
- package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/defaults/index.d.ts +118 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
- package/dist/types/runtime-mcp/config.d.ts +9 -0
- package/dist/types/runtime-mcp/manager.d.ts +26 -1
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
- package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
- package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
- package/dist/types/runtime-mcp/protocol.d.ts +278 -0
- package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
- package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
- package/dist/types/runtime-mcp/types.d.ts +102 -10
- package/dist/types/sdk/acp/adapter.d.ts +26 -5
- package/dist/types/sdk/broker/broker.d.ts +3 -55
- package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
- package/dist/types/sdk/broker/session-index.d.ts +14 -5
- package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
- package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
- package/dist/types/sdk/bus/index.d.ts +85 -99
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
- package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
- package/dist/types/sdk/bus/notification-service.d.ts +52 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
- package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
- package/dist/types/sdk/bus/session-control.d.ts +25 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
- package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
- package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
- package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
- package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
- package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
- package/dist/types/sdk/cli/rows.d.ts +2 -11
- package/dist/types/sdk/cli/session-cli.d.ts +15 -25
- package/dist/types/sdk/client/client.d.ts +97 -1
- package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +18 -5
- package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
- package/dist/types/sdk/host/host.d.ts +15 -1
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +127 -7
- package/dist/types/sdk/host/types.d.ts +1 -1
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +2 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
- package/dist/types/sdk/lifecycle/client.d.ts +59 -0
- package/dist/types/sdk/lifecycle/index.d.ts +3 -0
- package/dist/types/sdk/lifecycle/service.d.ts +232 -0
- package/dist/types/sdk/mcp/server.d.ts +9 -6
- package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
- package/dist/types/sdk/prompt-status.d.ts +10 -3
- package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
- package/dist/types/sdk/receipt-state.d.ts +17 -0
- package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
- package/dist/types/sdk/router/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +152 -0
- package/dist/types/sdk/session-list.d.ts +22 -0
- package/dist/types/sdk/session-reconnect.d.ts +43 -1
- package/dist/types/sdk/session.d.ts +34 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
- package/dist/types/session/account-inventory.d.ts +86 -0
- package/dist/types/session/agent-session.d.ts +145 -6
- package/dist/types/session/agent-storage.d.ts +10 -0
- package/dist/types/session/coordinator-tool-label.d.ts +28 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/secure-token-file.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +47 -1
- package/dist/types/session/startup-auth-config.d.ts +44 -0
- package/dist/types/session/terminal-abort.d.ts +378 -0
- package/dist/types/session/yield-queue.d.ts +7 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +88 -0
- package/dist/types/session-import/command.d.ts +35 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/provider-codex.d.ts +22 -0
- package/dist/types/session-import/provider-service.d.ts +38 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +47 -0
- package/dist/types/session-import/types.d.ts +120 -0
- package/dist/types/setup/paseo/check.d.ts +16 -0
- package/dist/types/setup/paseo/install-saga.d.ts +115 -0
- package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
- package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
- package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
- package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
- package/dist/types/setup/paseo/provider-config.d.ts +35 -0
- package/dist/types/setup/paseo/remove.d.ts +14 -0
- package/dist/types/setup/paseo/result-types.d.ts +59 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
- package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
- package/dist/types/skill-state/active-state.d.ts +5 -1
- package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
- package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
- package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
- package/dist/types/slash-commands/types.d.ts +7 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/tools/ask-answer-registry.d.ts +6 -3
- package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
- package/dist/types/tools/bash.d.ts +2 -6
- package/dist/types/tools/browser/attach.d.ts +11 -0
- package/dist/types/tools/browser/launch.d.ts +20 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +10 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/monitor.d.ts +1 -1
- package/dist/types/tools/skill-discovery.d.ts +8 -1
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
- package/dist/types/utils/herdr-pane.d.ts +76 -0
- package/package.json +19 -9
- package/scripts/build-binary.ts +2 -1
- package/scripts/build-sdk-package-smoke.ts +2 -13
- package/scripts/capture-sticky-viewport-showcase.ts +0 -1
- package/scripts/compile-args.ts +2 -0
- package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
- package/scripts/generate-sdk-operation-inventory.ts +28 -0
- package/scripts/generate-telegram-baseline-manifest.ts +1 -0
- package/scripts/generate-tool-catalog.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
- package/src/async/job-manager.ts +352 -17
- package/src/async/support.ts +25 -0
- package/src/autoresearch/dashboard.ts +28 -13
- package/src/capability/index.ts +3 -1
- package/src/capability/mcp.ts +6 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/accounts-cli.ts +395 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/auth-broker-cli.ts +136 -66
- package/src/cli/auth-gateway-cli.ts +94 -70
- package/src/cli/config-cli.ts +27 -2
- package/src/cli/crash-cli.ts +78 -0
- package/src/cli/customize-doctor.ts +1605 -0
- package/src/cli/daemon-cli.ts +12 -3
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/mcp-cli.ts +128 -60
- package/src/cli/notify-cli.ts +8 -14
- package/src/cli/root-flags.ts +8 -0
- package/src/cli/setup-cli.ts +145 -0
- package/src/cli/skills-cli.ts +38 -2
- package/src/cli/update-cli.ts +167 -12
- package/src/cli.ts +4 -0
- package/src/commands/accounts.ts +71 -0
- package/src/commands/crash.ts +35 -0
- package/src/commands/customize.ts +84 -0
- package/src/commands/daemon.ts +2 -2
- package/src/commands/deep-interview.ts +2 -0
- package/src/commands/launch.ts +1 -0
- package/src/commands/mcp.ts +12 -7
- package/src/commands/ralplan.ts +2 -0
- package/src/commands/sdk.ts +65 -32
- package/src/commands/setup.ts +5 -0
- package/src/commands/skills.ts +14 -4
- package/src/commands/ultragoal.ts +10 -1
- package/src/commands/update.ts +26 -15
- package/src/config/atomic-yaml-patch-worker.ts +57 -0
- package/src/config/atomic-yaml-patch.ts +769 -29
- package/src/config/config-schema-version.ts +10 -0
- package/src/config/file-lock.ts +70 -1
- package/src/config/mcp-schema.json +52 -14
- package/src/config/model-profile-activation.ts +23 -2
- package/src/config/model-profiles.ts +88 -21
- package/src/config/model-registry.ts +147 -21
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +77 -7
- package/src/config/settings.ts +4038 -199
- package/src/config/skill-settings-defaults.ts +41 -3
- package/src/config/task-delegation.ts +87 -0
- package/src/config/update-channel.ts +23 -0
- package/src/coordinator-mcp/server.ts +377 -308
- package/src/crash/index-store.ts +723 -0
- package/src/crash/nudge.ts +89 -0
- package/src/crash/record-loader.ts +149 -0
- package/src/crash/report.ts +551 -0
- package/src/crash/sanitize.ts +129 -0
- package/src/cursor.ts +36 -10
- package/src/customization/import.ts +946 -0
- package/src/customization/index.ts +4 -0
- package/src/customization/inventory.ts +335 -0
- package/src/customization/mutations.ts +141 -0
- package/src/customization/types.ts +223 -0
- package/src/daemon/control-types.ts +8 -1
- package/src/daemon/operator-contract.ts +6 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
- package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/discovery/claude-hooks.ts +38 -0
- package/src/discovery/claude.ts +142 -209
- package/src/discovery/codex-hooks.ts +38 -0
- package/src/discovery/codex.ts +132 -198
- package/src/discovery/helpers.ts +8 -1
- package/src/discovery/index.ts +2 -0
- package/src/discovery/mcp-compat.ts +234 -0
- package/src/discovery/mcp-json.ts +6 -0
- package/src/edit/renderer.ts +40 -0
- package/src/edit/streaming.ts +4 -2
- package/src/exec/bash-executor.ts +24 -20
- package/src/export/html/index.ts +51 -2
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +17 -4
- package/src/extensibility/extensions/types.ts +31 -6
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
- package/src/extensibility/gjc-plugins/installer.ts +9 -5
- package/src/extensibility/gjc-plugins/observability.ts +8 -2
- package/src/extensibility/gjc-plugins/registry.ts +13 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
- package/src/extensibility/gjc-plugins/schema.ts +222 -39
- package/src/extensibility/hooks/loader.ts +154 -10
- package/src/extensibility/runtime-skill-discovery.ts +257 -60
- package/src/extensibility/skill-management.ts +368 -0
- package/src/extensibility/skills.ts +52 -16
- package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
- package/src/gjc-runtime/launch-worktree.ts +56 -2
- package/src/gjc-runtime/linux-proc.ts +7 -1
- package/src/gjc-runtime/ralplan-runtime.ts +100 -164
- package/src/gjc-runtime/session-state-lock.ts +992 -0
- package/src/gjc-runtime/session-state-sidecar.ts +644 -142
- package/src/gjc-runtime/state-writer.ts +39 -14
- package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
- package/src/gjc-runtime/ultragoal-guard.ts +11 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
- package/src/gjc-runtime/workflow-settings.ts +596 -0
- package/src/goals/tools/goal-tool.ts +6 -1
- package/src/hooks/events.ts +291 -0
- package/src/hooks/native-skill-hook.ts +4 -0
- package/src/hooks/normalize.ts +369 -0
- package/src/hooks/skill-state.ts +165 -46
- package/src/internal-urls/docs-index.generated.ts +41 -32
- package/src/main.ts +36 -5
- package/src/migrate/mcp-mapper.ts +25 -2
- package/src/modes/acp/abort-scope.ts +29 -0
- package/src/modes/acp/acp-agent.ts +423 -98
- package/src/modes/components/assistant-message.ts +6 -1
- package/src/modes/components/chrome.ts +4 -3
- package/src/modes/components/customization/customization-dashboard.ts +443 -0
- package/src/modes/components/customization/import-wizard.ts +372 -0
- package/src/modes/components/customization/index.ts +1 -0
- package/src/modes/components/dynamic-theme-text.ts +22 -0
- package/src/modes/components/gajae-pet-widget.ts +607 -193
- package/src/modes/components/irc-sidebar.ts +11 -0
- package/src/modes/components/iterm-pet-transport.ts +585 -0
- package/src/modes/components/model-selector.ts +1 -6
- package/src/modes/components/oauth-selector.ts +277 -86
- package/src/modes/components/pet-capability.ts +35 -31
- package/src/modes/components/settings-selector.ts +29 -5
- package/src/modes/components/status-line/gh.ts +45 -1
- package/src/modes/components/status-line/segments.ts +19 -4
- package/src/modes/components/tool-status-header.ts +13 -3
- package/src/modes/controllers/command-controller.ts +20 -8
- package/src/modes/controllers/extension-ui-controller.ts +56 -257
- package/src/modes/controllers/input-controller.ts +10 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
- package/src/modes/controllers/selector-controller.ts +238 -77
- package/src/modes/controllers/stt-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +88 -16
- package/src/modes/print-mode.ts +35 -6
- package/src/modes/runtime-init.ts +21 -68
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/defaults/ouroboros.json +118 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +25 -0
- package/src/modes/utils/terminal-bell.ts +15 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/critic.md +1 -1
- package/src/prompts/system/rlm-research.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -2
- package/src/prompts/tools/bash.md +7 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +6 -6
- package/src/prompts/tools/read.md +0 -2
- package/src/prompts/tools/skill-discovery.md +4 -2
- package/src/runtime-mcp/client.ts +427 -23
- package/src/runtime-mcp/config.ts +37 -5
- package/src/runtime-mcp/manager-pool.test.ts +56 -0
- package/src/runtime-mcp/manager.ts +52 -2
- package/src/runtime-mcp/oauth-discovery.ts +13 -0
- package/src/runtime-mcp/oauth-flow.ts +46 -1
- package/src/runtime-mcp/pool-key.ts +5 -1
- package/src/runtime-mcp/pool.test.ts +9 -0
- package/src/runtime-mcp/protocol.test.ts +353 -0
- package/src/runtime-mcp/protocol.ts +624 -0
- package/src/runtime-mcp/scope-config.ts +78 -0
- package/src/runtime-mcp/tool-bridge.ts +44 -10
- package/src/runtime-mcp/transports/http.ts +84 -13
- package/src/runtime-mcp/types.ts +123 -12
- package/src/sdk/acp/adapter.ts +328 -97
- package/src/sdk/broker/broker.ts +160 -741
- package/src/sdk/broker/ensure.ts +9 -1
- package/src/sdk/broker/lifecycle.ts +510 -116
- package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
- package/src/sdk/broker/process-incarnation.ts +19 -0
- package/src/sdk/broker/session-index.ts +516 -117
- package/src/sdk/broker/startup-failure.ts +28 -5
- package/src/sdk/broker/transport.ts +1 -13
- package/src/sdk/bus/chat-daemon-cli.ts +4 -2
- package/src/sdk/bus/chat-daemon-control.ts +67 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
- package/src/sdk/bus/config.ts +13 -0
- package/src/sdk/bus/conversation-store.ts +59 -171
- package/src/sdk/bus/daemon-paths.ts +20 -0
- package/src/sdk/bus/discord-conversation.ts +2 -0
- package/src/sdk/bus/discord-daemon.ts +524 -126
- package/src/sdk/bus/discord-live-provider.ts +206 -79
- package/src/sdk/bus/existing-thread-readiness.ts +4 -97
- package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
- package/src/sdk/bus/index.ts +2480 -456
- package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
- package/src/sdk/bus/lifecycle-commands.ts +2 -2
- package/src/sdk/bus/managed-daemon.ts +2 -145
- package/src/sdk/bus/notification-service.ts +196 -0
- package/src/sdk/bus/operator-runtime.ts +1 -1
- package/src/sdk/bus/prompt-reconciliation.ts +24 -15
- package/src/sdk/bus/reconciliation-store.ts +553 -24
- package/src/sdk/bus/session-control.ts +49 -3
- package/src/sdk/bus/slack-conversation.ts +4 -0
- package/src/sdk/bus/slack-daemon.ts +733 -134
- package/src/sdk/bus/slack-live-provider.ts +65 -15
- package/src/sdk/bus/slack-provider.ts +4 -0
- package/src/sdk/bus/slack-thread-binding.ts +9 -104
- package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
- package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
- package/src/sdk/bus/telegram-daemon-control.ts +3 -13
- package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
- package/src/sdk/bus/telegram-daemon.ts +2552 -2280
- package/src/sdk/bus/telegram-reference.ts +1 -226
- package/src/sdk/bus/telegram-setup.ts +2 -2
- package/src/sdk/bus/threaded-render.ts +1 -0
- package/src/sdk/bus/topic-registry.ts +279 -146
- package/src/sdk/cli/rows.ts +2 -35
- package/src/sdk/cli/session-cli.ts +776 -700
- package/src/sdk/client/client.ts +892 -1
- package/src/sdk/host/control/dispatch.ts +93 -58
- package/src/sdk/host/control/index.ts +1 -1
- package/src/sdk/host/control/operations.ts +18 -10
- package/src/sdk/host/control/runtime-gate.ts +40 -0
- package/src/sdk/host/host.ts +63 -12
- package/src/sdk/host/query/handlers.ts +66 -7
- package/src/sdk/host/reverse-leases.ts +2 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +2787 -34
- package/src/sdk/host/session-runtime.ts +1489 -101
- package/src/sdk/host/surface-policy.ts +12 -8
- package/src/sdk/host/types.ts +1 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
- package/src/sdk/host/websocket-transport.ts +85 -50
- package/src/sdk/index.ts +2 -1
- package/src/sdk/lifecycle/broker-client.ts +53 -0
- package/src/sdk/lifecycle/client.ts +234 -0
- package/src/sdk/lifecycle/index.ts +3 -0
- package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
- package/src/sdk/lifecycle/service.ts +720 -0
- package/src/sdk/mcp/server.ts +226 -188
- package/src/sdk/prompt-deadline-lease.ts +55 -0
- package/src/sdk/prompt-deadline-manager.ts +148 -0
- package/src/sdk/prompt-status.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +272 -129
- package/src/sdk/protocol/operation-registry.ts +71 -8
- package/src/sdk/receipt-state.ts +43 -0
- package/src/sdk/reconciliation-extensions.ts +14 -0
- package/src/sdk/router/index.ts +1 -0
- package/src/sdk/router/session-router.ts +1658 -0
- package/src/sdk/session-list.ts +93 -0
- package/src/sdk/session-reconnect.ts +45 -1
- package/src/sdk/session.ts +716 -120
- package/src/sdk/transport/serve-cli.ts +18 -34
- package/src/session/account-inventory.ts +603 -0
- package/src/session/agent-session.ts +2873 -559
- package/src/session/agent-storage.ts +35 -0
- package/src/session/blob-store.ts +32 -1
- package/src/session/coordinator-tool-label.ts +40 -0
- package/src/session/internal/managed-session-scope.ts +4 -0
- package/src/session/internal/managed-session-storage.ts +199 -31
- package/src/session/messages.ts +1 -1
- package/src/session/secure-token-file.ts +80 -0
- package/src/session/session-manager.ts +757 -57
- package/src/session/session-storage.ts +25 -16
- package/src/session/startup-auth-config.ts +297 -0
- package/src/session/terminal-abort.ts +1253 -0
- package/src/session/yield-queue.ts +57 -15
- package/src/session-import/claude.ts +334 -0
- package/src/session-import/codex.ts +1033 -0
- package/src/session-import/command.ts +77 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +7 -0
- package/src/session-import/provider-codex.ts +327 -0
- package/src/session-import/provider-service.ts +457 -0
- package/src/session-import/redact.ts +128 -0
- package/src/session-import/service.ts +1264 -0
- package/src/session-import/types.ts +148 -0
- package/src/setup/paseo/check.ts +213 -0
- package/src/setup/paseo/install-saga.ts +293 -0
- package/src/setup/paseo/json-publisher.ts +263 -0
- package/src/setup/paseo/orchestration-preferences.ts +86 -0
- package/src/setup/paseo/paseo-ownership.ts +224 -0
- package/src/setup/paseo/paseo-setup.ts +253 -0
- package/src/setup/paseo/provider-config.ts +139 -0
- package/src/setup/paseo/remove.ts +183 -0
- package/src/setup/paseo/result-types.ts +79 -0
- package/src/setup/paseo/setup-deps.ts +175 -0
- package/src/setup/paseo/skills-bridge.ts +260 -0
- package/src/skill-state/active-state.ts +21 -11
- package/src/slash-commands/acp-builtins.ts +9 -1
- package/src/slash-commands/builtin-registry.ts +117 -19
- package/src/slash-commands/helpers/credential-switch.ts +143 -0
- package/src/slash-commands/helpers/mcp.ts +9 -3
- package/src/slash-commands/helpers/usage-report.ts +61 -56
- package/src/slash-commands/types.ts +7 -2
- package/src/task/executor.ts +30 -11
- package/src/task/index.ts +66 -7
- package/src/task/render.ts +23 -9
- package/src/tools/ask-answer-registry.ts +23 -7
- package/src/tools/ask.ts +8 -4
- package/src/tools/bash-sleep-advisory.ts +115 -0
- package/src/tools/bash.ts +98 -18
- package/src/tools/browser/attach.ts +71 -3
- package/src/tools/browser/launch.ts +86 -13
- package/src/tools/browser/profile-discovery.ts +57 -7
- package/src/tools/browser/render.ts +8 -1
- package/src/tools/browser.ts +130 -21
- package/src/tools/computer.enforcement.test.ts +1 -1
- package/src/tools/computer.ts +3 -2
- package/src/tools/cron.ts +12 -1
- package/src/tools/image-gen.ts +349 -142
- package/src/tools/index.ts +8 -0
- package/src/tools/job.ts +33 -6
- package/src/tools/monitor.ts +41 -2
- package/src/tools/skill-discovery.ts +21 -29
- package/src/tools/subagent.ts +142 -24
- package/src/tools/todo-contract.ts +55 -3
- package/src/tools/todo-write.ts +4 -3
- package/src/tools/tool-catalog.generated.ts +6 -6
- package/src/tools/ultragoal-ask-guard.ts +10 -3
- package/src/utils/herdr-pane.ts +345 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/scrapers/index.ts +6 -4
- package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
- package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
- package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
- package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
- package/dist/types/runtime/lazy-service.test.d.ts +0 -1
- package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
- package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
- package/dist/types/runtime-mcp/config.test.d.ts +0 -1
- package/dist/types/runtime-mcp/http.test.d.ts +0 -1
- package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
- package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
- package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
- package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
- package/dist/types/sdk/elevation/audit.d.ts +0 -93
- package/dist/types/sdk/elevation/capability.d.ts +0 -3
- package/dist/types/sdk/elevation/digest.d.ts +0 -72
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
- package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
- package/dist/types/sdk/elevation/owner.d.ts +0 -48
- package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
- package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
- package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
- package/dist/types/session/auth-broker-config.d.ts +0 -13
- package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
- package/dist/types/skills/skills.test.d.ts +0 -1
- package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
- package/dist/types/task/token-log.test.d.ts +0 -1
- package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
- package/dist/types/tools/descriptors.test.d.ts +0 -1
- package/scripts/g005-daemon-smoke.ts +0 -180
- package/scripts/g011-daemon-path-smoke.ts +0 -153
- package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
- package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
- package/src/sdk/bus/telegram-cli.ts +0 -212
- package/src/sdk/elevation/allowlist.ts +0 -54
- package/src/sdk/elevation/audit.ts +0 -208
- package/src/sdk/elevation/capability.ts +0 -41
- package/src/sdk/elevation/digest.ts +0 -129
- package/src/sdk/elevation/dispatch-receipt.ts +0 -184
- package/src/sdk/elevation/grant-ledger.ts +0 -1142
- package/src/sdk/elevation/owner.ts +0 -109
- package/src/session/auth-broker-config.ts +0 -109
- /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
- /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
- /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -6,25 +6,6 @@ import path from "node:path";
|
|
|
6
6
|
import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
|
|
7
7
|
import { logger } from "@gajae-code/utils";
|
|
8
8
|
import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
|
|
9
|
-
import { SdkClient, SdkClientError } from "../client";
|
|
10
|
-
import { elevationAuthorityPath, signElevationCapability } from "../elevation/capability";
|
|
11
|
-
import { type ElevationOperation, elevationRequestDigest, isElevatableOperation } from "../elevation/digest";
|
|
12
|
-
import {
|
|
13
|
-
type ElevationAnswerAuthority,
|
|
14
|
-
type ElevationClaimIdentity,
|
|
15
|
-
type ElevationDispatchOutcome,
|
|
16
|
-
isElevationRequestId,
|
|
17
|
-
} from "../elevation/dispatch-receipt";
|
|
18
|
-
import {
|
|
19
|
-
type ElevationAnswerValue,
|
|
20
|
-
type ElevationGrantRecord,
|
|
21
|
-
ElevationLedger,
|
|
22
|
-
type ElevationRequester,
|
|
23
|
-
type ElevationResult,
|
|
24
|
-
type ElevationSessionIdentity,
|
|
25
|
-
} from "../elevation/grant-ledger";
|
|
26
|
-
import { type BrokerOwnerPrincipal, readBrokerOwnerPrincipal } from "../elevation/owner";
|
|
27
|
-
import { findOperation } from "../protocol/operation-registry";
|
|
28
9
|
import {
|
|
29
10
|
type DirectoryMigrationPolicy,
|
|
30
11
|
listManagedSessionCandidates,
|
|
@@ -45,16 +26,17 @@ import {
|
|
|
45
26
|
readBrokerDiscovery,
|
|
46
27
|
redactBrokerDiscovery,
|
|
47
28
|
} from "./discovery";
|
|
48
|
-
import { deriveIdempotencyIdentity
|
|
29
|
+
import { deriveIdempotencyIdentity } from "./identity";
|
|
30
|
+
|
|
49
31
|
import { canonicalDeleteLocatorPath, executeLifecycle, isCanonicalSessionId } from "./lifecycle";
|
|
32
|
+
|
|
50
33
|
import {
|
|
51
34
|
type LifecycleDurableEffectsReceipt,
|
|
52
35
|
LifecycleLedger,
|
|
53
36
|
type LifecycleStartupFailureReceipt,
|
|
54
37
|
type LifecycleState,
|
|
55
38
|
} from "./lifecycle-ledger";
|
|
56
|
-
import {
|
|
57
|
-
import { type IndexedSession, SessionIndex, type SessionList } from "./session-index";
|
|
39
|
+
import { type IndexedSession, isSessionAuthorityEligible, SessionIndex, type SessionList } from "./session-index";
|
|
58
40
|
import { BrokerTransport } from "./transport";
|
|
59
41
|
|
|
60
42
|
export interface BrokerSettings {
|
|
@@ -90,7 +72,6 @@ export type BrokerErrorCode =
|
|
|
90
72
|
| "not_found"
|
|
91
73
|
| "live_session"
|
|
92
74
|
| "cleanup_pending"
|
|
93
|
-
| "ambiguous_session"
|
|
94
75
|
| (string & {});
|
|
95
76
|
|
|
96
77
|
export type BrokerCleanupIdentity = {
|
|
@@ -190,35 +171,6 @@ export type BrokerResponse =
|
|
|
190
171
|
};
|
|
191
172
|
const error = (code: BrokerErrorCode, message: string): BrokerResponse => ({ ok: false, error: { code, message } });
|
|
192
173
|
|
|
193
|
-
function elevationResultToBroker<T>(result: ElevationResult<T>): BrokerResponse {
|
|
194
|
-
if (result.ok) return { ok: true, result: result.value };
|
|
195
|
-
return error(result.error.code as BrokerErrorCode, result.error.message);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function elevationAnswerToBroker(result: ElevationResult<ElevationAnswerValue>): BrokerResponse {
|
|
199
|
-
if (!result.ok) return elevationResultToBroker(result);
|
|
200
|
-
const { outcome, grant } = result.value;
|
|
201
|
-
if (outcome === "granted")
|
|
202
|
-
return { ok: true, result: { elevationRequestId: grant.elevationRequestId, state: grant.state, outcome, grant } };
|
|
203
|
-
return error(outcome as BrokerErrorCode, `elevation ${outcome}: ${grant.elevationRequestId}`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Result of resolving the session behind an elevation grant/request. An
|
|
208
|
-
* ambiguous session (C6) can never bind an exact endpoint identity.
|
|
209
|
-
*/
|
|
210
|
-
type ElevationSessionResolution =
|
|
211
|
-
| { kind: "identity"; identity: ElevationSessionIdentity }
|
|
212
|
-
| { kind: "ambiguous" }
|
|
213
|
-
| { kind: "unavailable" };
|
|
214
|
-
function sameElevationSessionIdentity(left: ElevationSessionIdentity, right: ElevationSessionIdentity): boolean {
|
|
215
|
-
return (
|
|
216
|
-
left.sessionId === right.sessionId &&
|
|
217
|
-
path.resolve(left.endpointStateRoot) === path.resolve(right.endpointStateRoot) &&
|
|
218
|
-
left.endpointGeneration === right.endpointGeneration &&
|
|
219
|
-
left.endpointIncarnation === right.endpointIncarnation
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
174
|
function isCleanupPending(response: BrokerResponse): boolean {
|
|
223
175
|
return !response.ok && response.error.code === "cleanup_pending" && response.error.cleanup !== undefined;
|
|
224
176
|
}
|
|
@@ -231,12 +183,6 @@ function pendingCleanupSessionId(response: BrokerResponse): string | undefined {
|
|
|
231
183
|
return typeof response.error.cleanup?.sessionId === "string" ? response.error.cleanup.sessionId : undefined;
|
|
232
184
|
}
|
|
233
185
|
|
|
234
|
-
function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
235
|
-
if (response.ok) return "terminal_ok";
|
|
236
|
-
if (isCleanupPending(response)) return "effect_started";
|
|
237
|
-
return response.error.code === "terminal_uncertain" ? "terminal_uncertain" : "terminal_error";
|
|
238
|
-
}
|
|
239
|
-
|
|
240
186
|
const LIFECYCLE_OPERATIONS = new Set([
|
|
241
187
|
"session.create",
|
|
242
188
|
"session.fork",
|
|
@@ -245,6 +191,15 @@ const LIFECYCLE_OPERATIONS = new Set([
|
|
|
245
191
|
"session.delete",
|
|
246
192
|
]);
|
|
247
193
|
|
|
194
|
+
function lifecycleFingerprint(operation: string, input: unknown): string {
|
|
195
|
+
return createHash("sha256").update(JSON.stringify({ operation, input })).digest("hex");
|
|
196
|
+
}
|
|
197
|
+
function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
198
|
+
if (response.ok) return "terminal_ok";
|
|
199
|
+
if (isCleanupPending(response)) return "effect_started";
|
|
200
|
+
return response.error.code === "terminal_uncertain" ? "terminal_uncertain" : "terminal_error";
|
|
201
|
+
}
|
|
202
|
+
|
|
248
203
|
type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
|
|
249
204
|
|
|
250
205
|
type SessionListCursor = {
|
|
@@ -380,6 +335,25 @@ function canonicalJson(value: unknown): string {
|
|
|
380
335
|
.join(",")}}`;
|
|
381
336
|
}
|
|
382
337
|
|
|
338
|
+
function credentialFreeLifecycleResponse(value: unknown): unknown {
|
|
339
|
+
if (Array.isArray(value)) return value.map(credentialFreeLifecycleResponse);
|
|
340
|
+
if (value === null || typeof value !== "object") return value;
|
|
341
|
+
const output: Record<string, unknown> = {};
|
|
342
|
+
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
|
343
|
+
if (key === "token" || key === "url" || (key === "endpoint" && nested !== null && typeof nested === "object"))
|
|
344
|
+
continue;
|
|
345
|
+
output[key] = credentialFreeLifecycleResponse(nested);
|
|
346
|
+
}
|
|
347
|
+
return output;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
type LifecycleReplayEndpoint = {
|
|
351
|
+
endpoint: Record<string, unknown>;
|
|
352
|
+
endpointGeneration: number;
|
|
353
|
+
pid: number;
|
|
354
|
+
endpointMtimeMs: number;
|
|
355
|
+
};
|
|
356
|
+
|
|
383
357
|
type EndpointAuthority = { endpointGeneration?: number; endpointIncarnation?: string };
|
|
384
358
|
function endpointIncarnation(
|
|
385
359
|
record: Pick<IndexedSession, "endpointGeneration" | "endpointMtimeMs" | "pid">,
|
|
@@ -433,6 +407,7 @@ function matchesEndpointAuthority(record: IndexedSession, authority: EndpointAut
|
|
|
433
407
|
function sameEndpointRecord(expected: IndexedSession, current: IndexedSession): boolean {
|
|
434
408
|
return (
|
|
435
409
|
current.live &&
|
|
410
|
+
isSessionAuthorityEligible(current) &&
|
|
436
411
|
current.endpointGeneration === expected.endpointGeneration &&
|
|
437
412
|
current.pid === expected.pid &&
|
|
438
413
|
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
@@ -465,14 +440,13 @@ function lifecycleTarget(operation: string, input: Record<string, unknown>): unk
|
|
|
465
440
|
case "session.resume":
|
|
466
441
|
case "session.close":
|
|
467
442
|
case "session.delete":
|
|
468
|
-
return {
|
|
443
|
+
return { sessionId: id };
|
|
469
444
|
default:
|
|
470
445
|
return { operation, root, sessionId: id };
|
|
471
446
|
}
|
|
472
447
|
}
|
|
473
448
|
|
|
474
449
|
const BROKER_LOCK_RECORD = "owner.json";
|
|
475
|
-
const BROKER_INDEX_COMPACT_INTERVAL_MS = 6 * 60 * 60 * 1_000;
|
|
476
450
|
const BROKER_LOCK_STARTUP_WAIT_MS = 1_000;
|
|
477
451
|
const BROKER_LOCK_RETRY_MS = 10;
|
|
478
452
|
|
|
@@ -780,28 +754,11 @@ const terminalPersistenceHooksForTest = new WeakMap<Broker, () => void>();
|
|
|
780
754
|
const ambiguityGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
781
755
|
const publicationObservationOverridesForTest = new WeakMap<Broker, BrokerPublicationObservation>();
|
|
782
756
|
const lockArtifactGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
783
|
-
function elevationLedgerEnabledForBroker(): boolean {
|
|
784
|
-
const raw = (process.env.GJC_SDK_ELEVATION_ENABLED ?? "").trim().toLowerCase();
|
|
785
|
-
return raw === "1" || raw === "true";
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/** Recursive reserved-key rejection: the claim selector never travels inside approved input. */
|
|
789
|
-
function containsReservedKey(value: unknown, key: string, seen = new Set<object>()): boolean {
|
|
790
|
-
if (!value || typeof value !== "object" || seen.has(value)) return false;
|
|
791
|
-
seen.add(value);
|
|
792
|
-
if (Array.isArray(value)) return value.some(item => containsReservedKey(item, key, seen));
|
|
793
|
-
return (
|
|
794
|
-
Object.hasOwn(value, key) ||
|
|
795
|
-
Object.values(value as Record<string, unknown>).some(nested => containsReservedKey(nested, key, seen))
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
757
|
|
|
799
758
|
export class Broker {
|
|
800
759
|
readonly settings: ResolvedBrokerSettings;
|
|
801
760
|
readonly index: SessionIndex;
|
|
802
761
|
readonly ledger: LifecycleLedger;
|
|
803
|
-
readonly elevation: ElevationLedger;
|
|
804
|
-
readonly operationReceipts: OperationReceiptLedger;
|
|
805
762
|
discovery: BrokerDiscovery | null = null;
|
|
806
763
|
#lock: string;
|
|
807
764
|
#owner = randomBytes(12).toString("hex");
|
|
@@ -820,12 +777,6 @@ export class Broker {
|
|
|
820
777
|
#completion!: Promise<void>;
|
|
821
778
|
#resolveCompletion!: () => void;
|
|
822
779
|
#rejectCompletion!: (error: unknown) => void;
|
|
823
|
-
/** Broker tenure counter: increments per start so a restart is a new epoch. */
|
|
824
|
-
#epoch = 0;
|
|
825
|
-
#incarnation: string | null = null;
|
|
826
|
-
#elevationEnforced: boolean;
|
|
827
|
-
#compactTimer: NodeJS.Timeout | null = null;
|
|
828
|
-
#operatorTimer: NodeJS.Timeout | null = null;
|
|
829
780
|
constructor(settings: BrokerSettings) {
|
|
830
781
|
this.settings = {
|
|
831
782
|
agentDir: settings.agentDir,
|
|
@@ -836,10 +787,7 @@ export class Broker {
|
|
|
836
787
|
};
|
|
837
788
|
this.index = new SessionIndex(settings.agentDir);
|
|
838
789
|
this.ledger = new LifecycleLedger(settings.agentDir);
|
|
839
|
-
this.elevation = new ElevationLedger(settings.agentDir);
|
|
840
|
-
this.operationReceipts = new OperationReceiptLedger(settings.agentDir);
|
|
841
790
|
this.#lock = path.join(settings.agentDir, "sdk", "broker.lock");
|
|
842
|
-
this.#elevationEnforced = elevationLedgerEnabledForBroker();
|
|
843
791
|
const completion = Promise.withResolvers<void>();
|
|
844
792
|
this.#completion = completion.promise;
|
|
845
793
|
this.#resolveCompletion = completion.resolve;
|
|
@@ -992,15 +940,10 @@ export class Broker {
|
|
|
992
940
|
this.#startupAdmissions = new StartupAdmissionQueue(sdkHostStartupConcurrency());
|
|
993
941
|
}
|
|
994
942
|
this.#stopping = false;
|
|
995
|
-
this.#epoch += 1;
|
|
996
943
|
this.#publicationState = "healthy-owned";
|
|
997
944
|
this.#lossAt = null;
|
|
998
945
|
this.#ambiguousAt = null;
|
|
999
|
-
await Promise.all([
|
|
1000
|
-
this.ledger.assertSupportedStateVersions(),
|
|
1001
|
-
this.elevation.assertSupportedStateVersions(),
|
|
1002
|
-
readBrokerDiscovery(this.settings.agentDir),
|
|
1003
|
-
]);
|
|
946
|
+
await Promise.all([this.ledger.assertSupportedStateVersions(), readBrokerDiscovery(this.settings.agentDir)]);
|
|
1004
947
|
await fs.mkdir(path.dirname(this.#lock), { recursive: true, mode: 0o700 });
|
|
1005
948
|
for (;;) {
|
|
1006
949
|
try {
|
|
@@ -1047,20 +990,9 @@ export class Broker {
|
|
|
1047
990
|
try {
|
|
1048
991
|
await this.index.open();
|
|
1049
992
|
await this.ledger.open();
|
|
1050
|
-
await this.elevation.open();
|
|
1051
|
-
await this.operationReceipts.open();
|
|
1052
|
-
if (this.#elevationEnforced)
|
|
1053
|
-
await fs.mkdir(path.join(this.settings.agentDir, "sdk", "elevation", "operator"), {
|
|
1054
|
-
recursive: true,
|
|
1055
|
-
mode: 0o700,
|
|
1056
|
-
});
|
|
1057
|
-
// Broker-scheduled compaction (C3): apply the retention policy once at
|
|
1058
|
-
// startup and again every six hours, independently of log rotation.
|
|
1059
|
-
await this.#compactSessionIndex();
|
|
1060
993
|
const now = Date.now();
|
|
1061
994
|
const incarnation = brokerProcessIncarnation(process.pid);
|
|
1062
995
|
if (!incarnation) throw new Error("Broker process incarnation is unavailable.");
|
|
1063
|
-
this.#incarnation = incarnation;
|
|
1064
996
|
const token = newBrokerToken();
|
|
1065
997
|
this.#transport = new BrokerTransport(this, token, this.settings.port);
|
|
1066
998
|
const port = await this.#transport.start();
|
|
@@ -1085,8 +1017,7 @@ export class Broker {
|
|
|
1085
1017
|
Math.min(BROKER_PUBLICATION_CADENCE_MS, Math.floor(this.settings.heartbeatTtlMs / 3)),
|
|
1086
1018
|
);
|
|
1087
1019
|
this.#heartbeatTimer = setInterval(() => void this.#watchPublication(), cadenceMs);
|
|
1088
|
-
|
|
1089
|
-
if (this.#elevationEnforced) this.#operatorTimer = setInterval(() => void this.#pollOperatorDirectives(), 250);
|
|
1020
|
+
await this.#checkpointSessionHeartbeats();
|
|
1090
1021
|
return this.discovery;
|
|
1091
1022
|
} catch (error) {
|
|
1092
1023
|
await this.#transport?.stop();
|
|
@@ -1153,7 +1084,7 @@ export class Broker {
|
|
|
1153
1084
|
this.#lossAt = null;
|
|
1154
1085
|
this.#ambiguousAt = null;
|
|
1155
1086
|
if (writeHeartbeat) await this.#writeHeartbeat();
|
|
1156
|
-
await this.#checkpointSessionHeartbeats();
|
|
1087
|
+
if (this.#publicationState === "healthy-owned") await this.#checkpointSessionHeartbeats();
|
|
1157
1088
|
return;
|
|
1158
1089
|
}
|
|
1159
1090
|
this.#fence(observation === "ambiguous" ? "observation-ambiguous" : "suspect-unpublished");
|
|
@@ -1180,31 +1111,15 @@ export class Broker {
|
|
|
1180
1111
|
if (this.#publicationState !== "healthy-owned") return;
|
|
1181
1112
|
await this.#writeHeartbeat();
|
|
1182
1113
|
}
|
|
1183
|
-
|
|
1184
|
-
/**
|
|
1185
|
-
* Production coalesced heartbeat checkpoint seam (C2): delegates to the session
|
|
1186
|
-
* index writer, which appends at most one `host_heartbeat` per session per
|
|
1187
|
-
* {@link SESSION_HEARTBEAT_INTERVAL_MS} for hosts that are alive and, when recorded,
|
|
1188
|
-
* still carry the same OS process incarnation. Returns the number of checkpoints
|
|
1189
|
-
* written. Used by the periodic publication watch, by session reads so a fresh
|
|
1190
|
-
* registration is observed as live, and by tests.
|
|
1191
|
-
*/
|
|
1114
|
+
/** Re-observes provably live session hosts and checkpoints their liveness. */
|
|
1192
1115
|
async heartbeatSessions(now = Date.now()): Promise<number> {
|
|
1193
1116
|
return await this.index.checkpointLiveHeartbeats(now);
|
|
1194
1117
|
}
|
|
1195
|
-
|
|
1196
1118
|
async #checkpointSessionHeartbeats(): Promise<void> {
|
|
1197
1119
|
try {
|
|
1198
1120
|
await this.heartbeatSessions();
|
|
1199
|
-
} catch {
|
|
1200
|
-
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
async #compactSessionIndex(): Promise<void> {
|
|
1204
|
-
try {
|
|
1205
|
-
await this.index.compact();
|
|
1206
|
-
} catch {
|
|
1207
|
-
// Compaction is best-effort maintenance; the next scheduled pass retries.
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
logger.warn(`sdk broker: session heartbeat checkpoint failed: ${String(error)}`);
|
|
1208
1123
|
}
|
|
1209
1124
|
}
|
|
1210
1125
|
async #complete(mode: BrokerStopMode): Promise<void> {
|
|
@@ -1220,10 +1135,6 @@ export class Broker {
|
|
|
1220
1135
|
if (mode === "lost-root") this.#startupAdmissions.close();
|
|
1221
1136
|
if (this.#heartbeatTimer) clearInterval(this.#heartbeatTimer);
|
|
1222
1137
|
this.#heartbeatTimer = null;
|
|
1223
|
-
if (this.#compactTimer) clearInterval(this.#compactTimer);
|
|
1224
|
-
this.#compactTimer = null;
|
|
1225
|
-
if (this.#operatorTimer) clearInterval(this.#operatorTimer);
|
|
1226
|
-
this.#operatorTimer = null;
|
|
1227
1138
|
this.#completionTask = (async () => {
|
|
1228
1139
|
await this.#transport?.stop();
|
|
1229
1140
|
this.#transport = null;
|
|
@@ -1303,16 +1214,54 @@ export class Broker {
|
|
|
1303
1214
|
const authority = expectedEndpointAuthority(input);
|
|
1304
1215
|
if ("ok" in authority) return authority;
|
|
1305
1216
|
await this.index.refresh();
|
|
1306
|
-
|
|
1307
|
-
|
|
1217
|
+
let record = this.index.listSessions().sessions.find(session => session.sessionId === sessionId);
|
|
1218
|
+
if (
|
|
1219
|
+
record &&
|
|
1220
|
+
!record.live &&
|
|
1221
|
+
!record.terminal &&
|
|
1222
|
+
!record.terminalUncertain &&
|
|
1223
|
+
isSessionAuthorityEligible(record)
|
|
1224
|
+
) {
|
|
1225
|
+
await this.heartbeatSessions();
|
|
1226
|
+
await this.index.refresh();
|
|
1227
|
+
record = this.index.listSessions().sessions.find(session => session.sessionId === sessionId);
|
|
1228
|
+
}
|
|
1308
1229
|
if (!record) return error("resource_gone", "session is not indexed");
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
if (record.
|
|
1312
|
-
if (!record.live || !matchesEndpointAuthority(record, authority))
|
|
1313
|
-
return error("endpoint_stale", "session endpoint is stale");
|
|
1230
|
+
if (!isSessionAuthorityEligible(record)) return error("resource_gone", "session endpoint record is gone");
|
|
1231
|
+
if (!matchesEndpointAuthority(record, authority)) return error("endpoint_stale", "session endpoint is stale");
|
|
1232
|
+
if (!record.live) return error("resource_gone", "session endpoint record is gone");
|
|
1314
1233
|
return this.#readEndpoint(record, authority);
|
|
1315
1234
|
}
|
|
1235
|
+
async #readLifecycleReplayEndpoint(sessionId: string): Promise<LifecycleReplayEndpoint | BrokerResponse> {
|
|
1236
|
+
await this.index.refresh();
|
|
1237
|
+
const record = this.index.listSessions().sessions.find(session => session.sessionId === sessionId);
|
|
1238
|
+
if (!record) return error("resource_gone", "session endpoint record is gone");
|
|
1239
|
+
if (record.terminalUncertain)
|
|
1240
|
+
return error("terminal_uncertain", "Session ownership is uncertain and cannot be replayed safely");
|
|
1241
|
+
if (!isSessionAuthorityEligible(record) || !record.live)
|
|
1242
|
+
return error("resource_gone", "session endpoint record is gone");
|
|
1243
|
+
const endpointMtimeMs = record.endpointMtimeMs;
|
|
1244
|
+
if (
|
|
1245
|
+
!Number.isSafeInteger(record.endpointGeneration) ||
|
|
1246
|
+
record.endpointGeneration <= 0 ||
|
|
1247
|
+
!Number.isSafeInteger(record.pid) ||
|
|
1248
|
+
record.pid <= 0 ||
|
|
1249
|
+
endpointMtimeMs === undefined ||
|
|
1250
|
+
!Number.isFinite(endpointMtimeMs) ||
|
|
1251
|
+
endpointMtimeMs <= 0
|
|
1252
|
+
)
|
|
1253
|
+
return error("endpoint_stale", "session endpoint authority is incomplete");
|
|
1254
|
+
const endpoint = await this.#readEndpoint(record, {});
|
|
1255
|
+
if (!endpoint.ok) return endpoint;
|
|
1256
|
+
if (endpoint.result === null || typeof endpoint.result !== "object" || Array.isArray(endpoint.result))
|
|
1257
|
+
return error("endpoint_stale", "session endpoint is malformed");
|
|
1258
|
+
return {
|
|
1259
|
+
endpoint: endpoint.result as Record<string, unknown>,
|
|
1260
|
+
endpointGeneration: record.endpointGeneration,
|
|
1261
|
+
pid: record.pid,
|
|
1262
|
+
endpointMtimeMs,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1316
1265
|
async #readEndpoint(record: IndexedSession, authority: EndpointAuthority): Promise<BrokerResponse> {
|
|
1317
1266
|
if (!isCanonicalSessionId(record.sessionId))
|
|
1318
1267
|
return error("invalid_input", "indexed sessionId is not a canonical safe identifier");
|
|
@@ -1326,7 +1275,7 @@ export class Broker {
|
|
|
1326
1275
|
endpoint.pid !== record.pid ||
|
|
1327
1276
|
endpoint.stale === true ||
|
|
1328
1277
|
record.endpointMtimeMs === undefined ||
|
|
1329
|
-
metadata.mtimeMs
|
|
1278
|
+
Math.abs(metadata.mtimeMs - record.endpointMtimeMs) > 0.001
|
|
1330
1279
|
)
|
|
1331
1280
|
return error("endpoint_stale", "session endpoint is stale");
|
|
1332
1281
|
await this.index.refresh();
|
|
@@ -1374,7 +1323,10 @@ export class Broker {
|
|
|
1374
1323
|
offset: 0,
|
|
1375
1324
|
expiresAt: Date.now() + SESSION_LIST_CURSOR_TTL_MS,
|
|
1376
1325
|
};
|
|
1377
|
-
const sessions = snapshot.sessions.slice(snapshot.offset, snapshot.offset + snapshot.limit)
|
|
1326
|
+
const sessions = snapshot.sessions.slice(snapshot.offset, snapshot.offset + snapshot.limit).map(session => {
|
|
1327
|
+
const { lifecycleRequestId: _lifecycleRequestId, ...publicSession } = session;
|
|
1328
|
+
return publicSession;
|
|
1329
|
+
});
|
|
1378
1330
|
const offset = snapshot.offset + sessions.length;
|
|
1379
1331
|
if (offset >= snapshot.sessions.length && typeof cursor === "string") this.#sessionListCursors.delete(cursor);
|
|
1380
1332
|
const continuationCursor =
|
|
@@ -1396,18 +1348,13 @@ export class Broker {
|
|
|
1396
1348
|
indexSeq: snapshot.indexSeq,
|
|
1397
1349
|
};
|
|
1398
1350
|
}
|
|
1399
|
-
handleRequest(
|
|
1400
|
-
operation: string,
|
|
1401
|
-
input: Record<string, unknown>,
|
|
1402
|
-
idempotencyKey?: string,
|
|
1403
|
-
elevationRequestId?: string,
|
|
1404
|
-
): Promise<BrokerResponse> {
|
|
1351
|
+
handleRequest(operation: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<BrokerResponse> {
|
|
1405
1352
|
if (this.#stopping || (this.#publication !== null && this.#publicationState !== "healthy-owned"))
|
|
1406
1353
|
return Promise.resolve(error("unavailable", "broker publication is unavailable"));
|
|
1407
1354
|
let release!: () => void;
|
|
1408
1355
|
const admission = new Promise<void>(resolve => (release = resolve));
|
|
1409
1356
|
this.#admitted.add(admission);
|
|
1410
|
-
return this.#handleRequest(operation, input, idempotencyKey
|
|
1357
|
+
return this.#handleRequest(operation, input, idempotencyKey).finally(() => {
|
|
1411
1358
|
release();
|
|
1412
1359
|
this.#admitted.delete(admission);
|
|
1413
1360
|
});
|
|
@@ -1416,22 +1363,14 @@ export class Broker {
|
|
|
1416
1363
|
operation: string,
|
|
1417
1364
|
input: Record<string, unknown>,
|
|
1418
1365
|
idempotencyKey?: string,
|
|
1419
|
-
elevationRequestId?: string,
|
|
1420
1366
|
): Promise<BrokerResponse> {
|
|
1421
1367
|
if (this.#stopping) return error("broker_restarting", "broker is stopping");
|
|
1422
|
-
const fingerprint =
|
|
1368
|
+
const fingerprint = lifecycleFingerprint(operation, input);
|
|
1423
1369
|
const normalization = normalizeBrokerInput(operation, input);
|
|
1424
1370
|
if (isBrokerResponse(normalization)) return normalization;
|
|
1425
|
-
const approvedInput = input;
|
|
1426
1371
|
input = normalization.input;
|
|
1427
1372
|
if (operation === "session.list") {
|
|
1428
|
-
if (input.cursor === undefined)
|
|
1429
|
-
await this.index.refresh();
|
|
1430
|
-
// Self-healing heartbeat checkpoint (C2): a host registered since the
|
|
1431
|
-
// broker's last pass is observed as live on this read, so a fresh
|
|
1432
|
-
// registration never reads as unknown (while a restart still does).
|
|
1433
|
-
await this.heartbeatSessions();
|
|
1434
|
-
}
|
|
1373
|
+
if (input.cursor === undefined) await this.index.refresh();
|
|
1435
1374
|
const page = this.#sessionListPage(input, this.index.listSessions());
|
|
1436
1375
|
if (!page.ok) return page;
|
|
1437
1376
|
const pageResult = page.result as Record<string, unknown>;
|
|
@@ -1446,113 +1385,63 @@ export class Broker {
|
|
|
1446
1385
|
? listed.owned.filter(candidate => candidate.sessionId === resolveSessionId)
|
|
1447
1386
|
: [];
|
|
1448
1387
|
const match = matches.length === 1 ? matches[0] : undefined;
|
|
1388
|
+
const savedSession =
|
|
1389
|
+
match &&
|
|
1390
|
+
match.sessionId === resolveSessionId &&
|
|
1391
|
+
match.identity.nlink !== undefined &&
|
|
1392
|
+
match.identity.ctimeNs !== undefined
|
|
1393
|
+
? {
|
|
1394
|
+
id: match.sessionId,
|
|
1395
|
+
path: match.path,
|
|
1396
|
+
identity: {
|
|
1397
|
+
dev: match.identity.dev.toString(),
|
|
1398
|
+
ino: match.identity.ino.toString(),
|
|
1399
|
+
nlink: match.identity.nlink.toString(),
|
|
1400
|
+
size: match.identity.size,
|
|
1401
|
+
mtimeMs: match.identity.mtimeMs,
|
|
1402
|
+
mtimeNs: match.identity.mtimeNs.toString(),
|
|
1403
|
+
ctimeNs: match.identity.ctimeNs.toString(),
|
|
1404
|
+
sha256: match.identity.sha256,
|
|
1405
|
+
},
|
|
1406
|
+
}
|
|
1407
|
+
: undefined;
|
|
1449
1408
|
return {
|
|
1450
1409
|
...page,
|
|
1451
1410
|
result: {
|
|
1452
1411
|
...pageResult,
|
|
1453
|
-
savedSession:
|
|
1454
|
-
match && match.sessionId === resolveSessionId
|
|
1455
|
-
? { id: match.sessionId, path: match.path }
|
|
1456
|
-
: undefined,
|
|
1412
|
+
...(savedSession === undefined ? {} : { savedSession }),
|
|
1457
1413
|
},
|
|
1458
1414
|
};
|
|
1459
1415
|
}
|
|
1460
1416
|
return page;
|
|
1461
1417
|
}
|
|
1462
1418
|
if (operation === "session.get_endpoint") return this.#endpoint(input);
|
|
1463
|
-
if (operation === "elevation.issue") return this.#elevationIssueRequest(input);
|
|
1464
|
-
if (operation === "elevation.status") return this.#elevationStatusRequest(input);
|
|
1465
|
-
if (operation === "session.control") return this.#sessionControlRequest(input);
|
|
1466
1419
|
if (operation === "broker.lookup_lifecycle") {
|
|
1467
1420
|
const requestedOperation = typeof input.operation === "string" ? input.operation : undefined;
|
|
1468
|
-
const
|
|
1469
|
-
if (!idempotencyKey || !requestedOperation || !
|
|
1421
|
+
const requestedFingerprint = typeof input.fingerprint === "string" ? input.fingerprint : undefined;
|
|
1422
|
+
if (!idempotencyKey || !requestedOperation || !requestedFingerprint)
|
|
1470
1423
|
return error("invalid_input", "operation, idempotencyKey, and fingerprint are required");
|
|
1471
1424
|
if (!LIFECYCLE_OPERATIONS.has(requestedOperation))
|
|
1472
1425
|
return error("not_found", "lifecycle operation was not found");
|
|
1473
1426
|
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, requestedOperation, idempotencyKey);
|
|
1474
1427
|
const entry = this.ledger.get(identity);
|
|
1475
1428
|
if (!entry) return error("not_found", "lifecycle operation was not found");
|
|
1476
|
-
if (entry.fingerprint !==
|
|
1429
|
+
if (entry.fingerprint !== requestedFingerprint)
|
|
1477
1430
|
return error("idempotency_conflict", "lifecycle request fingerprint differs");
|
|
1478
1431
|
if (entry.state === "terminal_uncertain")
|
|
1479
|
-
return
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
// Reconcile to the terminal original response contract: callers expect
|
|
1484
|
-
// the BrokerResponse they would have received, not a lookup envelope.
|
|
1485
|
-
return (
|
|
1486
|
-
(entry.response as BrokerResponse) ?? {
|
|
1487
|
-
ok: false,
|
|
1488
|
-
error: { code: "terminal_uncertain", message: "lifecycle outcome has no recorded response" },
|
|
1489
|
-
}
|
|
1490
|
-
);
|
|
1432
|
+
return error("terminal_uncertain", "lifecycle outcome is still uncertain");
|
|
1433
|
+
return isBrokerResponse(entry.response)
|
|
1434
|
+
? entry.response
|
|
1435
|
+
: error("terminal_uncertain", "lifecycle outcome has no recorded response");
|
|
1491
1436
|
}
|
|
1437
|
+
|
|
1492
1438
|
if (!idempotencyKey) return error("invalid_input", "idempotencyKey is required for lifecycle operations");
|
|
1493
|
-
// Elevation gate (F1.3): allowlisted raw operations execute only behind a
|
|
1494
|
-
// broker-owned single-use grant. Validation is fail-closed and happens
|
|
1495
|
-
// before any lifecycle ledger write; the claim itself is consumed right
|
|
1496
|
-
// before dispatch and the truthful outcome is recorded after it.
|
|
1497
|
-
const elevationGateKind = this.#elevationGateFor(operation);
|
|
1498
|
-
let elevationRequestIdForDispatch: string | undefined;
|
|
1499
|
-
if (elevationGateKind !== undefined) {
|
|
1500
|
-
if (elevationRequestId === undefined)
|
|
1501
|
-
return error("elevation_required", "an elevation grant claim is required for this operation");
|
|
1502
|
-
if (!isElevationRequestId(elevationRequestId))
|
|
1503
|
-
return error("invalid_input", "elevationRequestId must be an RFC4122 lowercase UUID v4");
|
|
1504
|
-
const resolved = await this.elevation.get(elevationRequestId);
|
|
1505
|
-
if (!resolved.ok) {
|
|
1506
|
-
if (resolved.error.code === "not_found")
|
|
1507
|
-
return error(
|
|
1508
|
-
"elevation_required",
|
|
1509
|
-
"elevation grant was not found; a grant is required for this operation",
|
|
1510
|
-
);
|
|
1511
|
-
return elevationResultToBroker(resolved);
|
|
1512
|
-
}
|
|
1513
|
-
const grant = resolved.value.grant;
|
|
1514
|
-
if (grant.operation.kind !== elevationGateKind || grant.operation.sdkId !== operation)
|
|
1515
|
-
return error("elevation_digest_mismatch", "elevation grant was issued for a different operation");
|
|
1516
|
-
const digest = elevationRequestDigest({ kind: elevationGateKind, sdkId: operation, input: approvedInput });
|
|
1517
|
-
if (!digest.ok || digest.digest !== grant.requestDigest)
|
|
1518
|
-
return error("elevation_digest_mismatch", "elevation grant digest does not match the dispatched input");
|
|
1519
|
-
// Bind the grant to the actual lifecycle target before any claim: the
|
|
1520
|
-
// approved input must resolve to the exact session identity the grant
|
|
1521
|
-
// was issued against (C10). session.delete may target a retained
|
|
1522
|
-
// stopped session; everything else requires a live endpoint.
|
|
1523
|
-
const targetSessionId = typeof input.sessionId === "string" ? input.sessionId : undefined;
|
|
1524
|
-
if (targetSessionId !== undefined) {
|
|
1525
|
-
const target = await this.#elevationSessionResolution(targetSessionId, {
|
|
1526
|
-
allowStopped: operation === "session.delete",
|
|
1527
|
-
});
|
|
1528
|
-
if (target.kind === "ambiguous")
|
|
1529
|
-
return error("ambiguous_session", "session id maps to more than one state root");
|
|
1530
|
-
if (target.kind !== "identity")
|
|
1531
|
-
return error("target_unavailable", "session endpoint is unreachable or its identity is not provable");
|
|
1532
|
-
if (grant.sessionIdentity.sessionId !== target.identity.sessionId)
|
|
1533
|
-
return error("elevation_digest_mismatch", "elevation grant was issued for a different session target");
|
|
1534
|
-
if (!sameElevationSessionIdentity(grant.sessionIdentity, target.identity))
|
|
1535
|
-
return error("endpoint_stale", "session endpoint identity no longer matches the elevation grant");
|
|
1536
|
-
}
|
|
1537
|
-
elevationRequestIdForDispatch = elevationRequestId;
|
|
1538
|
-
}
|
|
1539
1439
|
const target = createHash("sha256")
|
|
1540
1440
|
.update(canonicalJson(lifecycleTarget(operation, input)))
|
|
1541
1441
|
.digest("hex");
|
|
1542
|
-
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, operation, idempotencyKey);
|
|
1442
|
+
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, operation, idempotencyKey, target);
|
|
1543
1443
|
const operationKey = `${operation}\0${idempotencyKey}`;
|
|
1544
|
-
|
|
1545
|
-
this.settings.agentDir,
|
|
1546
|
-
operation,
|
|
1547
|
-
idempotencyKey,
|
|
1548
|
-
target,
|
|
1549
|
-
);
|
|
1550
|
-
if (!this.ledger.get(identity)) {
|
|
1551
|
-
if (await this.ledger.migrateIdentity(legacyIdentity, identity, { operationKey, fingerprint })) {
|
|
1552
|
-
// Exact target-derived legacy identity migrated without granting new authority.
|
|
1553
|
-
} else if (this.ledger.findByOperationKey(operationKey) || this.ledger.hasLegacyIdentity())
|
|
1554
|
-
return error("idempotency_conflict", "legacy lifecycle request has an ambiguous target");
|
|
1555
|
-
}
|
|
1444
|
+
|
|
1556
1445
|
let reconstructedDeleteCleanup: BrokerCleanupEvidence | undefined;
|
|
1557
1446
|
if (operation === "session.delete" && input.cwd === undefined && input.sessionPath === undefined) {
|
|
1558
1447
|
const entry = this.ledger.get(identity);
|
|
@@ -1618,19 +1507,39 @@ export class Broker {
|
|
|
1618
1507
|
await prev;
|
|
1619
1508
|
try {
|
|
1620
1509
|
const beforeBegin = this.ledger.get(identity);
|
|
1621
|
-
const begun = await this.ledger.begin(identity, requestHash, {
|
|
1622
|
-
operationKey,
|
|
1623
|
-
fingerprint,
|
|
1624
|
-
});
|
|
1510
|
+
const begun = await this.ledger.begin(identity, requestHash, { operationKey, fingerprint });
|
|
1625
1511
|
if (begun.kind === "replay") {
|
|
1626
1512
|
const replay = begun.entry.response as BrokerResponse;
|
|
1627
1513
|
const cleanup = cleanupFromResponse(replay) ?? reconstructedDeleteCleanup;
|
|
1628
|
-
if (!cleanup)
|
|
1514
|
+
if (!cleanup) {
|
|
1515
|
+
if (
|
|
1516
|
+
replay.ok &&
|
|
1517
|
+
(operation === "session.create" || operation === "session.fork" || operation === "session.resume") &&
|
|
1518
|
+
typeof (replay.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
|
|
1519
|
+
) {
|
|
1520
|
+
const refreshed = await this.#readLifecycleReplayEndpoint(
|
|
1521
|
+
(replay.result as { sessionId: string }).sessionId,
|
|
1522
|
+
);
|
|
1523
|
+
if (isBrokerResponse(refreshed)) return refreshed;
|
|
1524
|
+
return {
|
|
1525
|
+
ok: true,
|
|
1526
|
+
result: {
|
|
1527
|
+
...(replay.result as Record<string, unknown>),
|
|
1528
|
+
endpointGeneration: refreshed.endpointGeneration,
|
|
1529
|
+
pid: refreshed.pid,
|
|
1530
|
+
endpointMtimeMs: refreshed.endpointMtimeMs,
|
|
1531
|
+
endpoint: refreshed.endpoint,
|
|
1532
|
+
},
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
return replay;
|
|
1536
|
+
}
|
|
1629
1537
|
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
1630
1538
|
const response = outcome.response;
|
|
1539
|
+
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
1631
1540
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
1632
|
-
response,
|
|
1633
|
-
responseDigest: createHash("sha256").update(canonicalJson(
|
|
1541
|
+
response: storedResponse,
|
|
1542
|
+
responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
|
|
1634
1543
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
1635
1544
|
...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
|
|
1636
1545
|
});
|
|
@@ -1645,64 +1554,28 @@ export class Broker {
|
|
|
1645
1554
|
return replay ?? error("terminal_uncertain", "prior lifecycle operation outcome is uncertain");
|
|
1646
1555
|
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
1647
1556
|
const response = outcome.response;
|
|
1557
|
+
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
1648
1558
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
1649
1559
|
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
1650
|
-
response,
|
|
1651
|
-
responseDigest: createHash("sha256").update(canonicalJson(
|
|
1560
|
+
response: storedResponse,
|
|
1561
|
+
responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
|
|
1652
1562
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
1653
1563
|
...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
|
|
1654
1564
|
});
|
|
1655
1565
|
return response;
|
|
1656
1566
|
}
|
|
1657
1567
|
if (begun.kind === "in_progress") return error("broker_restarting", "lifecycle operation is in progress");
|
|
1658
|
-
|
|
1659
|
-
// A claimed grant that crashes before outcome recording is replayed
|
|
1660
|
-
// truthfully as consumed/uncertain; retry requires a new grant.
|
|
1661
|
-
if (elevationRequestIdForDispatch !== undefined) {
|
|
1662
|
-
const claimed = await this.elevationClaim({ elevationRequestId: elevationRequestIdForDispatch });
|
|
1663
|
-
if (!claimed.ok) return claimed;
|
|
1664
|
-
}
|
|
1665
|
-
let outcome: Awaited<ReturnType<typeof executeLifecycle>>;
|
|
1666
|
-
try {
|
|
1667
|
-
outcome = await executeLifecycle(this, operation, input, identity);
|
|
1668
|
-
} catch (cause) {
|
|
1669
|
-
if (elevationRequestIdForDispatch !== undefined)
|
|
1670
|
-
await this.elevationDispatch({
|
|
1671
|
-
elevationRequestId: elevationRequestIdForDispatch,
|
|
1672
|
-
outcome: {
|
|
1673
|
-
status: "failed",
|
|
1674
|
-
code: "unavailable",
|
|
1675
|
-
message: cause instanceof Error ? cause.message : String(cause),
|
|
1676
|
-
dispatchedAt: Date.now(),
|
|
1677
|
-
},
|
|
1678
|
-
});
|
|
1679
|
-
throw cause;
|
|
1680
|
-
}
|
|
1568
|
+
const outcome = await executeLifecycle(this, operation, input, identity);
|
|
1681
1569
|
const response = outcome.response;
|
|
1682
|
-
|
|
1683
|
-
if (elevationRequestIdForDispatch !== undefined) {
|
|
1684
|
-
const dispatchedAt = Date.now();
|
|
1685
|
-
const recorded = await this.elevationDispatch({
|
|
1686
|
-
elevationRequestId: elevationRequestIdForDispatch,
|
|
1687
|
-
outcome: response.ok
|
|
1688
|
-
? { status: "ok", dispatchedAt }
|
|
1689
|
-
: {
|
|
1690
|
-
status: "failed",
|
|
1691
|
-
code: response.error.code,
|
|
1692
|
-
message: response.error.message,
|
|
1693
|
-
dispatchedAt,
|
|
1694
|
-
},
|
|
1695
|
-
});
|
|
1696
|
-
if (!recorded.ok) return recorded;
|
|
1697
|
-
}
|
|
1570
|
+
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
1698
1571
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
1699
1572
|
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
1700
1573
|
resultSessionId:
|
|
1701
1574
|
response.ok && typeof (response.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
|
|
1702
1575
|
? (response.result as { sessionId: string }).sessionId
|
|
1703
1576
|
: undefined,
|
|
1704
|
-
response,
|
|
1705
|
-
responseDigest: createHash("sha256").update(canonicalJson(
|
|
1577
|
+
response: storedResponse,
|
|
1578
|
+
responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
|
|
1706
1579
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
1707
1580
|
...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
|
|
1708
1581
|
});
|
|
@@ -1710,7 +1583,7 @@ export class Broker {
|
|
|
1710
1583
|
const persisted = await this.ledger.readTerminal(identity, requestHash);
|
|
1711
1584
|
const persistenceVerified =
|
|
1712
1585
|
persisted !== undefined &&
|
|
1713
|
-
canonicalJson(persisted.response) === canonicalJson(
|
|
1586
|
+
canonicalJson(persisted.response) === canonicalJson(storedResponse) &&
|
|
1714
1587
|
canonicalJson(persisted.durableEffects) === canonicalJson(outcome.durableEffects) &&
|
|
1715
1588
|
canonicalJson(persisted.startupFailure) === canonicalJson(outcome.startupFailure);
|
|
1716
1589
|
if (!persistenceVerified) {
|
|
@@ -1734,460 +1607,6 @@ export class Broker {
|
|
|
1734
1607
|
if (this.#chains.get(target) === current) this.#chains.delete(target);
|
|
1735
1608
|
}
|
|
1736
1609
|
}
|
|
1737
|
-
|
|
1738
|
-
#elevationGateFor(operation: string): "global" | undefined {
|
|
1739
|
-
return this.#elevationEnforced && isElevatableOperation("global", operation) ? "global" : undefined;
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
async #elevationIssueRequest(input: Record<string, unknown>): Promise<BrokerResponse> {
|
|
1743
|
-
const sessionId = input.sessionId;
|
|
1744
|
-
if (typeof sessionId !== "string" || !isCanonicalSessionId(sessionId))
|
|
1745
|
-
return error("invalid_input", "sessionId must be a canonical safe identifier");
|
|
1746
|
-
const rawOperation = input.operation;
|
|
1747
|
-
if (typeof rawOperation !== "object" || rawOperation === null || Array.isArray(rawOperation))
|
|
1748
|
-
return error("invalid_input", "operation must be an object with kind and sdkId");
|
|
1749
|
-
const { kind, sdkId } = rawOperation as { kind?: unknown; sdkId?: unknown };
|
|
1750
|
-
if (
|
|
1751
|
-
typeof kind !== "string" ||
|
|
1752
|
-
(kind !== "control" && kind !== "query" && kind !== "global") ||
|
|
1753
|
-
typeof sdkId !== "string" ||
|
|
1754
|
-
!findOperation(kind, sdkId)
|
|
1755
|
-
)
|
|
1756
|
-
return error("invalid_request", `unknown elevation operation: ${String(kind)}:${String(sdkId)}`);
|
|
1757
|
-
if (!isElevatableOperation(kind, sdkId))
|
|
1758
|
-
return error("elevation_not_allowlisted", `${sdkId} does not require elevation`);
|
|
1759
|
-
const approved = input.input;
|
|
1760
|
-
if (typeof approved !== "object" || approved === null || Array.isArray(approved))
|
|
1761
|
-
return error("invalid_input", "input must be the approved operation arguments");
|
|
1762
|
-
if (containsReservedKey(approved, "elevationRequestId"))
|
|
1763
|
-
return error(
|
|
1764
|
-
"invalid_input",
|
|
1765
|
-
"elevationRequestId is reserved and cannot appear inside the approved operation input",
|
|
1766
|
-
);
|
|
1767
|
-
if (input.idempotencyKey !== undefined)
|
|
1768
|
-
return error(
|
|
1769
|
-
"invalid_input",
|
|
1770
|
-
"idempotencyKey is broker-frame metadata and cannot appear inside elevation.issue input",
|
|
1771
|
-
);
|
|
1772
|
-
const elevationRequestId = input.elevationRequestId;
|
|
1773
|
-
if (elevationRequestId !== undefined && !isElevationRequestId(elevationRequestId))
|
|
1774
|
-
return error("invalid_input", "elevationRequestId must be an RFC4122 lowercase UUID v4");
|
|
1775
|
-
const expiresInMs = input.expiresInMs;
|
|
1776
|
-
if (
|
|
1777
|
-
expiresInMs !== undefined &&
|
|
1778
|
-
(typeof expiresInMs !== "number" || !Number.isSafeInteger(expiresInMs) || expiresInMs <= 0)
|
|
1779
|
-
)
|
|
1780
|
-
return error("invalid_input", "expiresInMs must be a positive safe integer");
|
|
1781
|
-
let requester: ElevationRequester;
|
|
1782
|
-
if (input.requester === undefined) {
|
|
1783
|
-
requester = { source: "broker_connection", connectionId: `broker:${this.#owner}` };
|
|
1784
|
-
} else {
|
|
1785
|
-
const candidate = input.requester as { source?: unknown; connectionId?: unknown };
|
|
1786
|
-
if (
|
|
1787
|
-
candidate?.source !== "broker_connection" ||
|
|
1788
|
-
typeof candidate.connectionId !== "string" ||
|
|
1789
|
-
candidate.connectionId.length === 0
|
|
1790
|
-
)
|
|
1791
|
-
return error("invalid_input", "requester must be a broker_connection audit identity");
|
|
1792
|
-
requester = { source: "broker_connection", connectionId: candidate.connectionId };
|
|
1793
|
-
}
|
|
1794
|
-
return await this.elevationIssue({
|
|
1795
|
-
sessionId,
|
|
1796
|
-
operation: { kind, sdkId },
|
|
1797
|
-
input: approved as Record<string, unknown>,
|
|
1798
|
-
requester,
|
|
1799
|
-
...(elevationRequestId === undefined ? {} : { elevationRequestId }),
|
|
1800
|
-
...(expiresInMs === undefined ? {} : { expiresInMs }),
|
|
1801
|
-
});
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
async #elevationStatusRequest(input: Record<string, unknown>): Promise<BrokerResponse> {
|
|
1805
|
-
const elevationRequestId = input.elevationRequestId;
|
|
1806
|
-
if (typeof elevationRequestId !== "string" || !isElevationRequestId(elevationRequestId))
|
|
1807
|
-
return error("invalid_input", "elevationRequestId must be an RFC4122 lowercase UUID v4");
|
|
1808
|
-
return await this.elevationResolve(elevationRequestId);
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
#elevationClaimIdentity(): ElevationClaimIdentity {
|
|
1812
|
-
const incarnation = this.#incarnation;
|
|
1813
|
-
if (incarnation === null) throw new Error("Broker process incarnation is unavailable.");
|
|
1814
|
-
return { ownerId: this.#owner, epoch: this.#epoch, pid: process.pid, incarnation };
|
|
1815
|
-
}
|
|
1816
|
-
async #elevationPrincipal(): Promise<BrokerOwnerPrincipal> {
|
|
1817
|
-
const incarnation = this.#incarnation;
|
|
1818
|
-
if (incarnation === null) throw new Error("Broker process incarnation is unavailable.");
|
|
1819
|
-
return readBrokerOwnerPrincipal(this.settings.agentDir, process.pid, incarnation);
|
|
1820
|
-
}
|
|
1821
|
-
/**
|
|
1822
|
-
* Resolves the exact elevation session identity (C6/C10). A cross-repo
|
|
1823
|
-
* duplicate (`ambiguous`) is rejected before any endpoint credential or
|
|
1824
|
-
* elevation grant can bind; an unreachable or unprovable session is
|
|
1825
|
-
* `unavailable`. The heartbeat checkpoint pass runs first so a fresh
|
|
1826
|
-
* registration is observable as live.
|
|
1827
|
-
*
|
|
1828
|
-
* `allowStopped` is the session.delete-only exemption: a retained stopped
|
|
1829
|
-
* session keeps its exact endpoint identity tuple (generation/pid/mtime)
|
|
1830
|
-
* in the index row, so a delete grant can still be bound and dispatched
|
|
1831
|
-
* against it. Everything else fails closed unless the endpoint is live.
|
|
1832
|
-
*/
|
|
1833
|
-
async #elevationSessionResolution(
|
|
1834
|
-
sessionId: string,
|
|
1835
|
-
options?: { allowStopped?: boolean },
|
|
1836
|
-
): Promise<ElevationSessionResolution> {
|
|
1837
|
-
if (!isCanonicalSessionId(sessionId)) return { kind: "unavailable" };
|
|
1838
|
-
await this.index.refresh();
|
|
1839
|
-
await this.heartbeatSessions();
|
|
1840
|
-
const record = this.index.listSessions().sessions.find(session => session.sessionId === sessionId);
|
|
1841
|
-
if (!record) return { kind: "unavailable" };
|
|
1842
|
-
if (record.ambiguous) return { kind: "ambiguous" };
|
|
1843
|
-
if (!record.live && options?.allowStopped !== true) return { kind: "unavailable" };
|
|
1844
|
-
const incarnation = endpointIncarnation(record, sessionId);
|
|
1845
|
-
if (incarnation === undefined) return { kind: "unavailable" };
|
|
1846
|
-
return {
|
|
1847
|
-
kind: "identity",
|
|
1848
|
-
identity: {
|
|
1849
|
-
sessionId: record.sessionId,
|
|
1850
|
-
endpointStateRoot: record.locator.stateRoot,
|
|
1851
|
-
endpointGeneration: record.endpointGeneration,
|
|
1852
|
-
endpointIncarnation: incarnation,
|
|
1853
|
-
},
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
async #pollOperatorDirectives(): Promise<void> {
|
|
1858
|
-
const directory = path.join(this.settings.agentDir, "sdk", "elevation", "operator");
|
|
1859
|
-
let names: string[];
|
|
1860
|
-
try {
|
|
1861
|
-
names = await fs.readdir(directory);
|
|
1862
|
-
} catch {
|
|
1863
|
-
return;
|
|
1864
|
-
}
|
|
1865
|
-
for (const name of names) {
|
|
1866
|
-
if (!name.endsWith(".json")) continue;
|
|
1867
|
-
const file = path.join(directory, name);
|
|
1868
|
-
try {
|
|
1869
|
-
const stat = await fs.stat(file);
|
|
1870
|
-
if ((stat.mode & 0o077) !== 0) throw new Error("operator directive is not private");
|
|
1871
|
-
const raw: unknown = JSON.parse(await fs.readFile(file, "utf8"));
|
|
1872
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("malformed operator directive");
|
|
1873
|
-
const directive = raw as Record<string, unknown>;
|
|
1874
|
-
if (
|
|
1875
|
-
typeof directive.elevationRequestId !== "string" ||
|
|
1876
|
-
(directive.answer !== "approve" && directive.answer !== "deny") ||
|
|
1877
|
-
typeof directive.presentedDigest !== "string"
|
|
1878
|
-
)
|
|
1879
|
-
throw new Error("malformed operator directive");
|
|
1880
|
-
const response = await this.elevationAnswer({
|
|
1881
|
-
elevationRequestId: directive.elevationRequestId,
|
|
1882
|
-
answer: directive.answer,
|
|
1883
|
-
presentedDigest: directive.presentedDigest,
|
|
1884
|
-
answerer: { source: "local_operator", attestedBy: "gjc-sdk-session-cli" },
|
|
1885
|
-
});
|
|
1886
|
-
if (!response.ok) throw new Error(response.error.message);
|
|
1887
|
-
await fs.rm(file, { force: true });
|
|
1888
|
-
} catch {
|
|
1889
|
-
await fs.rename(file, `${file}.rejected`).catch(() => undefined);
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
async #sessionControlRequest(input: Record<string, unknown>): Promise<BrokerResponse> {
|
|
1895
|
-
const sessionId = typeof input.sessionId === "string" ? input.sessionId : undefined;
|
|
1896
|
-
const operation = typeof input.operation === "string" ? input.operation : undefined;
|
|
1897
|
-
const rawInput = input.input;
|
|
1898
|
-
const controlInput =
|
|
1899
|
-
rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)
|
|
1900
|
-
? (rawInput as Record<string, unknown>)
|
|
1901
|
-
: undefined;
|
|
1902
|
-
if (!sessionId || !operation || !controlInput || !findOperation("control", operation))
|
|
1903
|
-
return error(
|
|
1904
|
-
"invalid_input",
|
|
1905
|
-
"session.control requires a valid sessionId, control operation, and object input",
|
|
1906
|
-
);
|
|
1907
|
-
const elevationRequestId = typeof input.elevationRequestId === "string" ? input.elevationRequestId : undefined;
|
|
1908
|
-
const elevatable = this.#elevationEnforced && isElevatableOperation("control", operation);
|
|
1909
|
-
// Elevation preflight (F1.3) runs before any clientRef reservation so a
|
|
1910
|
-
// no-grant attempt can never poison a later granted retry with a stale
|
|
1911
|
-
// pending receipt.
|
|
1912
|
-
let grant: ElevationGrantRecord | undefined;
|
|
1913
|
-
if (elevatable) {
|
|
1914
|
-
if (!elevationRequestId) return error("elevation_required", "an elevation grant is required for this control");
|
|
1915
|
-
const resolved = await this.elevation.get(elevationRequestId);
|
|
1916
|
-
if (!resolved.ok) return elevationResultToBroker(resolved);
|
|
1917
|
-
grant = resolved.value.grant;
|
|
1918
|
-
const digest = elevationRequestDigest({ kind: "control", sdkId: operation, input: controlInput });
|
|
1919
|
-
if (
|
|
1920
|
-
grant.operation.kind !== "control" ||
|
|
1921
|
-
grant.operation.sdkId !== operation ||
|
|
1922
|
-
!digest.ok ||
|
|
1923
|
-
digest.digest !== grant.requestDigest
|
|
1924
|
-
)
|
|
1925
|
-
return error("elevation_digest_mismatch", "elevation grant does not match the requested control");
|
|
1926
|
-
}
|
|
1927
|
-
await this.index.refresh();
|
|
1928
|
-
const matches = this.index.listSessions().sessions.filter(session => session.sessionId === sessionId);
|
|
1929
|
-
if (matches.some(session => session.ambiguous) || matches.length > 1)
|
|
1930
|
-
return error("ambiguous_session", "session id maps to more than one state root");
|
|
1931
|
-
const record = matches[0];
|
|
1932
|
-
if (!record?.live) return error("target_unavailable", "session endpoint is unavailable");
|
|
1933
|
-
const incarnation = endpointIncarnation(record, sessionId);
|
|
1934
|
-
if (!incarnation) return error("endpoint_stale", "session endpoint incarnation is unavailable");
|
|
1935
|
-
// Bind the grant to the actual approved target session identity before
|
|
1936
|
-
// claim/capability minting: a grant issued for a different session can
|
|
1937
|
-
// never authorize this control, even with byte-identical input.
|
|
1938
|
-
if (grant !== undefined) {
|
|
1939
|
-
if (grant.sessionIdentity.sessionId !== record.sessionId)
|
|
1940
|
-
return error("elevation_digest_mismatch", "elevation grant was issued for a different session target");
|
|
1941
|
-
const targetIdentity: ElevationSessionIdentity = {
|
|
1942
|
-
sessionId: record.sessionId,
|
|
1943
|
-
endpointStateRoot: record.locator.stateRoot,
|
|
1944
|
-
endpointGeneration: record.endpointGeneration,
|
|
1945
|
-
endpointIncarnation: incarnation,
|
|
1946
|
-
};
|
|
1947
|
-
if (!sameElevationSessionIdentity(grant.sessionIdentity, targetIdentity))
|
|
1948
|
-
return error("endpoint_stale", "session endpoint identity no longer matches the elevation grant");
|
|
1949
|
-
}
|
|
1950
|
-
const clientRef =
|
|
1951
|
-
typeof controlInput.clientRef === "string" && controlInput.clientRef.length > 0
|
|
1952
|
-
? controlInput.clientRef
|
|
1953
|
-
: undefined;
|
|
1954
|
-
// clientRef idempotency is target-session scoped: identical refs and
|
|
1955
|
-
// input on different sessions never collide, conflict, or replay.
|
|
1956
|
-
const receiptKey = clientRef === undefined ? undefined : operationReceiptKey(sessionId, clientRef);
|
|
1957
|
-
const receiptDigest =
|
|
1958
|
-
clientRef === undefined ? undefined : operationReceiptDigest(operation, controlInput, sessionId);
|
|
1959
|
-
if (receiptKey !== undefined && receiptDigest !== undefined) {
|
|
1960
|
-
const reservation = await this.operationReceipts.reserve(receiptKey, receiptDigest);
|
|
1961
|
-
if (reservation.status === "replay") return reservation.response;
|
|
1962
|
-
if (reservation.status === "in_progress")
|
|
1963
|
-
return error("operation_in_progress", "operation receipt is pending");
|
|
1964
|
-
if (reservation.status === "conflict")
|
|
1965
|
-
return error("idempotency_conflict", "clientRef was reused with different operation input");
|
|
1966
|
-
}
|
|
1967
|
-
const settle = async (response: BrokerResponse): Promise<BrokerResponse> => {
|
|
1968
|
-
if (receiptKey !== undefined && receiptDigest !== undefined)
|
|
1969
|
-
await this.operationReceipts.complete(receiptKey, receiptDigest, response);
|
|
1970
|
-
return response;
|
|
1971
|
-
};
|
|
1972
|
-
const endpointResponse = await this.#endpoint({
|
|
1973
|
-
sessionId,
|
|
1974
|
-
endpointGeneration: record.endpointGeneration,
|
|
1975
|
-
endpointIncarnation: incarnation,
|
|
1976
|
-
});
|
|
1977
|
-
if (!endpointResponse.ok) return settle(endpointResponse);
|
|
1978
|
-
const endpoint = endpointResponse.result as { url?: unknown; token?: unknown };
|
|
1979
|
-
if (typeof endpoint.url !== "string" || typeof endpoint.token !== "string")
|
|
1980
|
-
return settle(error("unavailable", "session endpoint is malformed"));
|
|
1981
|
-
let elevationCapability: string | undefined;
|
|
1982
|
-
if (elevatable && elevationRequestId) {
|
|
1983
|
-
let authorityToken: string | undefined;
|
|
1984
|
-
try {
|
|
1985
|
-
const authority = JSON.parse(
|
|
1986
|
-
await fs.readFile(elevationAuthorityPath(record.locator.stateRoot, sessionId), "utf8"),
|
|
1987
|
-
) as { token?: unknown };
|
|
1988
|
-
authorityToken = typeof authority.token === "string" ? authority.token : undefined;
|
|
1989
|
-
} catch {
|
|
1990
|
-
return settle(error("unavailable", "session elevation authority is unavailable"));
|
|
1991
|
-
}
|
|
1992
|
-
if (!authorityToken) return settle(error("unavailable", "session elevation authority is malformed"));
|
|
1993
|
-
const claimed = await this.elevationClaim({ elevationRequestId });
|
|
1994
|
-
if (!claimed.ok) return settle(claimed);
|
|
1995
|
-
elevationCapability = signElevationCapability(authorityToken, elevationRequestId, operation, controlInput);
|
|
1996
|
-
}
|
|
1997
|
-
let client: SdkClient | undefined;
|
|
1998
|
-
try {
|
|
1999
|
-
client = await SdkClient.connect(endpoint.url, endpoint.token);
|
|
2000
|
-
const response = await client.control(operation, controlInput, {
|
|
2001
|
-
confirm: input.confirm === true,
|
|
2002
|
-
...(elevationCapability === undefined ? {} : { elevationRequestId: elevationCapability }),
|
|
2003
|
-
});
|
|
2004
|
-
const ok = (response as { ok?: unknown }).ok === true;
|
|
2005
|
-
if (elevatable && elevationRequestId) {
|
|
2006
|
-
await this.elevationDispatch({
|
|
2007
|
-
elevationRequestId,
|
|
2008
|
-
outcome: ok
|
|
2009
|
-
? { status: "ok", dispatchedAt: Date.now() }
|
|
2010
|
-
: {
|
|
2011
|
-
status: "failed",
|
|
2012
|
-
code: String((response as { error?: { code?: unknown } }).error?.code ?? "control_failed"),
|
|
2013
|
-
message: String(
|
|
2014
|
-
(response as { error?: { message?: unknown } }).error?.message ?? "control failed",
|
|
2015
|
-
),
|
|
2016
|
-
dispatchedAt: Date.now(),
|
|
2017
|
-
},
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
const brokerResponse = ok
|
|
2021
|
-
? { ok: true as const, result: (response as { result?: unknown }).result }
|
|
2022
|
-
: error(
|
|
2023
|
-
String((response as { error?: { code?: unknown } }).error?.code ?? "unavailable"),
|
|
2024
|
-
String((response as { error?: { message?: unknown } }).error?.message ?? "control failed"),
|
|
2025
|
-
);
|
|
2026
|
-
return settle(brokerResponse);
|
|
2027
|
-
} catch (cause) {
|
|
2028
|
-
if (elevatable && elevationRequestId)
|
|
2029
|
-
await this.elevationDispatch({
|
|
2030
|
-
elevationRequestId,
|
|
2031
|
-
outcome: {
|
|
2032
|
-
status: "failed",
|
|
2033
|
-
code: "unavailable",
|
|
2034
|
-
message: cause instanceof Error ? cause.message : String(cause),
|
|
2035
|
-
dispatchedAt: Date.now(),
|
|
2036
|
-
},
|
|
2037
|
-
});
|
|
2038
|
-
return settle(
|
|
2039
|
-
cause instanceof SdkClientError
|
|
2040
|
-
? error(cause.code, cause.message)
|
|
2041
|
-
: error("unavailable", cause instanceof Error ? cause.message : String(cause)),
|
|
2042
|
-
);
|
|
2043
|
-
} finally {
|
|
2044
|
-
await client?.close();
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
/**
|
|
2048
|
-
* Elevation issuance (internal). Binds the exact endpoint identity
|
|
2049
|
-
* (endpointStateRoot/generation/incarnation) from the live index record;
|
|
2050
|
-
* the request digest is broker-computed from the full `{kind,sdkId,input}`
|
|
2051
|
-
* triple with the top-level `elevationRequestId` excluded.
|
|
2052
|
-
*/
|
|
2053
|
-
async elevationIssue(params: {
|
|
2054
|
-
sessionId: string;
|
|
2055
|
-
operation: ElevationOperation;
|
|
2056
|
-
input: Record<string, unknown>;
|
|
2057
|
-
requester: ElevationRequester;
|
|
2058
|
-
elevationRequestId?: string;
|
|
2059
|
-
expiresInMs?: number;
|
|
2060
|
-
}): Promise<BrokerResponse> {
|
|
2061
|
-
if (!isCanonicalSessionId(params.sessionId))
|
|
2062
|
-
return error("invalid_input", "sessionId must be a canonical safe identifier");
|
|
2063
|
-
// Retained stopped sessions may only back an elevation request for the
|
|
2064
|
-
// exact global lifecycle operation that needs them (session.delete); all
|
|
2065
|
-
// other operations still require a live endpoint (fail closed otherwise).
|
|
2066
|
-
const allowStopped = params.operation.kind === "global" && params.operation.sdkId === "session.delete";
|
|
2067
|
-
const resolution = await this.#elevationSessionResolution(params.sessionId, { allowStopped });
|
|
2068
|
-
if (resolution.kind === "ambiguous")
|
|
2069
|
-
return error("ambiguous_session", "session id maps to more than one state root");
|
|
2070
|
-
if (resolution.kind === "unavailable")
|
|
2071
|
-
return error("target_unavailable", "session endpoint is unreachable or its identity is not provable");
|
|
2072
|
-
// Bind the grant to the actual approved target session: the approved
|
|
2073
|
-
// input for a global lifecycle operation must name the elevation session
|
|
2074
|
-
// itself, never a different session (C10).
|
|
2075
|
-
if (params.operation.kind === "global") {
|
|
2076
|
-
const approvedTarget = typeof params.input.sessionId === "string" ? params.input.sessionId : undefined;
|
|
2077
|
-
if (approvedTarget !== undefined && approvedTarget !== params.sessionId)
|
|
2078
|
-
return error("invalid_input", "approved input targets a different session than the elevation session");
|
|
2079
|
-
}
|
|
2080
|
-
const { identity: sessionIdentity } = resolution;
|
|
2081
|
-
let principal: BrokerOwnerPrincipal;
|
|
2082
|
-
try {
|
|
2083
|
-
principal = await this.#elevationPrincipal();
|
|
2084
|
-
} catch {
|
|
2085
|
-
return error("unavailable", "broker owner principal is unavailable");
|
|
2086
|
-
}
|
|
2087
|
-
const result = await this.elevation.issue({ ...params, sessionIdentity, principal });
|
|
2088
|
-
return elevationResultToBroker(result);
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
/**
|
|
2092
|
-
* Elevation answer (internal, operator-only). There is no public
|
|
2093
|
-
* `elevation.answer` operation: this entry point is reserved for the local
|
|
2094
|
-
* operator approval surface and the separately authorized gate-answer
|
|
2095
|
-
* source, and the ledger enforces the distinct-answer-source boundary.
|
|
2096
|
-
*/
|
|
2097
|
-
async elevationAnswer(params: {
|
|
2098
|
-
elevationRequestId: string;
|
|
2099
|
-
answer: "approve" | "deny";
|
|
2100
|
-
presentedDigest: string;
|
|
2101
|
-
answerer: ElevationAnswerAuthority;
|
|
2102
|
-
}): Promise<BrokerResponse> {
|
|
2103
|
-
const resolved = await this.elevation.get(params.elevationRequestId);
|
|
2104
|
-
if (!resolved.ok) return elevationResultToBroker(resolved);
|
|
2105
|
-
const sessionId = resolved.value.grant.sessionIdentity.sessionId;
|
|
2106
|
-
const grantOperation = resolved.value.grant.operation;
|
|
2107
|
-
const resolution = await this.#elevationSessionResolution(sessionId, {
|
|
2108
|
-
allowStopped: grantOperation.kind === "global" && grantOperation.sdkId === "session.delete",
|
|
2109
|
-
});
|
|
2110
|
-
if (resolution.kind === "ambiguous")
|
|
2111
|
-
return error("ambiguous_session", "session id maps to more than one state root");
|
|
2112
|
-
const currentSessionIdentity = resolution.kind === "identity" ? resolution.identity : undefined;
|
|
2113
|
-
let principal: BrokerOwnerPrincipal;
|
|
2114
|
-
try {
|
|
2115
|
-
principal = await this.#elevationPrincipal();
|
|
2116
|
-
} catch {
|
|
2117
|
-
return error("unavailable", "broker owner principal is unavailable");
|
|
2118
|
-
}
|
|
2119
|
-
const result = await this.elevation.answer({ ...params, principal, currentSessionIdentity });
|
|
2120
|
-
return elevationAnswerToBroker(result);
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
/** Single-use grant claim (internal): consumes the grant before dispatch. */
|
|
2124
|
-
async elevationClaim(params: { elevationRequestId: string }): Promise<BrokerResponse> {
|
|
2125
|
-
const resolved = await this.elevation.get(params.elevationRequestId);
|
|
2126
|
-
if (!resolved.ok) return elevationResultToBroker(resolved);
|
|
2127
|
-
const sessionId = resolved.value.grant.sessionIdentity.sessionId;
|
|
2128
|
-
const grantOperation = resolved.value.grant.operation;
|
|
2129
|
-
const resolution = await this.#elevationSessionResolution(sessionId, {
|
|
2130
|
-
allowStopped: grantOperation.kind === "global" && grantOperation.sdkId === "session.delete",
|
|
2131
|
-
});
|
|
2132
|
-
if (resolution.kind === "ambiguous")
|
|
2133
|
-
return error("ambiguous_session", "session id maps to more than one state root");
|
|
2134
|
-
const currentSessionIdentity = resolution.kind === "identity" ? resolution.identity : undefined;
|
|
2135
|
-
let claimIdentity: ElevationClaimIdentity;
|
|
2136
|
-
try {
|
|
2137
|
-
claimIdentity = this.#elevationClaimIdentity();
|
|
2138
|
-
} catch {
|
|
2139
|
-
return error("unavailable", "broker process incarnation is unavailable");
|
|
2140
|
-
}
|
|
2141
|
-
const result = await this.elevation.claim({
|
|
2142
|
-
elevationRequestId: params.elevationRequestId,
|
|
2143
|
-
claimIdentity,
|
|
2144
|
-
currentSessionIdentity,
|
|
2145
|
-
});
|
|
2146
|
-
return elevationResultToBroker(result);
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
/** Records the dispatch outcome for a claimed grant (internal). */
|
|
2150
|
-
async elevationDispatch(params: {
|
|
2151
|
-
elevationRequestId: string;
|
|
2152
|
-
outcome: ElevationDispatchOutcome;
|
|
2153
|
-
}): Promise<BrokerResponse> {
|
|
2154
|
-
let dispatchIdentity: ElevationClaimIdentity;
|
|
2155
|
-
try {
|
|
2156
|
-
dispatchIdentity = this.#elevationClaimIdentity();
|
|
2157
|
-
} catch {
|
|
2158
|
-
return error("unavailable", "broker process incarnation is unavailable");
|
|
2159
|
-
}
|
|
2160
|
-
const result = await this.elevation.dispatch({
|
|
2161
|
-
elevationRequestId: params.elevationRequestId,
|
|
2162
|
-
dispatchIdentity,
|
|
2163
|
-
outcome: params.outcome,
|
|
2164
|
-
});
|
|
2165
|
-
return elevationResultToBroker(result);
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
/** Truthful elevation status read (internal; reconciles expiry and crash state). */
|
|
2169
|
-
async elevationResolve(elevationRequestId: string): Promise<BrokerResponse> {
|
|
2170
|
-
let claimIdentity: ElevationClaimIdentity;
|
|
2171
|
-
try {
|
|
2172
|
-
claimIdentity = this.#elevationClaimIdentity();
|
|
2173
|
-
} catch {
|
|
2174
|
-
return error("unavailable", "broker process incarnation is unavailable");
|
|
2175
|
-
}
|
|
2176
|
-
const result = await this.elevation.resolve(elevationRequestId, claimIdentity);
|
|
2177
|
-
return elevationResultToBroker(result);
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
/** Truthful elevation listing (internal). */
|
|
2181
|
-
async elevationList(): Promise<BrokerResponse> {
|
|
2182
|
-
let claimIdentity: ElevationClaimIdentity;
|
|
2183
|
-
try {
|
|
2184
|
-
claimIdentity = this.#elevationClaimIdentity();
|
|
2185
|
-
} catch {
|
|
2186
|
-
return error("unavailable", "broker process incarnation is unavailable");
|
|
2187
|
-
}
|
|
2188
|
-
const result = await this.elevation.list(claimIdentity);
|
|
2189
|
-
return elevationResultToBroker(result);
|
|
2190
|
-
}
|
|
2191
1610
|
}
|
|
2192
1611
|
|
|
2193
1612
|
/** Test-only hook for simulating a process crash after terminal persistence verification. */
|