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,82 +1,119 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
5
|
-
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { inspectPathIfPresentSync, readBoundedRegularFileSync, retryTransientMultipleLinksSync } from "./secure-file.mjs";
|
|
5
|
+
import { acquireMaintenanceLock, assertStateMaintenanceAvailable } from "./state.mjs";
|
|
6
|
+
import { ensureOwnerOnlyDir, ownerOnlyFile } from "./secure-file.mjs";
|
|
7
|
+
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_BROWSER_PORT = 39393;
|
|
10
10
|
const PAIRING_FILE = "browser-bridge.json";
|
|
11
|
+
const PAIRING_SCHEMA_VERSION = 2;
|
|
12
|
+
const PAIRING_AUTH_VERSION = 2;
|
|
13
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,100}$/;
|
|
11
14
|
|
|
12
|
-
export async function loadOrCreatePairing(stateRoot) {
|
|
13
|
-
if (!stateRoot) return
|
|
14
|
-
assertStateMaintenanceAvailable(stateRoot);
|
|
15
|
+
export async function loadOrCreatePairing(stateRoot, options = {}) {
|
|
16
|
+
if (!stateRoot) return newPairing(DEFAULT_BROWSER_PORT);
|
|
15
17
|
ensureOwnerOnlyDir(stateRoot);
|
|
16
18
|
const file = join(stateRoot, PAIRING_FILE);
|
|
19
|
+
const inspect = options.inspectPathIfPresentSync || inspectPathIfPresentSync;
|
|
20
|
+
const existing = inspect(file, "browser pairing state");
|
|
21
|
+
if (existing) {
|
|
22
|
+
if (existing.isSymbolicLink() || !existing.isFile()) throw new Error("browser pairing state must be a regular file and not a symbolic link");
|
|
23
|
+
const current = readPublishedPairing(file);
|
|
24
|
+
if (!current.requiresMigration) { assertStateMaintenanceAvailable(stateRoot); return current.value; }
|
|
25
|
+
return migratePreviousPairing(stateRoot, file);
|
|
26
|
+
}
|
|
27
|
+
assertStateMaintenanceAvailable(stateRoot);
|
|
17
28
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
18
|
-
|
|
19
|
-
ownerOnlyFile(file);
|
|
20
|
-
let parsed;
|
|
21
|
-
try { parsed = JSON.parse(readBoundedRegularFileSync(file, 64 * 1024).toString("utf8")); }
|
|
22
|
-
catch { throw new Error("browser pairing state is not valid bounded JSON"); }
|
|
23
|
-
if (!/^[A-Za-z0-9_-]{32,100}$/.test(parsed.token) || !Number.isInteger(parsed.port) || parsed.port < 1024 || parsed.port > 65535) {
|
|
24
|
-
throw new Error("browser pairing state is invalid");
|
|
25
|
-
}
|
|
26
|
-
return parsed;
|
|
27
|
-
}
|
|
28
|
-
const value = { token: randomBytes(32).toString("base64url"), port: DEFAULT_BROWSER_PORT };
|
|
29
|
+
const value = newPairing(DEFAULT_BROWSER_PORT);
|
|
29
30
|
try {
|
|
30
|
-
createExclusiveFileSync(file,
|
|
31
|
+
createExclusiveFileSync(file, pairingJson(value), { mode: 0o600 });
|
|
31
32
|
ownerOnlyFile(file);
|
|
32
33
|
return value;
|
|
33
34
|
} catch (error) {
|
|
34
35
|
if (error?.code !== "EEXIST") throw error;
|
|
36
|
+
const created = readPublishedPairing(file);
|
|
37
|
+
if (!created.requiresMigration) return created.value;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
|
-
|
|
40
|
+
return migratePreviousPairing(stateRoot, file);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export async function savePairing(stateRoot, value) {
|
|
41
44
|
assertStateMaintenanceAvailable(stateRoot);
|
|
45
|
+
const normalized = normalizePairing(value);
|
|
42
46
|
const file = join(stateRoot, PAIRING_FILE);
|
|
43
|
-
replaceFileAtomicallySync(file,
|
|
47
|
+
replaceFileAtomicallySync(file, pairingJson(normalized), { mode: 0o600 });
|
|
44
48
|
ownerOnlyFile(file);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
export function
|
|
48
|
-
|
|
51
|
+
export function readBrowserPairing(stateRoot, options = {}) {
|
|
52
|
+
const current = readBrowserPairingEntry(stateRoot, options); if (!current) return null;
|
|
53
|
+
if (current.requiresMigration) throw new Error("browser pairing state requires migration before pairing"); return current.value;
|
|
54
|
+
}
|
|
55
|
+
export function readBrowserPairingPort(stateRoot, options = {}) { return readBrowserPairingEntry(stateRoot, options)?.value.port ?? null; }
|
|
56
|
+
function readBrowserPairingEntry(stateRoot, options) {
|
|
57
|
+
const file = join(stateRoot, PAIRING_FILE); const inspect = options.inspectPathIfPresentSync || inspectPathIfPresentSync;
|
|
58
|
+
const existing = inspect(file, "browser pairing state");
|
|
59
|
+
if (!existing) return null; if (existing.isSymbolicLink() || !existing.isFile()) throw new Error("browser pairing state must be a regular file and not a symbolic link");
|
|
60
|
+
return readPublishedPairing(file);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
&& !parsed.hash;
|
|
63
|
+
async function migratePreviousPairing(stateRoot, file) {
|
|
64
|
+
const lock = await acquirePairingMigrationLock(stateRoot);
|
|
65
|
+
try {
|
|
66
|
+
const current = readPublishedPairing(file);
|
|
67
|
+
if (!current.requiresMigration) return current.value;
|
|
68
|
+
const value = { schemaVersion: PAIRING_SCHEMA_VERSION, pairingAuthVersion: PAIRING_AUTH_VERSION, extensionToken: token(), runtimeToken: current.value.runtimeToken || token(), port: current.value.port, migrationPending: true };
|
|
69
|
+
replaceFileAtomicallySync(file, pairingJson(value), { mode: 0o600 });
|
|
70
|
+
ownerOnlyFile(file);
|
|
71
|
+
return value;
|
|
72
|
+
} finally { lock.release(); }
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
75
|
+
async function acquirePairingMigrationLock(stateRoot) {
|
|
76
|
+
const deadline = createMonotonicDeadline(5_000);
|
|
77
|
+
do {
|
|
78
|
+
const lock = acquireMaintenanceLock(stateRoot, { operation: "browser-pairing-migration" });
|
|
79
|
+
if (lock.acquired) return lock;
|
|
80
|
+
await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(50, Math.max(1, deadline.remainingMs()))); });
|
|
81
|
+
} while (!deadline.expired());
|
|
82
|
+
throw new Error("browser pairing migration could not acquire the state maintenance lock");
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
"content-type": contentType,
|
|
72
|
-
"cache-control": "no-store",
|
|
73
|
-
"content-security-policy": "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
74
|
-
"x-content-type-options": "nosniff",
|
|
75
|
-
"referrer-policy": "no-referrer",
|
|
76
|
-
};
|
|
85
|
+
function readPublishedPairing(file) {
|
|
86
|
+
return retryTransientMultipleLinksSync(() => readPairing(file));
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
function readPairing(file) {
|
|
90
|
+
ownerOnlyFile(file);
|
|
91
|
+
let parsed;
|
|
92
|
+
try { parsed = JSON.parse(readBoundedRegularFileSync(file, 64 * 1024, "browser pairing state", { verifyPathIdentity: true, rejectMultipleLinks: true }).toString("utf8")); }
|
|
93
|
+
catch { throw new Error("browser pairing state is not valid bounded JSON"); }
|
|
94
|
+
if (parsed?.schemaVersion === PAIRING_SCHEMA_VERSION && parsed?.pairingAuthVersion === PAIRING_AUTH_VERSION) {
|
|
95
|
+
return { requiresMigration: false, value: normalizePairing(parsed) };
|
|
96
|
+
}
|
|
97
|
+
if (parsed?.schemaVersion === PAIRING_SCHEMA_VERSION && parsed?.pairingAuthVersion === undefined
|
|
98
|
+
&& TOKEN_PATTERN.test(String(parsed?.extensionToken || ""))
|
|
99
|
+
&& TOKEN_PATTERN.test(String(parsed?.runtimeToken || "")) && parsed.extensionToken !== parsed.runtimeToken && validPort(parsed?.port)) {
|
|
100
|
+
return { requiresMigration: true, value: { extensionToken: parsed.extensionToken, runtimeToken: parsed.runtimeToken, port: Number(parsed.port) } };
|
|
101
|
+
}
|
|
102
|
+
if (TOKEN_PATTERN.test(String(parsed?.token || "")) && validPort(parsed?.port)) {
|
|
103
|
+
return { requiresMigration: true, value: { extensionToken: parsed.token, runtimeToken: "", port: Number(parsed.port) } };
|
|
104
|
+
}
|
|
105
|
+
throw new Error("browser pairing state is invalid");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizePairing(value) {
|
|
109
|
+
if (value?.schemaVersion !== PAIRING_SCHEMA_VERSION || value?.pairingAuthVersion !== PAIRING_AUTH_VERSION || !TOKEN_PATTERN.test(String(value.extensionToken || ""))
|
|
110
|
+
|| !TOKEN_PATTERN.test(String(value.runtimeToken || "")) || value.extensionToken === value.runtimeToken || !validPort(value.port)
|
|
111
|
+
|| typeof value.migrationPending !== "boolean") {
|
|
112
|
+
throw new Error("browser pairing state is invalid");
|
|
113
|
+
}
|
|
114
|
+
return { schemaVersion: PAIRING_SCHEMA_VERSION, pairingAuthVersion: PAIRING_AUTH_VERSION, extensionToken: String(value.extensionToken), runtimeToken: String(value.runtimeToken), port: Number(value.port), migrationPending: value.migrationPending };
|
|
82
115
|
}
|
|
116
|
+
function newPairing(port) { return { schemaVersion: PAIRING_SCHEMA_VERSION, pairingAuthVersion: PAIRING_AUTH_VERSION, extensionToken: token(), runtimeToken: token(), port, migrationPending: false }; }
|
|
117
|
+
function token() { return randomBytes(32).toString("base64url"); }
|
|
118
|
+
function validPort(value) { return Number.isInteger(Number(value)) && Number(value) >= 1024 && Number(value) <= 65535; }
|
|
119
|
+
function pairingJson(value) { return `${JSON.stringify(value, null, 2)}\n`; }
|
|
@@ -1,67 +1,93 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { MAX_BROWSER_MESSAGE_BYTES } from "./browser-extension-protocol.mjs";
|
|
3
|
-
|
|
3
|
+
import { browserRequestResponseError, browserRequestTransportError, invalidBrowserResponseSettlement } from "./browser-request-settlement.mjs";
|
|
4
4
|
export class BrowserRequestRegistry {
|
|
5
5
|
constructor({ maximum = 32 } = {}) {
|
|
6
6
|
this.maximum = maximum;
|
|
7
7
|
this.pending = new Map();
|
|
8
8
|
}
|
|
9
|
-
|
|
10
9
|
request({ transport, method, params, timeoutSeconds, callId = "" }) {
|
|
11
10
|
if (this.pending.size >= this.maximum) throw new Error("too many concurrent browser requests");
|
|
11
|
+
if (typeof method !== "string" || !/^[a-z][a-z0-9_]{0,63}$/.test(method)) throw new Error("browser request method is invalid");
|
|
12
|
+
if (!Number.isSafeInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 185) throw new Error("browser request timeout is invalid");
|
|
12
13
|
const id = `browser_${randomBytes(18).toString("base64url")}`;
|
|
13
14
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
14
15
|
const timeoutMs = timeoutSeconds * 1000;
|
|
15
16
|
const timeout = setTimeout(() => {
|
|
17
|
+
const pending = this.pending.get(id);
|
|
18
|
+
if (!pending) return;
|
|
16
19
|
this.pending.delete(id);
|
|
17
|
-
sendCancelQuietly(transport, id);
|
|
18
|
-
rejectPromise(
|
|
20
|
+
const terminationRequested = sendCancelQuietly(transport, id);
|
|
21
|
+
rejectPromise(browserRequestTransportError({
|
|
22
|
+
pending,
|
|
23
|
+
code: "timeout",
|
|
24
|
+
fallback: `browser request timed out after ${timeoutSeconds}s`,
|
|
25
|
+
terminationRequested,
|
|
26
|
+
}));
|
|
19
27
|
}, timeoutMs);
|
|
20
28
|
timeout.unref?.();
|
|
21
|
-
|
|
29
|
+
const pending = { resolve: resolvePromise, reject: rejectPromise, timeout, callId, method, dispatchAttempted: false };
|
|
30
|
+
this.pending.set(id, pending);
|
|
22
31
|
try {
|
|
23
32
|
const message = JSON.stringify({ type: "request", id, method, params, timeout_ms: timeoutMs });
|
|
24
33
|
if (Buffer.byteLength(message) > MAX_BROWSER_MESSAGE_BYTES) throw new Error("browser request exceeds maximum message size");
|
|
34
|
+
pending.dispatchAttempted = true;
|
|
25
35
|
transport.send(message);
|
|
26
36
|
} catch (error) {
|
|
27
37
|
clearTimeout(timeout);
|
|
28
38
|
this.pending.delete(id);
|
|
29
|
-
rejectPromise(
|
|
39
|
+
rejectPromise(pending.dispatchAttempted
|
|
40
|
+
? browserRequestTransportError({ pending, code: "unavailable", fallback: error })
|
|
41
|
+
: error);
|
|
30
42
|
}
|
|
31
43
|
});
|
|
32
44
|
}
|
|
33
|
-
|
|
34
45
|
settle(message) {
|
|
35
46
|
if (message?.type !== "response" || typeof message.id !== "string") return false;
|
|
36
47
|
const pending = this.pending.get(message.id);
|
|
37
48
|
if (!pending) return false;
|
|
38
49
|
clearTimeout(pending.timeout);
|
|
39
50
|
this.pending.delete(message.id);
|
|
40
|
-
if (message.ok === false)
|
|
41
|
-
|
|
51
|
+
if (message.ok === false) {
|
|
52
|
+
if (typeof message.error !== "string" || !message.error || message.error.includes("\0") || message.error.length > 2000) {
|
|
53
|
+
pending.reject(invalidBrowserResponseSettlement(pending));
|
|
54
|
+
} else {
|
|
55
|
+
pending.reject(browserRequestResponseError(message.error, pending));
|
|
56
|
+
}
|
|
57
|
+
} else if (message.ok === true && message.result && typeof message.result === "object" && !Array.isArray(message.result)) {
|
|
58
|
+
pending.resolve(message.result);
|
|
59
|
+
} else {
|
|
60
|
+
pending.reject(invalidBrowserResponseSettlement(pending));
|
|
61
|
+
}
|
|
42
62
|
return true;
|
|
43
63
|
}
|
|
44
64
|
|
|
45
|
-
cancelCall(callId, transport) {
|
|
65
|
+
cancelCall(callId, transport, reason = null) {
|
|
46
66
|
if (!callId) return;
|
|
47
67
|
for (const [id, pending] of this.pending) {
|
|
48
68
|
if (pending.callId !== callId) continue;
|
|
49
69
|
clearTimeout(pending.timeout);
|
|
50
70
|
this.pending.delete(id);
|
|
51
|
-
sendCancelQuietly(transport, id);
|
|
52
|
-
|
|
71
|
+
const terminationRequested = sendCancelQuietly(transport, id);
|
|
72
|
+
const code = reason?.code === "timeout" ? "timeout" : "cancelled";
|
|
73
|
+
pending.reject(browserRequestTransportError({
|
|
74
|
+
pending,
|
|
75
|
+
code,
|
|
76
|
+
fallback: code === "timeout" ? "browser request timed out" : "browser request cancelled",
|
|
77
|
+
terminationRequested,
|
|
78
|
+
}));
|
|
53
79
|
}
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
rejectAll(message) {
|
|
57
83
|
for (const pending of this.pending.values()) {
|
|
58
84
|
clearTimeout(pending.timeout);
|
|
59
|
-
pending.reject(
|
|
85
|
+
pending.reject(browserRequestTransportError({ pending, code: "unavailable", fallback: message }));
|
|
60
86
|
}
|
|
61
87
|
this.pending.clear();
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
function sendCancelQuietly(transport, id) {
|
|
66
|
-
try { transport?.send(JSON.stringify({ type: "cancel", id })); } catch {}
|
|
92
|
+
try { transport?.send(JSON.stringify({ type: "cancel", id })); return true; } catch { return false; }
|
|
67
93
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { browserMethodMayMutate, browserPostDispatchTransportFailure } from "./browser-extension-protocol.mjs";
|
|
3
|
+
import { BridgeError } from "./errors.mjs";
|
|
4
|
+
/** @typedef {{method: unknown, dispatchAttempted?: boolean}} BrowserPendingSettlement */
|
|
5
|
+
/** @param {{pending?: BrowserPendingSettlement | null, code: string, fallback: unknown, terminationRequested?: boolean}} options */
|
|
6
|
+
export function browserRequestTransportError({ pending, code, fallback, terminationRequested = false }) {
|
|
7
|
+
const mutation = pending?.dispatchAttempted === true && browserMethodMayMutate(pending.method);
|
|
8
|
+
const message = mutation
|
|
9
|
+
? browserPostDispatchTransportFailure(pending.method, boundedMessage(fallback))
|
|
10
|
+
: boundedMessage(fallback);
|
|
11
|
+
return new BridgeError(code, message, {
|
|
12
|
+
expose: true,
|
|
13
|
+
retryable: mutation ? false : code !== "cancelled",
|
|
14
|
+
...(mutation ? {
|
|
15
|
+
details: {
|
|
16
|
+
request_delivery: "sent",
|
|
17
|
+
side_effects_started: "unknown",
|
|
18
|
+
termination_requested: terminationRequested === true,
|
|
19
|
+
effect_settlement: terminationRequested === true ? "pending" : "unknown",
|
|
20
|
+
},
|
|
21
|
+
} : {}),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** @param {string} message @param {BrowserPendingSettlement | null | undefined} pending */
|
|
25
|
+
export function browserRequestResponseError(message, pending) {
|
|
26
|
+
const mutation = pending?.dispatchAttempted === true && browserMethodMayMutate(pending.method);
|
|
27
|
+
if (message === "browser broker is busy") return new BridgeError("limit_exceeded", message, { retryable: true });
|
|
28
|
+
if (["browser extension is not connected", "browser extension send failed"].includes(message)) return new BridgeError("unavailable", message, { retryable: true });
|
|
29
|
+
if (message.startsWith("browser mutation request may have been dispatched;")) {
|
|
30
|
+
return browserRequestTransportError({ pending, code: "unavailable", fallback: message });
|
|
31
|
+
}
|
|
32
|
+
if (message.startsWith("browser broker request timed out after dispatch")) {
|
|
33
|
+
return browserRequestTransportError({
|
|
34
|
+
pending,
|
|
35
|
+
code: "timeout",
|
|
36
|
+
fallback: message,
|
|
37
|
+
terminationRequested: mutation && message.includes("cancellation requested"),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (message.startsWith("browser connection changed after request dispatch")) {
|
|
41
|
+
return browserRequestTransportError({ pending, code: "unavailable", fallback: message });
|
|
42
|
+
}
|
|
43
|
+
const partial = mutation && (
|
|
44
|
+
message.startsWith("browser mutation may have completed;")
|
|
45
|
+
|| message.startsWith("trusted browser input may have been partially dispatched")
|
|
46
|
+
|| message.startsWith("form submission failed after partial changes")
|
|
47
|
+
|| message.startsWith("browser action may have been dispatched")
|
|
48
|
+
|| message.startsWith("browser tab mutation may have been dispatched")
|
|
49
|
+
|| message.startsWith("browser screenshot temporary tab activation may have been dispatched")
|
|
50
|
+
|| message.startsWith("browser screenshot restoration may have been dispatched")
|
|
51
|
+
);
|
|
52
|
+
return partial
|
|
53
|
+
? new BridgeError("execution_failed", message, {
|
|
54
|
+
expose: true,
|
|
55
|
+
retryable: false,
|
|
56
|
+
details: { side_effects_started: true, termination_requested: false, effect_settlement: "unknown" },
|
|
57
|
+
})
|
|
58
|
+
: new BridgeError("execution_failed", message, { expose: true, retryable: false });
|
|
59
|
+
}
|
|
60
|
+
/** @param {BrowserPendingSettlement | null | undefined} pending */
|
|
61
|
+
export function invalidBrowserResponseSettlement(pending) {
|
|
62
|
+
if (pending?.dispatchAttempted === true && browserMethodMayMutate(pending.method)) {
|
|
63
|
+
return browserRequestTransportError({
|
|
64
|
+
pending,
|
|
65
|
+
code: "execution_failed",
|
|
66
|
+
fallback: "browser extension response was malformed",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return new BridgeError("execution_failed", "browser extension response was malformed", {
|
|
70
|
+
expose: true,
|
|
71
|
+
retryable: false,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** @param {unknown} value */
|
|
75
|
+
function boundedMessage(value) {
|
|
76
|
+
const message = value instanceof Error ? value.message : typeof value === "string" ? value : "browser transport failed";
|
|
77
|
+
return message ? message.slice(0, 2000) : "browser transport failed";
|
|
78
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { normalizeBrowserSelector, normalizeFormAction } from "./browser-command.mjs";
|
|
2
|
+
|
|
3
|
+
const MAX_FIELD_VALUE_CHARS = 128 * 1024;
|
|
4
|
+
const RESOURCE_NAME = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
5
|
+
|
|
6
|
+
export function prepareBrowserFormField(input, index) {
|
|
7
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error(`fields[${index}] must be an object`);
|
|
8
|
+
const allowed = new Set(["selector", "value", "value_resource", "action", "sensitive"]);
|
|
9
|
+
for (const key of Object.keys(input)) if (!allowed.has(key)) throw new Error(`unknown fields[${index}] property: ${key}`);
|
|
10
|
+
if (input.sensitive !== undefined && typeof input.sensitive !== "boolean") throw new Error(`fields[${index}].sensitive must be boolean`);
|
|
11
|
+
const action = input.action === undefined ? "fill" : normalizeFormAction(input.action);
|
|
12
|
+
const selector = normalizeBrowserSelector(input.selector, action);
|
|
13
|
+
const directValue = input.value === undefined ? null : boundedBrowserValue(input.value, `fields[${index}].value`);
|
|
14
|
+
const resourceName = input.value_resource === undefined ? "" : validateBrowserResource(input.value_resource);
|
|
15
|
+
if (directValue !== null && resourceName) throw new Error(`fields[${index}] value and value_resource are mutually exclusive`);
|
|
16
|
+
const hasValue = directValue !== null || Boolean(resourceName);
|
|
17
|
+
if (!hasValue && !["check", "uncheck", "click"].includes(action)) throw new Error(`fields[${index}] requires value or value_resource`);
|
|
18
|
+
if (hasValue && !["fill", "select"].includes(action)) throw new Error(`fields[${index}] value is not valid for action '${action}'`);
|
|
19
|
+
return { index, selector, action, sensitive: input.sensitive === true, value: directValue, resourceName };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeUploadFilename(value, { derived = false } = {}) {
|
|
23
|
+
if (typeof value !== "string") {
|
|
24
|
+
if (derived) return "upload.bin";
|
|
25
|
+
throw new Error("filenames entries must be safe single-component filenames of at most 255 characters");
|
|
26
|
+
}
|
|
27
|
+
let name = value;
|
|
28
|
+
if (derived) name = name.replace(/[\u0000-\u001f\u007f/\\]+/g, "_").trim();
|
|
29
|
+
if (!name || name === "." || name === ".." || name.length > 255 || /[\u0000-\u001f\u007f/\\]/.test(name)) {
|
|
30
|
+
if (derived) return "upload.bin";
|
|
31
|
+
throw new Error("filenames entries must be safe single-component filenames of at most 255 characters");
|
|
32
|
+
}
|
|
33
|
+
return name;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeMimeType(value) {
|
|
37
|
+
if (typeof value !== "string") throw new Error("mime_types entries must be valid media types");
|
|
38
|
+
const mime = value.trim().toLowerCase();
|
|
39
|
+
if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mime) || mime.length > 200) {
|
|
40
|
+
throw new Error("mime_types entries must be valid media types");
|
|
41
|
+
}
|
|
42
|
+
return mime;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function optionalStringArray(value, label, maxItems, maxLength) {
|
|
46
|
+
if (value === undefined) return [];
|
|
47
|
+
if (!Array.isArray(value) || value.length > maxItems) throw new Error(`${label} must be an array with at most ${maxItems} entries`);
|
|
48
|
+
return value.map((item, index) => {
|
|
49
|
+
if (typeof item !== "string" || item.includes("\0") || !item.length || item.length > maxLength) {
|
|
50
|
+
throw new Error(`${label}[${index}] must be a non-empty string of at most ${maxLength} characters`);
|
|
51
|
+
}
|
|
52
|
+
return item;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function resolveBrowserActionValue(args, readResourceText) {
|
|
57
|
+
let value = args.value === undefined ? null : boundedBrowserValue(args.value, "value");
|
|
58
|
+
if (args.value_resource !== undefined) {
|
|
59
|
+
if (value !== null) throw new Error("value and value_resource are mutually exclusive");
|
|
60
|
+
value = boundedBrowserValue(await readResourceText(validateBrowserResource(args.value_resource)), "value_resource");
|
|
61
|
+
}
|
|
62
|
+
return { value, source: args.value_resource !== undefined ? "local-resource" : value === null ? "none" : "mcp-argument" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function boundedBrowserValue(value, label) {
|
|
66
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
67
|
+
if (value.includes("\0") || value.length > MAX_FIELD_VALUE_CHARS) throw new Error(`${label} exceeds the maximum length or contains a NUL byte`);
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function validateBrowserResource(value) {
|
|
72
|
+
if (typeof value !== "string" || !RESOURCE_NAME.test(value)) throw new Error("value_resource is invalid");
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { BridgeError } from "./errors.mjs";
|
|
2
|
+
|
|
3
|
+
const TRUSTED_PAGE_ACTIONS = new Set(["click", "double_click", "hover", "press", "type_text"]);
|
|
4
|
+
const AMBIGUOUS_TRUSTED_INPUT = "trusted browser input may have been partially dispatched";
|
|
5
|
+
export const TRUSTED_INPUT_QUARANTINE_REASON = "browser_trusted_input_quarantined";
|
|
6
|
+
export const TRUSTED_INPUT_QUARANTINE_FALLBACK = "trusted_input_quarantined_after_ambiguous_failure";
|
|
7
|
+
|
|
8
|
+
export class BrowserTrustedInputHealth {
|
|
9
|
+
constructor({ logger = null } = {}) {
|
|
10
|
+
this.logger = logger || { event() {} };
|
|
11
|
+
this.quarantinedGeneration = 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
status(bridge = {}) {
|
|
15
|
+
const connected = bridge.extensionConnected === true;
|
|
16
|
+
const generation = extensionGeneration(bridge);
|
|
17
|
+
const supported = bridge.extensionInfo?.capabilities?.includes("trusted_input") === true;
|
|
18
|
+
const quarantined = connected && generation > 0 && this.quarantinedGeneration === generation;
|
|
19
|
+
return Object.freeze({ connected, generation, supported, quarantined, available: connected && supported && !quarantined });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pageInputMode({ inputMode, action, bridge }) {
|
|
23
|
+
const health = this.status(bridge);
|
|
24
|
+
if (!health.quarantined || inputMode === "dom" || !TRUSTED_PAGE_ACTIONS.has(action)) {
|
|
25
|
+
return Object.freeze({ inputMode, fallback: false });
|
|
26
|
+
}
|
|
27
|
+
if (inputMode === "trusted") throw quarantinedError();
|
|
28
|
+
return Object.freeze({ inputMode: "dom", fallback: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
assertTrustedAvailable(bridge) {
|
|
32
|
+
if (this.status(bridge).quarantined) throw quarantinedError();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
noteAmbiguousFailure(error, bridge = {}) {
|
|
36
|
+
const message = String(error?.message || error || "");
|
|
37
|
+
if (!message.startsWith(AMBIGUOUS_TRUSTED_INPUT)) return false;
|
|
38
|
+
const health = this.status(bridge);
|
|
39
|
+
if (!health.connected || health.generation < 1) return false;
|
|
40
|
+
if (this.quarantinedGeneration !== health.generation) {
|
|
41
|
+
this.quarantinedGeneration = health.generation;
|
|
42
|
+
this.logger.event?.("warn", "browser.trusted_input.quarantined", {
|
|
43
|
+
reason: "ambiguous_input_settlement",
|
|
44
|
+
}, "trusted browser input quarantined for the current extension connection");
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extensionGeneration(bridge) {
|
|
51
|
+
const value = bridge.extensionGeneration;
|
|
52
|
+
if (Number.isSafeInteger(value) && value > 0) return value;
|
|
53
|
+
return bridge.extensionConnected === true ? 1 : 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function quarantinedError() {
|
|
57
|
+
return new BridgeError("unavailable",
|
|
58
|
+
"trusted browser input is quarantined for the current extension connection after an ambiguous dispatch",
|
|
59
|
+
{
|
|
60
|
+
retryable: false,
|
|
61
|
+
details: { reason: TRUSTED_INPUT_QUARANTINE_REASON, side_effects_started: false },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { recordMatchesAuthorityRevocation } from "../shared/authority-revocation.mjs";
|
|
2
|
+
|
|
3
|
+
export function bindCallPrincipal(record, principal) {
|
|
4
|
+
if (!record || !principal || typeof principal !== "object") return false;
|
|
5
|
+
if (principal.kind !== "account") {
|
|
6
|
+
record.owner_kind = "local";
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
record.owner_kind = "account";
|
|
10
|
+
record.owner_account_id = String(principal.accountId || "");
|
|
11
|
+
record.owner_account_version = Number(principal.accountVersion);
|
|
12
|
+
record.owner_client_id = String(principal.clientId || "");
|
|
13
|
+
record.owner_family_id = String(principal.familyId || "");
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function callIdsForAuthority(records, revocation) {
|
|
18
|
+
return [...records]
|
|
19
|
+
.filter((record) => recordMatchesAuthorityRevocation(record, revocation))
|
|
20
|
+
.map((record) => record.id);
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { toolCallAdmission, toolCallCapacityConfig, toolCallCapacityUsage } from "../shared/tool-call-capacity.mjs";
|
|
2
|
+
import { BridgeError } from "./errors.mjs";
|
|
3
|
+
|
|
4
|
+
/** @param {unknown} maximumValue @param {unknown} reservedValue @param {Iterable<string>} [reservedTools] */
|
|
5
|
+
export function callCapacityConfig(maximumValue, reservedValue, reservedTools = []) {
|
|
6
|
+
const maximum = Number(maximumValue);
|
|
7
|
+
return toolCallCapacityConfig(
|
|
8
|
+
Number.isFinite(maximum) && maximum > 0 ? Math.floor(maximum) : 16,
|
|
9
|
+
reservedValue,
|
|
10
|
+
reservedTools,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {Map<string, any>} calls @param {ReturnType<typeof callCapacityConfig>} config @param {string} toolName */
|
|
15
|
+
export function assertCallCapacity(calls, config, toolName) {
|
|
16
|
+
const decision = toolCallAdmission(callSnapshot(calls), config, toolName);
|
|
17
|
+
if (decision.allowed) return;
|
|
18
|
+
if (decision.reason === "total_capacity") {
|
|
19
|
+
throw new BridgeError("limit_exceeded", `too many concurrent tool calls (${config.maximum})`, { retryable: true });
|
|
20
|
+
}
|
|
21
|
+
throw new BridgeError(
|
|
22
|
+
"limit_exceeded",
|
|
23
|
+
`ordinary tool-call capacity reached (${config.ordinaryMaximum}); control-plane capacity is reserved for diagnosis and recovery`,
|
|
24
|
+
{ retryable: true },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {Map<string, any>} calls @param {ReturnType<typeof callCapacityConfig>} config */
|
|
29
|
+
export function callCapacitySnapshot(calls, config) {
|
|
30
|
+
const usage = toolCallCapacityUsage(callSnapshot(calls), config);
|
|
31
|
+
return {
|
|
32
|
+
maximum: usage.maximum,
|
|
33
|
+
ordinary_capacity: usage.ordinaryMaximum,
|
|
34
|
+
reserved_capacity: usage.reserved,
|
|
35
|
+
active_reserved: usage.activeReserved,
|
|
36
|
+
active_ordinary: usage.activeOrdinary,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {Map<string, any>} calls */
|
|
41
|
+
function callSnapshot(calls) {
|
|
42
|
+
const byTool = Object.create(null);
|
|
43
|
+
for (const call of calls.values()) byTool[call.tool] = (byTool[call.tool] || 0) + 1;
|
|
44
|
+
return { active: calls.size, byTool };
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BridgeError } from "./errors.mjs";
|
|
2
|
+
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
3
|
+
|
|
4
|
+
const states = new WeakMap();
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ calls: ReadonlyMap<string, unknown>, cancel: (callId: unknown, reason?: unknown, code?: string) => boolean }} DrainableCallRegistry */
|
|
7
|
+
|
|
8
|
+
export function assertCallRegistryOpen(registry) {
|
|
9
|
+
if (!stateFor(registry).draining) return;
|
|
10
|
+
throw new BridgeError("unavailable", "runtime is stopping and cannot accept new tool calls", { retryable: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @param {DrainableCallRegistry} registry @param {unknown} [reason] @param {number} [waitMs] */
|
|
14
|
+
export async function cancelAllCallsAndWait(registry, reason = "runtime stopped", waitMs = 5_000) {
|
|
15
|
+
const state = stateFor(registry);
|
|
16
|
+
state.draining = true;
|
|
17
|
+
for (const id of [...registry.calls.keys()]) registry.cancel(id, reason);
|
|
18
|
+
const deadline = createMonotonicDeadline(waitMs);
|
|
19
|
+
while (registry.calls.size && !deadline.expired()) {
|
|
20
|
+
await waitForRegistryChange(state, Math.max(1, deadline.remainingMs()));
|
|
21
|
+
}
|
|
22
|
+
if (registry.calls.size) {
|
|
23
|
+
throw new BridgeError("unavailable", "tool call shutdown did not settle before the runtime teardown deadline", {
|
|
24
|
+
retryable: true,
|
|
25
|
+
details: { active_calls: registry.calls.size },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function notifyCallRegistryChanged(registry) {
|
|
31
|
+
for (const waiter of [...stateFor(registry).waiters]) waiter();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stateFor(registry) {
|
|
35
|
+
let state = states.get(registry);
|
|
36
|
+
if (!state) {
|
|
37
|
+
state = { draining: false, waiters: new Set() };
|
|
38
|
+
states.set(registry, state);
|
|
39
|
+
}
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function waitForRegistryChange(state, waitMs) {
|
|
44
|
+
return new Promise((resolvePromise) => {
|
|
45
|
+
let timer;
|
|
46
|
+
const done = () => {
|
|
47
|
+
if (timer) clearTimeout(timer);
|
|
48
|
+
state.waiters.delete(done);
|
|
49
|
+
resolvePromise();
|
|
50
|
+
};
|
|
51
|
+
state.waiters.add(done);
|
|
52
|
+
timer = setTimeout(done, waitMs);
|
|
53
|
+
});
|
|
54
|
+
}
|