machine-bridge-mcp 2.0.0 → 3.0.0-beta.102
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 +1142 -0
- package/CONTRIBUTING.md +23 -25
- package/GOVERNANCE.md +2 -2
- package/README.md +58 -20
- package/SECURITY.md +161 -120
- package/SUPPORT.md +1 -1
- package/browser-extension/broker-auth.js +83 -0
- package/browser-extension/broker-liveness.js +35 -0
- package/browser-extension/browser-error-boundary.js +96 -0
- package/browser-extension/browser-operations.js +1456 -101
- package/browser-extension/devtools-input.js +195 -34
- package/browser-extension/devtools-observation.js +429 -0
- package/browser-extension/devtools-session.js +48 -0
- package/browser-extension/manifest.json +4 -3
- package/browser-extension/page-automation.js +578 -118
- package/browser-extension/pairing-bootstrap.js +42 -0
- package/browser-extension/pairing.js +28 -12
- package/browser-extension/service-worker.js +126 -106
- package/docs/AGENT_CONTEXT.md +13 -10
- package/docs/ARCHITECTURE.md +159 -67
- package/docs/AUDIT.md +1302 -4
- package/docs/CLIENTS.md +19 -6
- package/docs/COMPUTER_USE.md +297 -0
- package/docs/ENGINEERING.md +72 -13
- package/docs/GETTING_STARTED.md +18 -20
- package/docs/LOCAL_AUTHORIZATION.md +115 -72
- package/docs/LOCAL_AUTOMATION.md +27 -21
- package/docs/LOGGING.md +40 -19
- package/docs/MANAGED_JOBS.md +37 -34
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +145 -51
- package/docs/OVERVIEW.md +14 -11
- package/docs/POLICY_REFERENCE.md +71 -3
- package/docs/PRIVACY.md +41 -2
- package/docs/PROJECT_STANDARDS.md +29 -6
- package/docs/RELEASING.md +238 -64
- package/docs/TESTING.md +150 -39
- package/docs/THREAT_MODEL.md +164 -67
- package/docs/TOOL_REFERENCE.md +653 -60
- package/docs/UPGRADING.md +78 -18
- package/native/macos/MachineBridgeBackgroundInput.swift +1051 -0
- package/native/macos/MachineBridgeBackgroundInputSmokeFixture.swift +295 -0
- package/native/macos/MachineBridgeTrustBroker.swift +233 -0
- package/package.json +94 -16
- package/scripts/accepted-candidate-tarball.mjs +77 -0
- package/scripts/candidate-runtime-store.mjs +93 -0
- package/scripts/check-plan.mjs +73 -3
- package/scripts/check-runner.mjs +129 -17
- package/scripts/commit-message-check.mjs +5 -2
- package/scripts/consumer-package-security.mjs +293 -0
- package/scripts/coverage-check.mjs +251 -15
- package/scripts/coverage-generation.mjs +41 -0
- package/scripts/coverage-range-merge.mjs +41 -0
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/generate-policy-reference.mjs +28 -5
- package/scripts/generate-tool-reference.mjs +1 -1
- package/scripts/generate-worker-types.mjs +21 -11
- package/scripts/github-backlog.mjs +11 -10
- package/scripts/github-push.mjs +62 -16
- package/scripts/github-release-asset.mjs +58 -0
- package/scripts/github-release.mjs +241 -132
- package/scripts/global-package-installation.mjs +68 -0
- package/scripts/hardened-npm-session.mjs +42 -0
- package/scripts/install-published-prerelease.mjs +198 -0
- package/scripts/local-release-acceptance.mjs +103 -45
- package/scripts/macos-background-input-smoke.mjs +515 -0
- package/scripts/network-retry.mjs +8 -1
- package/scripts/npm-global-prefix.mjs +27 -0
- package/scripts/npm-publication-policy.mjs +17 -0
- package/scripts/official-mcp-conformance.mjs +267 -0
- package/scripts/persistent-activation-process.mjs +101 -0
- package/scripts/prepare-pinned-npm.mjs +12 -69
- package/scripts/prerelease-activation.mjs +160 -0
- package/scripts/privacy-check.mjs +14 -6
- package/scripts/promotion-digest.mjs +128 -0
- package/scripts/publish-npm.mjs +260 -0
- package/scripts/published-release.mjs +132 -0
- package/scripts/release-acceptance.mjs +62 -54
- package/scripts/release-browser-extension-store.mjs +172 -0
- package/scripts/release-candidate-manifest.mjs +52 -0
- package/scripts/release-channel.mjs +106 -0
- package/scripts/release-diagnostic.mjs +41 -0
- package/scripts/release-impact-check.mjs +12 -22
- package/scripts/release-oauth-canary-core.mjs +325 -0
- package/scripts/release-oauth-canary-evidence.mjs +100 -0
- package/scripts/release-oauth-canary.mjs +127 -0
- package/scripts/release-publication-guard.mjs +67 -0
- package/scripts/release-soak.mjs +284 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/run-checks.mjs +44 -7
- package/scripts/run-worker-dry-run.mjs +21 -0
- package/scripts/sarif-security-gate.mjs +2 -1
- package/scripts/sbom-check.mjs +106 -0
- package/scripts/start-release-candidate.mjs +267 -44
- package/scripts/syntax-check.mjs +36 -14
- package/scripts/verification-generation-guard.mjs +15 -0
- package/scripts/verification-idle-sleep-guard.mjs +37 -0
- package/scripts/verification-state.mjs +108 -0
- package/scripts/wrangler-command-lifecycle.mjs +115 -0
- package/src/local/account-access.mjs +13 -1
- package/src/local/account-admin-response.mjs +92 -0
- package/src/local/account-admin.mjs +52 -19
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +88 -49
- package/src/local/agent-skill-discovery.mjs +16 -5
- package/src/local/app-automation-macos-jxa.mjs +350 -0
- package/src/local/app-automation.mjs +873 -163
- package/src/local/application-capability-projection.mjs +24 -0
- package/src/local/authority-context.mjs +106 -0
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/browser-bridge-http.mjs +5 -3
- package/src/local/browser-bridge.mjs +109 -31
- package/src/local/browser-broker-auth-http.mjs +67 -0
- package/src/local/browser-broker-auth.mjs +98 -0
- package/src/local/browser-broker-routes.mjs +63 -20
- package/src/local/browser-broker-server.mjs +17 -7
- package/src/local/browser-command.mjs +79 -30
- package/src/local/browser-computer-observation-service.mjs +266 -0
- package/src/local/browser-extension-identity.mjs +50 -0
- package/src/local/browser-extension-path.mjs +22 -0
- package/src/local/browser-extension-protocol.mjs +50 -11
- package/src/local/browser-operation-service.mjs +147 -117
- package/src/local/browser-pairing-grant.mjs +109 -0
- package/src/local/browser-pairing-http.mjs +36 -0
- package/src/local/browser-pairing-launch.mjs +58 -0
- package/src/local/browser-pairing-store.mjs +85 -48
- package/src/local/browser-request-registry.mjs +40 -14
- package/src/local/browser-request-settlement.mjs +78 -0
- package/src/local/browser-resource-input.mjs +74 -0
- package/src/local/browser-trusted-input-health.mjs +63 -0
- package/src/local/call-authority.mjs +21 -0
- package/src/local/call-capacity.mjs +45 -0
- package/src/local/call-registry-drain.mjs +54 -0
- package/src/local/call-registry.mjs +30 -15
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/capability-ranking.mjs +3 -2
- package/src/local/child-process-settlement.mjs +132 -0
- package/src/local/cli-account-admin.mjs +40 -6
- package/src/local/cli-activate.mjs +162 -0
- package/src/local/cli-local-admin.mjs +20 -45
- package/src/local/cli-options.mjs +12 -13
- package/src/local/cli-ready-output.mjs +52 -0
- package/src/local/cli-service.mjs +91 -11
- package/src/local/cli.mjs +232 -150
- package/src/local/computer-use-application-observation.mjs +224 -0
- package/src/local/computer-use-arguments.mjs +270 -0
- package/src/local/computer-use-deadline.mjs +50 -0
- package/src/local/computer-use-dispatch-settlement.mjs +40 -0
- package/src/local/computer-use-expectation.mjs +171 -0
- package/src/local/computer-use-observation.mjs +769 -0
- package/src/local/computer-use-recovery.mjs +82 -0
- package/src/local/computer-use-result-budget.mjs +145 -0
- package/src/local/computer-use-snapshot-store.mjs +67 -0
- package/src/local/computer-use.mjs +2255 -0
- package/src/local/daemon-http-relay-auth.mjs +26 -0
- package/src/local/daemon-http-relay-connection.mjs +298 -0
- package/src/local/daemon-http-relay-request.mjs +64 -0
- package/src/local/daemon-http-relay-sequence.mjs +49 -0
- package/src/local/daemon-process.mjs +40 -7
- package/src/local/delegated-process-sandbox.mjs +148 -0
- package/src/local/device-identity.mjs +156 -49
- package/src/local/device-root-provider.mjs +81 -0
- package/src/local/directory-metadata.mjs +47 -0
- package/src/local/doctor-reporting.mjs +23 -0
- package/src/local/durable-process-spec.mjs +80 -0
- package/src/local/errors.mjs +25 -15
- package/src/local/exclusive-file.mjs +74 -26
- package/src/local/execution-limits.mjs +9 -0
- package/src/local/execution-routing.mjs +276 -0
- package/src/local/execution-surface.mjs +23 -0
- package/src/local/file-mutation-coordinator.mjs +47 -0
- package/src/local/file-snapshot-preservation.mjs +24 -0
- package/src/local/filesystem-identity.mjs +54 -0
- package/src/local/fixed-process-environment.mjs +21 -0
- package/src/local/full-access-test.mjs +31 -13
- package/src/local/git-commit.mjs +69 -0
- package/src/local/git-config-safety.mjs +55 -0
- package/src/local/git-log-parser.mjs +41 -0
- package/src/local/git-metadata-boundary.mjs +77 -0
- package/src/local/git-metadata-tree-safety.mjs +59 -0
- package/src/local/git-operation-state.mjs +19 -0
- package/src/local/git-service.mjs +165 -30
- package/src/local/hardened-npm-download-timeout.mjs +46 -0
- package/src/local/hardened-npm-download.mjs +109 -0
- package/src/local/hardened-npm-extract.mjs +20 -0
- package/src/local/hardened-npm-verification.mjs +104 -0
- package/src/local/hardened-npm.mjs +199 -0
- package/src/local/job-runner.mjs +244 -89
- package/src/local/lifecycle.mjs +6 -0
- package/src/local/log.mjs +19 -45
- package/src/local/macos-background-input.mjs +398 -0
- package/src/local/macos-idle-sleep-assertion.mjs +65 -0
- package/src/local/macos-trust-broker.mjs +404 -0
- package/src/local/managed-job-cancellation.mjs +42 -0
- package/src/local/managed-job-capacity.mjs +16 -0
- package/src/local/managed-job-directory-generation.mjs +60 -0
- package/src/local/managed-job-directory.mjs +75 -0
- package/src/local/managed-job-durable-process.mjs +49 -0
- package/src/local/managed-job-lock.mjs +46 -14
- package/src/local/managed-job-plan.mjs +38 -14
- package/src/local/managed-job-projection.mjs +23 -15
- package/src/local/managed-job-retention.mjs +128 -0
- package/src/local/managed-job-runner-claim.mjs +70 -0
- package/src/local/managed-job-runner.mjs +35 -10
- package/src/local/managed-job-storage.mjs +22 -6
- package/src/local/managed-job-terminal-maintenance.mjs +40 -0
- package/src/local/managed-job-terminal.mjs +150 -0
- package/src/local/managed-jobs.mjs +424 -287
- package/src/local/npm-cli.mjs +42 -0
- package/src/local/npm-environment.mjs +37 -0
- package/src/local/operation-authorization.mjs +68 -324
- package/src/local/operation-risk.mjs +51 -4
- package/src/local/owner-state-lock.mjs +118 -0
- package/src/local/package-identity.mjs +9 -0
- package/src/local/patch.mjs +20 -18
- package/src/local/path-inspection.mjs +23 -0
- package/src/local/policy.mjs +19 -0
- package/src/local/private-toolchain-integrity.mjs +54 -0
- package/src/local/process-execution.mjs +87 -75
- package/src/local/process-foreground-timeout.mjs +50 -0
- package/src/local/process-identity.mjs +41 -7
- package/src/local/process-nonreplayable-settlement.mjs +50 -0
- package/src/local/process-result-projection.mjs +22 -0
- package/src/local/process-session-events.mjs +47 -0
- package/src/local/process-session-remote-activity.mjs +10 -0
- package/src/local/process-session-termination.mjs +73 -0
- package/src/local/process-sessions.mjs +144 -112
- package/src/local/process-tracker.mjs +140 -13
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +57 -0
- package/src/local/process-tree-signal.mjs +34 -0
- package/src/local/process-tree-snapshot.mjs +77 -0
- package/src/local/process-tree-supervisor.mjs +69 -0
- package/src/local/process-tree.mjs +11 -53
- package/src/local/project-metadata.mjs +19 -6
- package/src/local/relay-call-recovery.mjs +58 -38
- package/src/local/relay-connection-classification.mjs +131 -0
- package/src/local/relay-connection-support.mjs +91 -0
- package/src/local/relay-connection.mjs +179 -272
- package/src/local/relay-diagnostics.mjs +91 -0
- package/src/local/relay-heartbeat.mjs +119 -0
- package/src/local/relay-liveness.mjs +109 -0
- package/src/local/relay-peer-diagnostics.mjs +29 -0
- package/src/local/release-runtime-lock.mjs +18 -0
- package/src/local/remote-activity-idle-sleep-guard.mjs +77 -0
- package/src/local/remote-configuration.mjs +48 -0
- package/src/local/resilient-relay-connection.mjs +178 -0
- package/src/local/resource-admission-diagnostics.mjs +52 -0
- package/src/local/resource-admission-policy.mjs +115 -0
- package/src/local/resource-admission.mjs +339 -0
- package/src/local/resource-build-root.mjs +83 -0
- package/src/local/resource-cargo-concurrency.mjs +38 -0
- package/src/local/resource-cmake-concurrency.mjs +42 -0
- package/src/local/resource-command-concurrency.mjs +37 -0
- package/src/local/resource-command-profile.mjs +176 -0
- package/src/local/resource-coordinator-accounting.mjs +21 -0
- package/src/local/resource-cpu-window.mjs +20 -0
- package/src/local/resource-disk-headroom.mjs +13 -0
- package/src/local/resource-elastic-memory.mjs +19 -0
- package/src/local/resource-elastic-request.mjs +36 -0
- package/src/local/resource-foreground-wait.mjs +29 -0
- package/src/local/resource-go-concurrency.mjs +43 -0
- package/src/local/resource-gradle-concurrency.mjs +57 -0
- package/src/local/resource-host-cache.mjs +39 -0
- package/src/local/resource-host-darwin.mjs +54 -0
- package/src/local/resource-host-linux.mjs +35 -0
- package/src/local/resource-host-sample-file.mjs +23 -0
- package/src/local/resource-host-snapshot.mjs +99 -0
- package/src/local/resource-lease-accounting.mjs +119 -0
- package/src/local/resource-light-command.mjs +6 -0
- package/src/local/resource-make-concurrency.mjs +42 -0
- package/src/local/resource-maven-concurrency.mjs +38 -0
- package/src/local/resource-ninja-command-concurrency.mjs +20 -0
- package/src/local/resource-ninja-concurrency.mjs +28 -0
- package/src/local/resource-operations.mjs +26 -10
- package/src/local/resource-pressure.mjs +67 -0
- package/src/local/resource-probe-command.mjs +35 -0
- package/src/local/resource-process-admission.mjs +59 -0
- package/src/local/resource-process-ancestry-cache.mjs +43 -0
- package/src/local/resource-process-ancestry.mjs +32 -0
- package/src/local/resource-process-priority.mjs +15 -0
- package/src/local/resource-project-key.mjs +53 -0
- package/src/local/resource-pytest-concurrency.mjs +33 -0
- package/src/local/resource-release-control-classification.mjs +14 -0
- package/src/local/resource-release-control-executable.mjs +70 -0
- package/src/local/resource-release-control-workspace.mjs +41 -0
- package/src/local/resource-request-contract.mjs +30 -0
- package/src/local/resource-script-classification.mjs +44 -0
- package/src/local/resource-shell-analysis.mjs +58 -0
- package/src/local/resource-staging-recovery.mjs +97 -0
- package/src/local/resource-swift-concurrency.mjs +31 -0
- package/src/local/resource-transaction-lock.mjs +189 -0
- package/src/local/resource-wait.mjs +48 -0
- package/src/local/resource-waiters.mjs +139 -0
- package/src/local/resource-xcode-command.mjs +7 -0
- package/src/local/resource-xcode-concurrency.mjs +34 -0
- package/src/local/resource-xcode-non-build.mjs +19 -0
- package/src/local/runtime-activation.mjs +700 -0
- package/src/local/runtime-activity-projection.mjs +48 -0
- package/src/local/runtime-capabilities.mjs +48 -15
- package/src/local/runtime-diagnostic-state.mjs +14 -0
- package/src/local/runtime-diagnostics.mjs +40 -13
- package/src/local/runtime-info-projection.mjs +60 -0
- package/src/local/runtime-info-relay-projection.mjs +21 -0
- package/src/local/runtime-paths.mjs +2 -1
- package/src/local/runtime-process-routing.mjs +33 -0
- package/src/local/runtime-relay-connection-options.mjs +44 -0
- package/src/local/runtime-relay-control.mjs +98 -0
- package/src/local/runtime-relay.mjs +26 -50
- package/src/local/runtime-reporting.mjs +37 -21
- package/src/local/runtime-resource-service.mjs +77 -0
- package/src/local/runtime-tool-handlers.mjs +13 -10
- package/src/local/runtime.mjs +244 -208
- package/src/local/secure-file.mjs +73 -20
- package/src/local/security-audit-dispatch.mjs +35 -0
- package/src/local/security-audit-log.mjs +215 -0
- package/src/local/security-audit-state.mjs +139 -0
- package/src/local/security-audit-storage.mjs +135 -0
- package/src/local/security-audit-warning.mjs +31 -0
- package/src/local/security-audit-worker.mjs +87 -0
- package/src/local/service-convergence.mjs +42 -1
- package/src/local/service-definition.mjs +24 -0
- package/src/local/service-environment.mjs +18 -10
- package/src/local/service-owner.mjs +146 -0
- package/src/local/service-ownership.mjs +17 -0
- package/src/local/service-restart-handoff.mjs +64 -0
- package/src/local/service-restart-scheduler.mjs +49 -0
- package/src/local/service-runtime-convergence.mjs +52 -0
- package/src/local/service-runtime.mjs +105 -0
- package/src/local/service-status.mjs +60 -0
- package/src/local/service.mjs +385 -92
- package/src/local/shell.mjs +39 -19
- package/src/local/ssh-key.mjs +158 -42
- package/src/local/state-inventory.mjs +50 -24
- package/src/local/state-owner-lock-inventory.mjs +23 -0
- package/src/local/state-root-owned-namespaces.mjs +83 -0
- package/src/local/state-root-retirement.mjs +89 -0
- package/src/local/state.mjs +386 -96
- package/src/local/stdio.mjs +96 -66
- package/src/local/support-state-projection.mjs +30 -0
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/systemd-removal.mjs +20 -0
- package/src/local/tool-executor.mjs +85 -20
- package/src/local/tool-result-boundary.mjs +51 -0
- package/src/local/toolchain-operation-lock.mjs +41 -0
- package/src/local/tools.mjs +7 -5
- package/src/local/trusted-executable.mjs +53 -0
- package/src/local/trusted-git-executable.mjs +34 -0
- package/src/local/trusted-github-cli.mjs +24 -0
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +52 -45
- package/src/local/worker-deployment-fingerprint.mjs +102 -0
- package/src/local/worker-deployment.mjs +48 -60
- package/src/local/worker-health.mjs +12 -4
- package/src/local/worker-secret-file.mjs +37 -17
- package/src/local/workspace-file-service.mjs +156 -239
- package/src/local/workspace-file-transaction.mjs +189 -0
- package/src/local/workspace-search.mjs +55 -0
- package/src/local/wrangler-toolchain/package-lock.json +1505 -0
- package/src/local/wrangler-toolchain/package.json +23 -0
- package/src/local/wrangler-toolchain-verification.mjs +135 -0
- package/src/local/wrangler-toolchain.mjs +188 -0
- package/src/shared/access-contract.json +7 -1
- package/src/shared/activation-recovery.mjs +49 -0
- package/src/shared/admin-auth.d.mts +2 -1
- package/src/shared/admin-auth.mjs +3 -3
- package/src/shared/authority-revocation.d.mts +9 -0
- package/src/shared/authority-revocation.mjs +28 -0
- package/src/shared/daemon-auth.d.mts +11 -0
- package/src/shared/daemon-auth.mjs +17 -0
- package/src/shared/device-session-auth.d.mts +20 -0
- package/src/shared/device-session-auth.mjs +51 -0
- package/src/shared/foreground-timeout.d.mts +15 -0
- package/src/shared/foreground-timeout.mjs +70 -0
- package/src/shared/log-redaction.d.mts +9 -0
- package/src/shared/log-redaction.mjs +51 -0
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +275 -0
- package/src/shared/project-overview-projection.d.mts +4 -0
- package/src/shared/project-overview-projection.mjs +96 -0
- package/src/shared/relay-contract.json +28 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +15 -9
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-call-capacity.d.mts +41 -0
- package/src/shared/tool-call-capacity.mjs +59 -0
- package/src/shared/tool-catalog.json +626 -57
- package/src/worker/access.ts +12 -0
- package/src/worker/account-admin.ts +55 -33
- package/src/worker/authority-revocations.ts +174 -0
- package/src/worker/authority.ts +52 -6
- package/src/worker/daemon-auth.ts +49 -68
- package/src/worker/daemon-channel.ts +36 -0
- package/src/worker/daemon-http-auth.ts +63 -0
- package/src/worker/daemon-http-channel.ts +120 -0
- package/src/worker/daemon-http-controller.ts +163 -0
- package/src/worker/daemon-http-protocol.ts +62 -0
- package/src/worker/daemon-http-registry.ts +82 -0
- package/src/worker/daemon-last-observation.ts +30 -0
- package/src/worker/daemon-liveness.ts +8 -6
- package/src/worker/daemon-ready-dispatch.ts +17 -0
- package/src/worker/daemon-ready-messages.ts +73 -0
- package/src/worker/daemon-ready-waiters.ts +79 -0
- package/src/worker/daemon-recovery-budget.ts +13 -0
- package/src/worker/daemon-registry.ts +58 -0
- package/src/worker/daemon-relay-diagnostics.ts +127 -0
- package/src/worker/daemon-socket-attachment.ts +55 -0
- package/src/worker/daemon-sockets.ts +72 -62
- package/src/worker/daemon-status.ts +38 -0
- package/src/worker/device-session-verifier.ts +129 -0
- package/src/worker/dpop.ts +157 -0
- package/src/worker/durable-process-timeout.ts +29 -0
- package/src/worker/errors.ts +57 -0
- package/src/worker/http.ts +112 -26
- package/src/worker/index.ts +446 -440
- package/src/worker/mcp-access.ts +57 -0
- package/src/worker/mcp-controller.ts +188 -0
- package/src/worker/mcp-http-accept.ts +11 -0
- package/src/worker/mcp-http-contract.ts +322 -0
- package/src/worker/mcp-initialization-compat.ts +141 -0
- package/src/worker/mcp-jsonrpc.ts +11 -29
- package/src/worker/mcp-removed-protocol.ts +17 -0
- package/src/worker/mcp-response-proxy.ts +135 -0
- package/src/worker/mcp-response-stream.ts +65 -0
- package/src/worker/mcp-stale-schema-compat.ts +57 -0
- package/src/worker/mcp-stream-proxy-contract.ts +48 -0
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/nonce-store.ts +33 -35
- package/src/worker/oauth-authorization-page.ts +21 -1
- package/src/worker/oauth-client-admin.ts +52 -0
- package/src/worker/oauth-client-contract.ts +38 -0
- package/src/worker/oauth-controller.ts +107 -47
- package/src/worker/oauth-field-contract.ts +31 -0
- package/src/worker/oauth-record-contract.ts +7 -0
- package/src/worker/oauth-refresh-authority.ts +27 -0
- package/src/worker/oauth-refresh-exchange.ts +162 -0
- package/src/worker/oauth-refresh-families.ts +130 -27
- package/src/worker/oauth-refresh-persistence.ts +116 -0
- package/src/worker/oauth-state.ts +33 -18
- package/src/worker/oauth-store-validation.ts +131 -0
- package/src/worker/oauth-token-derivation.ts +33 -0
- package/src/worker/oauth-token-issuance.ts +119 -0
- package/src/worker/oauth-tokens.ts +56 -190
- package/src/worker/observability.ts +38 -5
- package/src/worker/pending-admission.ts +15 -0
- package/src/worker/pending-call-capacity.ts +108 -0
- package/src/worker/pending-call-contract.ts +42 -5
- package/src/worker/pending-call-deadlines.ts +75 -0
- package/src/worker/pending-calls.ts +131 -125
- package/src/worker/runtime-alarm-storage.ts +37 -0
- package/src/worker/runtime-alarm.ts +136 -0
- package/src/worker/server-info-activity.ts +36 -0
- package/src/worker/server-info-tool-delivery.ts +15 -0
- package/src/worker/server-info.ts +168 -0
- package/src/worker/tool-catalog.ts +47 -1
- package/src/worker/tool-timeout.ts +75 -14
- package/src/worker/websocket-protocol.ts +26 -2
- package/src/worker/worker-edge-guard.ts +90 -0
- package/src/worker/worker-edge-log.ts +65 -0
- package/src/worker/worker-entry.ts +52 -0
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +48 -0
- package/src/worker/worker-rate-limit-key.ts +30 -0
- package/src/worker/worker-runtime-config.ts +20 -0
- package/src/worker/worker-static-routes.ts +54 -0
- package/src/worker/worker-task-lifetime.ts +18 -0
- package/tsconfig.local.json +17 -1
- package/wrangler.jsonc +13 -0
- package/src/local/cli-approval.mjs +0 -117
- package/src/local/operation-state-lock.mjs +0 -92
- package/src/worker/mcp-session.ts +0 -72
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { DAEMON_AUTH_CHALLENGE_TTL_SECONDS, DAEMON_AUTH_SCHEME, DAEMON_PREFLIGHT_SCHEME, DAEMON_PREFLIGHT_TTL_SECONDS, daemonAuthTranscript, daemonPreflightTranscript } from "../shared/daemon-auth.mjs";
|
|
2
2
|
import { safeEqual } from "./oauth-state.ts";
|
|
3
3
|
import { consumeBoundedNonce } from "./nonce-store.ts";
|
|
4
|
+
import {
|
|
5
|
+
decodeBase64Url,
|
|
6
|
+
parsePublicJwk,
|
|
7
|
+
publicKeyId,
|
|
8
|
+
verifyDeviceSessionCertificate,
|
|
9
|
+
verifyP256Signature,
|
|
10
|
+
} from "./device-session-verifier.ts";
|
|
11
|
+
|
|
12
|
+
const SESSION_CERTIFICATE_HEADER = "X-Bridge-Device-Certificate";
|
|
4
13
|
|
|
5
14
|
export interface DaemonChallenge {
|
|
6
15
|
scheme: typeof DAEMON_AUTH_SCHEME;
|
|
@@ -22,26 +31,39 @@ export function createDaemonChallenge(workerOrigin: string, now = Math.floor(Dat
|
|
|
22
31
|
};
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
|
|
26
34
|
export function sanitizeDaemonChallengeAttachment(value: Record<string, unknown>): Partial<{
|
|
27
35
|
authChallenge: string;
|
|
28
36
|
authIssuedAt: number;
|
|
29
37
|
authExpiresAt: number;
|
|
30
38
|
workerOrigin: string;
|
|
39
|
+
authSessionPublicKeyJson: string;
|
|
40
|
+
authSessionKeyId: string;
|
|
41
|
+
authCertificateExpiresAt: number;
|
|
31
42
|
}> {
|
|
32
43
|
const authChallenge = typeof value.authChallenge === "string" && /^daemon_challenge_[A-Za-z0-9_-]{40,96}$/.test(value.authChallenge)
|
|
33
44
|
? value.authChallenge
|
|
34
45
|
: undefined;
|
|
35
46
|
const authIssuedAt = positiveSafeInteger(value.authIssuedAt);
|
|
36
47
|
const authExpiresAt = positiveSafeInteger(value.authExpiresAt);
|
|
48
|
+
const authCertificateExpiresAt = positiveSafeInteger(value.authCertificateExpiresAt);
|
|
49
|
+
const authSessionKeyId = typeof value.authSessionKeyId === "string" && /^device_[A-Za-z0-9_-]{32}$/.test(value.authSessionKeyId)
|
|
50
|
+
? value.authSessionKeyId
|
|
51
|
+
: undefined;
|
|
37
52
|
let workerOrigin: string | undefined;
|
|
38
|
-
try { workerOrigin = normalizeWorkerOrigin(String(value.workerOrigin || "")); }
|
|
39
|
-
|
|
53
|
+
try { workerOrigin = normalizeWorkerOrigin(String(value.workerOrigin || "")); }
|
|
54
|
+
catch { /* Invalid optional persisted auth metadata remains undefined and fails the caller's completeness checks. */ }
|
|
55
|
+
let authSessionPublicKeyJson: string | undefined;
|
|
56
|
+
try { authSessionPublicKeyJson = JSON.stringify(parsePublicJwk(String(value.authSessionPublicKeyJson || ""))); }
|
|
57
|
+
catch { /* Invalid optional persisted auth metadata remains undefined and fails the caller's completeness checks. */ }
|
|
58
|
+
return { authChallenge, authIssuedAt, authExpiresAt, workerOrigin, authSessionPublicKeyJson, authSessionKeyId, authCertificateExpiresAt };
|
|
40
59
|
}
|
|
41
60
|
|
|
42
61
|
export interface DaemonPreflightAuthorization {
|
|
43
62
|
nonce: string;
|
|
44
63
|
expiresAt: number;
|
|
64
|
+
sessionPublicKeyJson: string;
|
|
65
|
+
sessionKeyId: string;
|
|
66
|
+
certificateExpiresAt: number;
|
|
45
67
|
}
|
|
46
68
|
|
|
47
69
|
export async function verifyDaemonPreflight(input: {
|
|
@@ -53,6 +75,15 @@ export async function verifyDaemonPreflight(input: {
|
|
|
53
75
|
now?: number;
|
|
54
76
|
}): Promise<DaemonPreflightAuthorization | null> {
|
|
55
77
|
const now = Number.isSafeInteger(input.now) ? Number(input.now) : Math.floor(Date.now() / 1000);
|
|
78
|
+
const certificate = await verifyDeviceSessionCertificate({
|
|
79
|
+
encodedCertificate: input.headers.get(SESSION_CERTIFICATE_HEADER) || "",
|
|
80
|
+
rootPublicKeyJson: input.publicKeyJson,
|
|
81
|
+
workerOrigin: input.workerOrigin,
|
|
82
|
+
server: input.server,
|
|
83
|
+
version: input.version,
|
|
84
|
+
now,
|
|
85
|
+
});
|
|
86
|
+
if (!certificate) return null;
|
|
56
87
|
const scheme = input.headers.get("X-Bridge-Device-Scheme") || "";
|
|
57
88
|
const keyId = input.headers.get("X-Bridge-Device-Key") || "";
|
|
58
89
|
const nonce = input.headers.get("X-Bridge-Device-Nonce") || "";
|
|
@@ -61,36 +92,23 @@ export async function verifyDaemonPreflight(input: {
|
|
|
61
92
|
if (scheme !== DAEMON_PREFLIGHT_SCHEME || !signature) return null;
|
|
62
93
|
if (!Number.isSafeInteger(issuedAt) || Math.abs(now - issuedAt) > DAEMON_PREFLIGHT_TTL_SECONDS) return null;
|
|
63
94
|
if (!/^[A-Za-z0-9_-]{24,128}$/.test(nonce)) return null;
|
|
64
|
-
|
|
65
|
-
try { publicJwk = parsePublicJwk(input.publicKeyJson); } catch { return null; }
|
|
66
|
-
if (!(await safeEqual(keyId, await publicKeyId(publicJwk)))) return null;
|
|
95
|
+
if (!(await safeEqual(keyId, certificate.sessionKeyId))) return null;
|
|
67
96
|
let transcript: string;
|
|
68
97
|
try {
|
|
69
|
-
transcript = daemonPreflightTranscript({
|
|
70
|
-
workerOrigin: input.workerOrigin,
|
|
71
|
-
server: input.server,
|
|
72
|
-
version: input.version,
|
|
73
|
-
nonce,
|
|
74
|
-
issuedAt,
|
|
75
|
-
});
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
const key = await crypto.subtle.importKey("jwk", publicJwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
81
|
-
const valid = await crypto.subtle.verify(
|
|
82
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
83
|
-
key,
|
|
84
|
-
signature,
|
|
85
|
-
new TextEncoder().encode(transcript),
|
|
86
|
-
);
|
|
87
|
-
return valid ? { nonce, expiresAt: issuedAt + DAEMON_PREFLIGHT_TTL_SECONDS } : null;
|
|
98
|
+
transcript = daemonPreflightTranscript({ workerOrigin: input.workerOrigin, server: input.server, version: input.version, nonce, issuedAt });
|
|
88
99
|
} catch {
|
|
89
100
|
return null;
|
|
90
101
|
}
|
|
102
|
+
if (!(await verifyP256Signature(certificate.sessionPublicJwk, transcript, signature))) return null;
|
|
103
|
+
return {
|
|
104
|
+
nonce,
|
|
105
|
+
expiresAt: issuedAt + DAEMON_PREFLIGHT_TTL_SECONDS,
|
|
106
|
+
sessionPublicKeyJson: certificate.sessionPublicKeyJson,
|
|
107
|
+
sessionKeyId: certificate.sessionKeyId,
|
|
108
|
+
certificateExpiresAt: certificate.expiresAt,
|
|
109
|
+
};
|
|
91
110
|
}
|
|
92
111
|
|
|
93
|
-
|
|
94
112
|
export async function consumeDaemonPreflightNonce(
|
|
95
113
|
storage: DurableObjectStorage,
|
|
96
114
|
authorization: DaemonPreflightAuthorization,
|
|
@@ -103,6 +121,7 @@ export async function consumeDaemonPreflightNonce(
|
|
|
103
121
|
now,
|
|
104
122
|
noncePattern: /^[A-Za-z0-9_-]{24,128}$/,
|
|
105
123
|
maximum: 128,
|
|
124
|
+
maxFutureSeconds: DAEMON_PREFLIGHT_TTL_SECONDS * 2,
|
|
106
125
|
});
|
|
107
126
|
}
|
|
108
127
|
|
|
@@ -113,10 +132,12 @@ export async function verifyDaemonAuthentication(input: {
|
|
|
113
132
|
server: string;
|
|
114
133
|
version: string;
|
|
115
134
|
instanceId: string;
|
|
135
|
+
certificateExpiresAt?: number;
|
|
116
136
|
now?: number;
|
|
117
137
|
}): Promise<boolean> {
|
|
118
138
|
const now = Number.isSafeInteger(input.now) ? Number(input.now) : Math.floor(Date.now() / 1000);
|
|
119
139
|
if (now < input.challenge.issuedAt - 5 || now > input.challenge.expiresAt) return false;
|
|
140
|
+
if (input.certificateExpiresAt !== undefined && (!Number.isSafeInteger(input.certificateExpiresAt) || now > input.certificateExpiresAt)) return false;
|
|
120
141
|
if (!input.authentication || typeof input.authentication !== "object" || Array.isArray(input.authentication)) return false;
|
|
121
142
|
const auth = input.authentication as Record<string, unknown>;
|
|
122
143
|
if (auth.scheme !== DAEMON_AUTH_SCHEME) return false;
|
|
@@ -126,8 +147,7 @@ export async function verifyDaemonAuthentication(input: {
|
|
|
126
147
|
if (!signature) return false;
|
|
127
148
|
let publicJwk: JsonWebKey;
|
|
128
149
|
try { publicJwk = parsePublicJwk(input.publicKeyJson); } catch { return false; }
|
|
129
|
-
|
|
130
|
-
if (!(await safeEqual(String(auth.key_id || ""), expectedKeyId))) return false;
|
|
150
|
+
if (!(await safeEqual(String(auth.key_id || ""), await publicKeyId(publicJwk)))) return false;
|
|
131
151
|
let transcript: string;
|
|
132
152
|
try {
|
|
133
153
|
transcript = daemonAuthTranscript({
|
|
@@ -141,45 +161,7 @@ export async function verifyDaemonAuthentication(input: {
|
|
|
141
161
|
} catch {
|
|
142
162
|
return false;
|
|
143
163
|
}
|
|
144
|
-
|
|
145
|
-
const key = await crypto.subtle.importKey("jwk", publicJwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
146
|
-
return crypto.subtle.verify(
|
|
147
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
148
|
-
key,
|
|
149
|
-
signature,
|
|
150
|
-
new TextEncoder().encode(transcript),
|
|
151
|
-
);
|
|
152
|
-
} catch {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function parsePublicJwk(value: string): JsonWebKey {
|
|
158
|
-
const parsed = JSON.parse(value) as JsonWebKey;
|
|
159
|
-
if (!parsed || parsed.kty !== "EC" || parsed.crv !== "P-256") throw new Error("invalid device public key");
|
|
160
|
-
if (typeof parsed.x !== "string" || typeof parsed.y !== "string" || parsed.d !== undefined) throw new Error("invalid device public key");
|
|
161
|
-
return { kty: "EC", crv: "P-256", x: parsed.x, y: parsed.y, ext: true, key_ops: ["verify"] };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function publicKeyId(jwk: JsonWebKey): Promise<string> {
|
|
165
|
-
const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
|
|
166
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical));
|
|
167
|
-
return `device_${base64Url(new Uint8Array(digest)).slice(0, 32)}`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function decodeBase64Url(value: string, expectedBytes: number): Uint8Array<ArrayBuffer> | null {
|
|
171
|
-
if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
|
|
172
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
173
|
-
const padding = "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
174
|
-
try {
|
|
175
|
-
const binary = atob(normalized + padding);
|
|
176
|
-
if (binary.length !== expectedBytes) return null;
|
|
177
|
-
const bytes = new Uint8Array(expectedBytes);
|
|
178
|
-
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
179
|
-
return bytes;
|
|
180
|
-
} catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
164
|
+
return verifyP256Signature(publicJwk, transcript, signature);
|
|
183
165
|
}
|
|
184
166
|
|
|
185
167
|
function base64Url(bytes: Uint8Array): string {
|
|
@@ -188,7 +170,6 @@ function base64Url(bytes: Uint8Array): string {
|
|
|
188
170
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
189
171
|
}
|
|
190
172
|
|
|
191
|
-
|
|
192
173
|
function positiveSafeInteger(value: unknown): number | undefined {
|
|
193
174
|
const number = Number(value);
|
|
194
175
|
return Number.isSafeInteger(number) && number > 0 ? number : undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const DAEMON_CHANNEL_OPEN = 1;
|
|
2
|
+
|
|
3
|
+
export interface DaemonChannel {
|
|
4
|
+
readonly readyState: number;
|
|
5
|
+
send(data: string): void;
|
|
6
|
+
readonly daemonTransport?: "https";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ReadyDaemonRegistry {
|
|
10
|
+
readyChannels?(): DaemonChannel[];
|
|
11
|
+
readySockets?(): DaemonChannel[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readyDaemonChannels(registry: ReadyDaemonRegistry): DaemonChannel[] {
|
|
15
|
+
if (typeof registry.readyChannels === "function") return registry.readyChannels();
|
|
16
|
+
if (typeof registry.readySockets === "function") return registry.readySockets();
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function daemonChannelOpen(channel: DaemonChannel | undefined): boolean {
|
|
21
|
+
return Boolean(channel && channel.readyState === DAEMON_CHANNEL_OPEN);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function daemonChannelTransport(channel: DaemonChannel | undefined): "https" | "websocket" {
|
|
25
|
+
return channel?.daemonTransport === "https" ? "https" : "websocket";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function trySendDaemonChannel(channel: DaemonChannel | undefined, value: unknown): boolean {
|
|
29
|
+
if (!channel || channel.readyState !== DAEMON_CHANNEL_OPEN) return false;
|
|
30
|
+
try {
|
|
31
|
+
channel.send(JSON.stringify(value));
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DAEMON_HTTP_RELAY_SCHEME, DAEMON_HTTP_RELAY_TTL_SECONDS, daemonHttpRelayTranscript,
|
|
3
|
+
} from "../shared/daemon-auth.mjs";
|
|
4
|
+
import { safeEqual } from "./oauth-state.ts";
|
|
5
|
+
import { consumeBoundedNonce } from "./nonce-store.ts";
|
|
6
|
+
import {
|
|
7
|
+
decodeBase64Url, verifyDeviceSessionCertificate, verifyP256Signature,
|
|
8
|
+
} from "./device-session-verifier.ts";
|
|
9
|
+
|
|
10
|
+
const SESSION_CERTIFICATE_HEADER = "X-Bridge-Device-Certificate";
|
|
11
|
+
|
|
12
|
+
export async function verifyDaemonHttpRelayRequest(input: {
|
|
13
|
+
storage: DurableObjectStorage;
|
|
14
|
+
publicKeyJson: string;
|
|
15
|
+
headers: Headers;
|
|
16
|
+
body: Uint8Array;
|
|
17
|
+
workerOrigin: string;
|
|
18
|
+
server: string;
|
|
19
|
+
version: string;
|
|
20
|
+
now?: number;
|
|
21
|
+
}): Promise<boolean> {
|
|
22
|
+
const now = Number.isSafeInteger(input.now) ? Number(input.now) : Math.floor(Date.now() / 1000);
|
|
23
|
+
const certificate = await verifyDeviceSessionCertificate({
|
|
24
|
+
encodedCertificate: input.headers.get(SESSION_CERTIFICATE_HEADER) || "",
|
|
25
|
+
rootPublicKeyJson: input.publicKeyJson,
|
|
26
|
+
workerOrigin: input.workerOrigin,
|
|
27
|
+
server: input.server,
|
|
28
|
+
version: input.version,
|
|
29
|
+
now,
|
|
30
|
+
});
|
|
31
|
+
if (!certificate) return false;
|
|
32
|
+
const scheme = input.headers.get("X-Bridge-Device-Scheme") || "";
|
|
33
|
+
const keyId = input.headers.get("X-Bridge-Device-Key") || "";
|
|
34
|
+
const nonce = input.headers.get("X-Bridge-Device-Nonce") || "";
|
|
35
|
+
const issuedAt = Number(input.headers.get("X-Bridge-Device-Time"));
|
|
36
|
+
const claimedBodyHash = input.headers.get("X-Bridge-Body-SHA256") || "";
|
|
37
|
+
const signature = decodeBase64Url(input.headers.get("X-Bridge-Device-Signature") || "", 64);
|
|
38
|
+
if (scheme !== DAEMON_HTTP_RELAY_SCHEME || !signature || !/^[A-Za-z0-9_-]{43}$/.test(claimedBodyHash)) return false;
|
|
39
|
+
if (!Number.isSafeInteger(issuedAt) || Math.abs(now - issuedAt) > DAEMON_HTTP_RELAY_TTL_SECONDS) return false;
|
|
40
|
+
if (!/^[A-Za-z0-9_-]{24,128}$/.test(nonce) || !(await safeEqual(keyId, certificate.sessionKeyId))) return false;
|
|
41
|
+
const digestInput = new Uint8Array(input.body.byteLength);
|
|
42
|
+
digestInput.set(input.body);
|
|
43
|
+
const actualBodyHash = base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", digestInput)));
|
|
44
|
+
if (!(await safeEqual(claimedBodyHash, actualBodyHash))) return false;
|
|
45
|
+
let transcript: string;
|
|
46
|
+
try {
|
|
47
|
+
transcript = daemonHttpRelayTranscript({
|
|
48
|
+
workerOrigin: input.workerOrigin, server: input.server, version: input.version,
|
|
49
|
+
nonce, issuedAt, bodySha256: claimedBodyHash,
|
|
50
|
+
});
|
|
51
|
+
} catch { return false; }
|
|
52
|
+
if (!(await verifyP256Signature(certificate.sessionPublicJwk, transcript, signature))) return false;
|
|
53
|
+
return consumeBoundedNonce(input.storage, {
|
|
54
|
+
key: "daemon-http-relay-nonces", nonce, expiresAt: issuedAt + DAEMON_HTTP_RELAY_TTL_SECONDS, now,
|
|
55
|
+
noncePattern: /^[A-Za-z0-9_-]{24,128}$/, maximum: 1024, maxFutureSeconds: DAEMON_HTTP_RELAY_TTL_SECONDS * 2,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function base64Url(bytes: Uint8Array): string {
|
|
60
|
+
let binary = "";
|
|
61
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
62
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
63
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import relayContract from "../shared/relay-contract.json" with { type: "json" };
|
|
2
|
+
import { DAEMON_CHANNEL_OPEN, type DaemonChannel } from "./daemon-channel.ts";
|
|
3
|
+
import type { DaemonAttachment } from "./daemon-socket-attachment.ts";
|
|
4
|
+
import { relayDiagnosticsAfterReady } from "./daemon-relay-diagnostics.ts";
|
|
5
|
+
|
|
6
|
+
type SequencedMessage = Readonly<{ seq: number; payload: unknown; bytes: number }>;
|
|
7
|
+
|
|
8
|
+
export class DaemonHttpChannel implements DaemonChannel {
|
|
9
|
+
readonly daemonTransport = "https" as const;
|
|
10
|
+
readonly sessionId: string;
|
|
11
|
+
readonly activationToken: string;
|
|
12
|
+
readonly attachment: DaemonAttachment;
|
|
13
|
+
private activatedAtMs = 0;
|
|
14
|
+
private verifiedReadyAtMs = 0;
|
|
15
|
+
private lastSeenAtMs: number;
|
|
16
|
+
private nextWorkerSeq = 1;
|
|
17
|
+
private acknowledgedWorkerSeq = 0;
|
|
18
|
+
private acknowledgedDaemonSeq = 0;
|
|
19
|
+
private queuedBytes = 0;
|
|
20
|
+
private readonly outbound: SequencedMessage[] = [];
|
|
21
|
+
private closed = false;
|
|
22
|
+
|
|
23
|
+
constructor(input: {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
activationToken: string;
|
|
26
|
+
attachment: DaemonAttachment;
|
|
27
|
+
now?: number;
|
|
28
|
+
}) {
|
|
29
|
+
this.sessionId = input.sessionId;
|
|
30
|
+
this.activationToken = input.activationToken;
|
|
31
|
+
this.attachment = input.attachment;
|
|
32
|
+
this.lastSeenAtMs = input.now ?? Date.now();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get readyState(): number { return !this.closed && this.verifiedReadyAtMs > 0 ? DAEMON_CHANNEL_OPEN : 0; }
|
|
36
|
+
get lastSeenMs(): number { return this.lastSeenAtMs; }
|
|
37
|
+
get activatedMs(): number { return this.activatedAtMs; }
|
|
38
|
+
get isActivated(): boolean { return !this.closed && this.activatedAtMs > 0; }
|
|
39
|
+
get daemonSequence(): number { return this.acknowledgedDaemonSeq; }
|
|
40
|
+
|
|
41
|
+
activate(now = Date.now()): void {
|
|
42
|
+
if (this.closed) throw new Error("HTTP daemon channel is closed");
|
|
43
|
+
this.activatedAtMs = now;
|
|
44
|
+
this.touch(now);
|
|
45
|
+
this.attachment.role = "probing";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
verifyReady(now = Date.now()): void {
|
|
49
|
+
if (!this.isActivated) throw new Error("HTTP daemon channel was not activated");
|
|
50
|
+
this.verifiedReadyAtMs = now;
|
|
51
|
+
this.touch(now);
|
|
52
|
+
this.attachment.role = "daemon";
|
|
53
|
+
this.attachment.connectedAt = new Date(now).toISOString();
|
|
54
|
+
this.attachment.lastSeenAt = this.attachment.connectedAt;
|
|
55
|
+
this.attachment.relayDiagnostics = relayDiagnosticsAfterReady(
|
|
56
|
+
this.attachment.relayDiagnostics,
|
|
57
|
+
this.attachment.connectedAt,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
touch(now = Date.now()): void {
|
|
62
|
+
this.lastSeenAtMs = now;
|
|
63
|
+
this.attachment.lastSeenAt = new Date(now).toISOString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
send(data: string): void {
|
|
67
|
+
if (!this.isActivated) throw new Error("HTTP daemon channel is not activated");
|
|
68
|
+
const bytes = new TextEncoder().encode(data).byteLength;
|
|
69
|
+
if (bytes > relayContract.httpFallbackMaximumMessageBytes
|
|
70
|
+
|| this.outbound.length >= relayContract.httpFallbackMaximumQueuedMessages
|
|
71
|
+
|| this.queuedBytes + bytes > relayContract.httpFallbackMaximumQueuedBytes) {
|
|
72
|
+
throw new Error("HTTP daemon relay queue capacity exceeded");
|
|
73
|
+
}
|
|
74
|
+
let payload: unknown;
|
|
75
|
+
try { payload = JSON.parse(data); } catch { throw new Error("HTTP daemon relay message is not JSON"); }
|
|
76
|
+
this.outbound.push(Object.freeze({ seq: this.nextWorkerSeq++, payload, bytes }));
|
|
77
|
+
this.queuedBytes += bytes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
acknowledgeWorker(sequence: number): boolean {
|
|
81
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence >= this.nextWorkerSeq) return false;
|
|
82
|
+
if (sequence <= this.acknowledgedWorkerSeq) return true;
|
|
83
|
+
this.acknowledgedWorkerSeq = sequence;
|
|
84
|
+
while (this.outbound[0]?.seq <= sequence) {
|
|
85
|
+
const removed = this.outbound.shift();
|
|
86
|
+
if (removed) this.queuedBytes = Math.max(0, this.queuedBytes - removed.bytes);
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
outboundMessages(): Array<{ seq: number; payload: unknown }> {
|
|
92
|
+
const selected: Array<{ seq: number; payload: unknown }> = [];
|
|
93
|
+
let bytes = 0;
|
|
94
|
+
for (const message of this.outbound) {
|
|
95
|
+
if (selected.length >= relayContract.httpFallbackMaximumQueuedMessages) break;
|
|
96
|
+
if (selected.length > 0 && bytes + message.bytes > relayContract.httpFallbackMaximumEnvelopeBytes) break;
|
|
97
|
+
selected.push({ seq: message.seq, payload: message.payload });
|
|
98
|
+
bytes += message.bytes;
|
|
99
|
+
}
|
|
100
|
+
return selected;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
acceptDaemonSequence(sequence: number): "new" | "duplicate" | "gap" {
|
|
104
|
+
if (!Number.isSafeInteger(sequence) || sequence <= 0) return "gap";
|
|
105
|
+
if (sequence <= this.acknowledgedDaemonSeq) return "duplicate";
|
|
106
|
+
if (sequence !== this.acknowledgedDaemonSeq + 1) return "gap";
|
|
107
|
+
return "new";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
commitDaemonSequence(sequence: number): void {
|
|
111
|
+
if (sequence !== this.acknowledgedDaemonSeq + 1) throw new Error("HTTP daemon sequence commit is not contiguous");
|
|
112
|
+
this.acknowledgedDaemonSeq = sequence;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
close(): void {
|
|
116
|
+
this.closed = true;
|
|
117
|
+
this.outbound.length = 0;
|
|
118
|
+
this.queuedBytes = 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import relayContract from "../shared/relay-contract.json" with { type: "json" };
|
|
2
|
+
import { authorityRevocations, authorityRevocationWireMessage } from "./authority-revocations.ts";
|
|
3
|
+
import type { DaemonChannel } from "./daemon-channel.ts";
|
|
4
|
+
import { verifyDaemonHttpRelayRequest } from "./daemon-http-auth.ts";
|
|
5
|
+
import type { DaemonHttpChannel } from "./daemon-http-channel.ts";
|
|
6
|
+
import { normalizeDaemonHttpExchange } from "./daemon-http-protocol.ts";
|
|
7
|
+
import type { DaemonRegistry } from "./daemon-registry.ts";
|
|
8
|
+
import { handleReadyDaemonMessage } from "./daemon-ready-messages.ts";
|
|
9
|
+
import { notifyReadyDaemon } from "./daemon-ready-waiters.ts";
|
|
10
|
+
import { HttpError, json, readBoundedBytes } from "./http.ts";
|
|
11
|
+
import type { WorkerObservability } from "./observability.ts";
|
|
12
|
+
import type { PendingCallRegistry } from "./pending-calls.ts";
|
|
13
|
+
import { sanitizeDaemonPolicy, sanitizeDaemonTools } from "./policy.ts";
|
|
14
|
+
import { sanitizeDaemonRelayDiagnostics } from "./daemon-relay-diagnostics.ts";
|
|
15
|
+
import { randomToken } from "./oauth-state.ts";
|
|
16
|
+
|
|
17
|
+
export async function handleDaemonHttpRelay(input: {
|
|
18
|
+
request: Request;
|
|
19
|
+
storage: DurableObjectStorage;
|
|
20
|
+
registry: DaemonRegistry;
|
|
21
|
+
pending: PendingCallRegistry;
|
|
22
|
+
observability: WorkerObservability;
|
|
23
|
+
publicKeyJson: string;
|
|
24
|
+
server: string;
|
|
25
|
+
version: string;
|
|
26
|
+
scheduleAlarm: () => Promise<void>;
|
|
27
|
+
detachChannel: (channel: DaemonChannel, message: string) => Promise<void>;
|
|
28
|
+
retireWebSocket: (socket: WebSocket, message: string) => Promise<void>;
|
|
29
|
+
}): Promise<Response> {
|
|
30
|
+
const bodyBytes = await readBoundedBytes(input.request, relayContract.httpFallbackMaximumEnvelopeBytes);
|
|
31
|
+
const workerOrigin = new URL(input.request.url).origin;
|
|
32
|
+
if (!(await verifyDaemonHttpRelayRequest({
|
|
33
|
+
storage: input.storage, publicKeyJson: input.publicKeyJson, headers: input.request.headers,
|
|
34
|
+
body: bodyBytes, workerOrigin, server: input.server, version: input.version,
|
|
35
|
+
}))) return json({ error: "daemon_http_unauthorized" }, 401);
|
|
36
|
+
const exchange = normalizeDaemonHttpExchange(parseJson(bodyBytes));
|
|
37
|
+
if (!exchange) return json({ error: "invalid_daemon_http_exchange" }, 400);
|
|
38
|
+
|
|
39
|
+
let channel = input.registry.http.get(exchange.sessionId);
|
|
40
|
+
if (!channel && (exchange.activationToken || exchange.ackWorkerSeq !== 0 || exchange.messages.length > 0)) {
|
|
41
|
+
return json({ error: "unknown_daemon_http_session" }, 409);
|
|
42
|
+
}
|
|
43
|
+
const readyHttpChannels = input.registry.httpReadyChannels();
|
|
44
|
+
if (!channel && readyHttpChannels.some((candidate) => candidate.attachment.instanceId !== exchange.instanceId)) {
|
|
45
|
+
return relayResponse("standby", null, 0, []);
|
|
46
|
+
}
|
|
47
|
+
const readySockets = input.registry.readySockets();
|
|
48
|
+
if (readySockets.length > 0) {
|
|
49
|
+
if (channel || !exchange.takeoverWebSocket || readySockets.some((socket) =>
|
|
50
|
+
input.registry.readyAttachment(socket)?.instanceId !== exchange.instanceId)) {
|
|
51
|
+
if (channel) input.registry.http.close(channel);
|
|
52
|
+
return relayResponse("standby", null, 0, []);
|
|
53
|
+
}
|
|
54
|
+
for (const socket of readySockets) {
|
|
55
|
+
await input.retireWebSocket(socket, "authenticated HTTPS fallback replacing same-instance WebSocket");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!channel) {
|
|
60
|
+
const sameInstanceChannels = [
|
|
61
|
+
...readyHttpChannels,
|
|
62
|
+
...input.registry.httpCandidates().filter((candidate) => candidate.sessionId !== exchange.sessionId),
|
|
63
|
+
].filter((candidate) => candidate.attachment.instanceId === exchange.instanceId);
|
|
64
|
+
for (const previous of sameInstanceChannels) {
|
|
65
|
+
await input.detachChannel(previous, "new authenticated HTTPS fallback session replaced same-instance channel");
|
|
66
|
+
input.registry.http.close(previous);
|
|
67
|
+
}
|
|
68
|
+
const policy = sanitizeDaemonPolicy(exchange.policy);
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
channel = input.registry.http.beginCandidate(exchange.sessionId, {
|
|
71
|
+
role: "candidate", connectedAt: new Date(now).toISOString(), lastSeenAt: new Date(now).toISOString(),
|
|
72
|
+
instanceId: exchange.instanceId, connectionId: randomToken("connection"), policy,
|
|
73
|
+
tools: sanitizeDaemonTools(exchange.tools, policy),
|
|
74
|
+
relayDiagnostics: sanitizeDaemonRelayDiagnostics(exchange.relayDiagnostics),
|
|
75
|
+
}, now);
|
|
76
|
+
if (!channel) return json({ error: "daemon_http_candidate_capacity" }, 503);
|
|
77
|
+
const queuedRevocations = await authorityRevocations(input.storage);
|
|
78
|
+
input.registry.http.activate(exchange.sessionId, channel.activationToken, now);
|
|
79
|
+
const rebound = input.pending.rebindInstance(exchange.instanceId, channel);
|
|
80
|
+
channel.send(JSON.stringify({ type: "resume_calls", ids: rebound }));
|
|
81
|
+
for (const revocation of queuedRevocations) channel.send(JSON.stringify(authorityRevocationWireMessage(revocation)));
|
|
82
|
+
channel.send(JSON.stringify({ type: "ready_ack", server: input.server, version: input.version }));
|
|
83
|
+
await input.scheduleAlarm();
|
|
84
|
+
return relayResponse("probing", channel.activationToken, channel.daemonSequence, channel.outboundMessages());
|
|
85
|
+
}
|
|
86
|
+
if (channel.attachment.instanceId !== exchange.instanceId) return json({ error: "daemon_http_instance_mismatch" }, 409);
|
|
87
|
+
|
|
88
|
+
if (channel.readyState !== 1) {
|
|
89
|
+
if (!channel.isActivated) {
|
|
90
|
+
input.registry.http.close(channel);
|
|
91
|
+
return json({ error: "invalid_daemon_http_candidate_state" }, 409);
|
|
92
|
+
}
|
|
93
|
+
if (exchange.activationToken !== channel.activationToken || !channel.acknowledgeWorker(exchange.ackWorkerSeq)) {
|
|
94
|
+
return json({ error: "invalid_daemon_http_acknowledgement" }, 409);
|
|
95
|
+
}
|
|
96
|
+
channel.touch();
|
|
97
|
+
for (const message of exchange.messages) {
|
|
98
|
+
const sequence = channel.acceptDaemonSequence(message.seq);
|
|
99
|
+
if (sequence === "duplicate") continue;
|
|
100
|
+
if (sequence === "gap") return invalidate(channel, input, "daemon_http_sequence_gap");
|
|
101
|
+
if (message.payload.type === "https_ready") {
|
|
102
|
+
if (channel.outboundMessages().length > 0) {
|
|
103
|
+
return invalidate(channel, input, "daemon_http_ready_before_control_ack");
|
|
104
|
+
}
|
|
105
|
+
channel.verifyReady();
|
|
106
|
+
input.registry.rememberReady(channel);
|
|
107
|
+
channel.commitDaemonSequence(message.seq);
|
|
108
|
+
const previous = input.registry.httpReadyChannels().filter((candidate) => candidate !== channel);
|
|
109
|
+
for (const old of previous) {
|
|
110
|
+
await input.detachChannel(old, "HTTPS fallback channel replaced after verified handover");
|
|
111
|
+
input.registry.http.close(old);
|
|
112
|
+
}
|
|
113
|
+
input.observability.event("info", "daemon.https_fallback.ready", { daemon_owned_calls: exchange.ownedCallIds.length });
|
|
114
|
+
notifyReadyDaemon(input.registry);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const handled = await handleReadyDaemonMessage({
|
|
118
|
+
channel, body: message.payload, pending: input.pending, storage: input.storage, observability: input.observability,
|
|
119
|
+
});
|
|
120
|
+
if (!handled.ok) return invalidate(channel, input, handled.errorCode ?? "invalid_daemon_http_message");
|
|
121
|
+
channel.commitDaemonSequence(message.seq);
|
|
122
|
+
}
|
|
123
|
+
await input.scheduleAlarm();
|
|
124
|
+
return relayResponse(channel.readyState === 1 ? "ready" : "probing", channel.activationToken, channel.daemonSequence, channel.outboundMessages());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (exchange.activationToken !== channel.activationToken || !channel.acknowledgeWorker(exchange.ackWorkerSeq)) {
|
|
128
|
+
return json({ error: "invalid_daemon_http_acknowledgement" }, 409);
|
|
129
|
+
}
|
|
130
|
+
channel.touch();
|
|
131
|
+
for (const message of exchange.messages) {
|
|
132
|
+
const sequence = channel.acceptDaemonSequence(message.seq);
|
|
133
|
+
if (sequence === "duplicate") continue;
|
|
134
|
+
if (sequence === "gap") return invalidate(channel, input, "daemon_http_sequence_gap");
|
|
135
|
+
const handled = await handleReadyDaemonMessage({
|
|
136
|
+
channel, body: message.payload, pending: input.pending, storage: input.storage, observability: input.observability,
|
|
137
|
+
});
|
|
138
|
+
if (!handled.ok) return invalidate(channel, input, handled.errorCode ?? "invalid_daemon_http_message");
|
|
139
|
+
channel.commitDaemonSequence(message.seq);
|
|
140
|
+
}
|
|
141
|
+
await input.scheduleAlarm();
|
|
142
|
+
return relayResponse("ready", channel.activationToken, channel.daemonSequence, channel.outboundMessages());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function invalidate(channel: DaemonHttpChannel, input: Parameters<typeof handleDaemonHttpRelay>[0], code: string): Promise<Response> {
|
|
146
|
+
await input.detachChannel(channel, `HTTPS fallback protocol failure: ${code}`);
|
|
147
|
+
input.registry.http.close(channel);
|
|
148
|
+
input.observability.event("warn", "daemon.https_fallback.invalidated", { error_class: code });
|
|
149
|
+
await input.scheduleAlarm();
|
|
150
|
+
return json({ error: code }, 409);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function relayResponse(phase: "probing" | "ready" | "standby", activationToken: string | null, ackDaemonSeq: number, messages: unknown[]): Response {
|
|
154
|
+
return json({ protocol: 1, phase, activation_token: activationToken, ack_daemon_seq: ackDaemonSeq, messages });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseJson(bytes: Uint8Array): unknown {
|
|
158
|
+
let text: string;
|
|
159
|
+
try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
|
160
|
+
catch { throw new HttpError(400, "invalid_encoding", "daemon HTTP relay body must be UTF-8"); }
|
|
161
|
+
try { return JSON.parse(text); }
|
|
162
|
+
catch { throw new HttpError(400, "invalid_json", "daemon HTTP relay body is not valid JSON"); }
|
|
163
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { sanitizeDaemonInstanceId } from "./daemon-socket-attachment.ts";
|
|
2
|
+
|
|
3
|
+
const SESSION_ID = /^relay_http_[A-Za-z0-9_-]{43}$/;
|
|
4
|
+
const ACTIVATION_TOKEN = /^activate_[A-Za-z0-9_-]{43}$/;
|
|
5
|
+
const CALL_ID = /^call_[A-Za-z0-9_-]{8,240}$/;
|
|
6
|
+
|
|
7
|
+
export interface DaemonHttpExchange {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
instanceId: string;
|
|
10
|
+
activationToken: string;
|
|
11
|
+
ackWorkerSeq: number;
|
|
12
|
+
takeoverWebSocket: boolean;
|
|
13
|
+
ownedCallIds: string[];
|
|
14
|
+
messages: Array<{ seq: number; payload: Record<string, unknown> }>;
|
|
15
|
+
tools: unknown;
|
|
16
|
+
policy: unknown;
|
|
17
|
+
relayDiagnostics: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeDaemonHttpExchange(value: unknown): DaemonHttpExchange | null {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
22
|
+
const body = value as Record<string, unknown>;
|
|
23
|
+
if (body.protocol !== 1) return null;
|
|
24
|
+
const sessionId = typeof body.session_id === "string" && SESSION_ID.test(body.session_id) ? body.session_id : "";
|
|
25
|
+
const instanceId = sanitizeDaemonInstanceId(body.instance_id) ?? "";
|
|
26
|
+
const activationToken = body.activation_token === undefined ? ""
|
|
27
|
+
: typeof body.activation_token === "string" && ACTIVATION_TOKEN.test(body.activation_token) ? body.activation_token : "invalid";
|
|
28
|
+
const ackWorkerSeq = Number(body.ack_worker_seq);
|
|
29
|
+
const takeoverWebSocket = body.takeover_websocket === undefined ? false : body.takeover_websocket;
|
|
30
|
+
const ownedCallIds = normalizeCallIds(body.owned_call_ids);
|
|
31
|
+
const messages = normalizeMessages(body.messages);
|
|
32
|
+
if (!sessionId || !instanceId || activationToken === "invalid" || !Number.isSafeInteger(ackWorkerSeq) || ackWorkerSeq < 0
|
|
33
|
+
|| typeof takeoverWebSocket !== "boolean" || !ownedCallIds || !messages) return null;
|
|
34
|
+
return {
|
|
35
|
+
sessionId, instanceId, activationToken, ackWorkerSeq, takeoverWebSocket, ownedCallIds, messages,
|
|
36
|
+
tools: body.tools, policy: body.policy, relayDiagnostics: body.relay_diagnostics,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeCallIds(value: unknown): string[] | null {
|
|
41
|
+
if (!Array.isArray(value) || value.length > 32) return null;
|
|
42
|
+
const result: string[] = [];
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
for (const item of value) {
|
|
45
|
+
if (typeof item !== "string" || !CALL_ID.test(item) || seen.has(item)) return null;
|
|
46
|
+
seen.add(item); result.push(item);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeMessages(value: unknown): Array<{ seq: number; payload: Record<string, unknown> }> | null {
|
|
52
|
+
if (!Array.isArray(value) || value.length > 64) return null;
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const item of value) {
|
|
55
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
56
|
+
const record = item as Record<string, unknown>;
|
|
57
|
+
if (!Number.isSafeInteger(record.seq) || Number(record.seq) <= 0
|
|
58
|
+
|| !record.payload || typeof record.payload !== "object" || Array.isArray(record.payload)) return null;
|
|
59
|
+
result.push({ seq: Number(record.seq), payload: record.payload as Record<string, unknown> });
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|