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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function resolveNpmCli(options = {}) {
|
|
5
|
+
const env = options.env && typeof options.env === "object" ? options.env : process.env;
|
|
6
|
+
const nodeExecutable = path.resolve(String(options.nodeExecutable || process.execPath));
|
|
7
|
+
const nodeDirectory = path.dirname(nodeExecutable);
|
|
8
|
+
const packageManagerPrefix = nodePackageManagerPrefix(nodeExecutable);
|
|
9
|
+
const candidates = [
|
|
10
|
+
options.npmCli,
|
|
11
|
+
...(options.allowLifecycleNpmCli === false ? [] : [env.npm_execpath]),
|
|
12
|
+
path.resolve(nodeDirectory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
13
|
+
path.join(nodeDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
|
|
14
|
+
packageManagerPrefix && path.join(packageManagerPrefix, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
15
|
+
...(options.allowFallbackLocations === false ? [] : [
|
|
16
|
+
env.ProgramFiles && path.join(env.ProgramFiles, "nodejs", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
17
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, "Programs", "nodejs", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
18
|
+
"/opt/homebrew/lib/node_modules/npm/bin/npm-cli.js",
|
|
19
|
+
"/usr/local/lib/node_modules/npm/bin/npm-cli.js",
|
|
20
|
+
"/usr/lib/node_modules/npm/bin/npm-cli.js",
|
|
21
|
+
]),
|
|
22
|
+
].filter(Boolean);
|
|
23
|
+
for (const candidate of candidates) {
|
|
24
|
+
if (!path.isAbsolute(String(candidate))) continue;
|
|
25
|
+
try {
|
|
26
|
+
const canonical = realpathSync(String(candidate));
|
|
27
|
+
const info = statSync(canonical);
|
|
28
|
+
if (!info.isFile()) continue;
|
|
29
|
+
if (process.platform !== "win32" && (Number(info.mode) & 0o022) !== 0) continue;
|
|
30
|
+
return canonical;
|
|
31
|
+
} catch {
|
|
32
|
+
// Candidate discovery is bounded and failure of one conventional location is irrelevant.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new Error("npm 12 CLI could not be located; install npm 12 alongside Node.js 26 and retry");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nodePackageManagerPrefix(nodeExecutable) {
|
|
39
|
+
const marker = `${path.sep}Cellar${path.sep}`;
|
|
40
|
+
const index = nodeExecutable.indexOf(marker);
|
|
41
|
+
return index > 0 ? nodeExecutable.slice(0, index) : "";
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const NESTED_NPM_MODE_KEYS = new Set([
|
|
2
|
+
"npm_config_global_style",
|
|
3
|
+
"npm_config_install_strategy",
|
|
4
|
+
"npm_config_strict_peer_deps",
|
|
5
|
+
"npm_config_legacy_peer_deps",
|
|
6
|
+
"npm_config_force",
|
|
7
|
+
"npm_config_shrinkwrap",
|
|
8
|
+
"npm_config_package_lock",
|
|
9
|
+
"npm_config_parseable",
|
|
10
|
+
"npm_config_json",
|
|
11
|
+
"npm_config_if_present",
|
|
12
|
+
"npm_config_dry_run",
|
|
13
|
+
"npm_config_global",
|
|
14
|
+
"npm_config_prefix",
|
|
15
|
+
"npm_config_workspace",
|
|
16
|
+
"npm_config_workspaces",
|
|
17
|
+
"npm_config_ignore_scripts",
|
|
18
|
+
"npm_config_package_lock_only",
|
|
19
|
+
"npm_config_omit",
|
|
20
|
+
"npm_config_include",
|
|
21
|
+
"npm_config_production",
|
|
22
|
+
"npm_config_save",
|
|
23
|
+
"npm_config_save_dev",
|
|
24
|
+
"npm_config_save_optional",
|
|
25
|
+
"npm_config_save_peer",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function nestedNpmEnvironment(environment = process.env) {
|
|
29
|
+
if (!environment || typeof environment !== "object" || Array.isArray(environment)) {
|
|
30
|
+
throw new TypeError("nested npm environment must be an environment record");
|
|
31
|
+
}
|
|
32
|
+
const result = { ...environment };
|
|
33
|
+
for (const key of Object.keys(result)) {
|
|
34
|
+
if (NESTED_NPM_MODE_KEYS.has(key.toLowerCase())) delete result[key];
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
@@ -1,24 +1,12 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import accessContract from "../shared/access-contract.json" with { type: "json" };
|
|
4
4
|
import { BridgeError } from "./errors.mjs";
|
|
5
5
|
import { OPERATION_APPROVAL_SCOPES, classifyOperation } from "./operation-risk.mjs";
|
|
6
|
-
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
7
|
-
import { withOperationStateLock } from "./operation-state-lock.mjs";
|
|
8
|
-
import { ensureOwnerOnlyDirectorySync, readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
9
|
-
|
|
10
|
-
const SCHEMA_VERSION = 1;
|
|
11
|
-
const MAX_STATE_BYTES = 256 * 1024;
|
|
12
|
-
const MAX_PENDING = 100;
|
|
13
|
-
const MAX_LEASES = 256;
|
|
14
|
-
const PENDING_TTL_SECONDS = 10 * 60;
|
|
15
|
-
const DEFAULT_LEASE_SECONDS = 60 * 60;
|
|
16
|
-
const MAX_LEASE_SECONDS = 12 * 60 * 60;
|
|
17
|
-
const FULL_MAX_LEASE_SECONDS = 8 * 60 * 60;
|
|
18
6
|
|
|
19
7
|
export { OPERATION_APPROVAL_SCOPES, classifyOperation };
|
|
20
8
|
|
|
21
|
-
const
|
|
9
|
+
const OWNER_ONLY_TOOLS = new Set((accessContract.ownerOnlyTools || []).map(String));
|
|
22
10
|
|
|
23
11
|
export class OperationAuthorizer {
|
|
24
12
|
constructor(options = {}) {
|
|
@@ -26,341 +14,97 @@ export class OperationAuthorizer {
|
|
|
26
14
|
this.root = options.root ? path.resolve(String(options.root)) : "";
|
|
27
15
|
this.resolveExistingPath = options.resolveExistingPath;
|
|
28
16
|
this.resolveWritePath = options.resolveWritePath;
|
|
17
|
+
this.protectedRoots = Array.isArray(options.protectedRoots) ? options.protectedRoots.filter(Boolean).map((value) => path.resolve(String(value))) : [];
|
|
18
|
+
this.auditTargetKey = Buffer.from(options.auditTargetKey || randomBytes(32));
|
|
29
19
|
this.now = typeof options.now === "function" ? options.now : Date.now;
|
|
30
|
-
this.queue = Promise.resolve();
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
async authorize(operation) {
|
|
34
|
-
if (operation?.context?.origin !== "relay")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
throw new BridgeError("authorization_denied", "relay operation is missing
|
|
23
|
+
if (operation?.context?.origin !== "relay") {
|
|
24
|
+
return { allowed: true, source: "local", category: "local operation", scopes: [], targetHash: "" };
|
|
25
|
+
}
|
|
26
|
+
const authority = operation.context?.authority;
|
|
27
|
+
const principal = authority?.principal;
|
|
28
|
+
if (!authority || principal?.kind !== "account") {
|
|
29
|
+
throw new BridgeError("authorization_denied", "relay operation is missing effective authority context");
|
|
41
30
|
}
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
allowed:
|
|
45
|
-
source: "
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
if (!authority.owner && OWNER_ONLY_TOOLS.has(operation.tool)) {
|
|
32
|
+
operation.context.operationAuthorization = {
|
|
33
|
+
allowed: false,
|
|
34
|
+
source: "role-ceiling",
|
|
35
|
+
category: "owner-only tool",
|
|
36
|
+
scopes: [],
|
|
37
|
+
targetHash: "",
|
|
48
38
|
};
|
|
39
|
+
throw new BridgeError("authorization_denied", `account role ${principal.role} cannot use owner-only tool ${operation.tool}`, {
|
|
40
|
+
details: { reason: "account_role_owner_only_tool_ceiling", tool: operation.tool, role: principal.role },
|
|
41
|
+
});
|
|
49
42
|
}
|
|
50
43
|
const requirement = await classifyOperation(operation.tool, operation.args, {
|
|
51
44
|
workspace: this.workspace,
|
|
52
|
-
resolveExistingPath: this.resolveExistingPath,
|
|
53
|
-
resolveWritePath: this.resolveWritePath,
|
|
45
|
+
resolveExistingPath: (value) => this.resolveExistingPath(value, operation.context),
|
|
46
|
+
resolveWritePath: (value) => this.resolveWritePath(value, operation.context),
|
|
54
47
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!missingScopes.length) {
|
|
62
|
-
const leaseIds = [...new Set(matches.leases.map((lease) => lease.id))];
|
|
63
|
-
return {
|
|
64
|
-
allowed: true,
|
|
65
|
-
source: "lease",
|
|
66
|
-
leaseId: leaseIds[0],
|
|
67
|
-
leaseIds,
|
|
68
|
-
scope: requiredScopes[0],
|
|
69
|
-
scopes: requiredScopes,
|
|
70
|
-
};
|
|
48
|
+
const decision = operationDecision(requirement, authority, this.auditTargetKey);
|
|
49
|
+
operation.context.operationAuthorization = { ...decision, allowed: false };
|
|
50
|
+
if (requirement && requirement.canonicalTargets?.some((target) => this.protectedRoots.some((root) => isWithinRoot(root, target)))) {
|
|
51
|
+
throw new BridgeError("authorization_denied", "Machine Bridge control-plane state cannot be accessed through generic path-based remote tools", {
|
|
52
|
+
details: { reason: "control_plane_state_protected", tool: operation.tool },
|
|
53
|
+
});
|
|
71
54
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
accountId,
|
|
75
|
-
clientId,
|
|
76
|
-
tool: operation.tool,
|
|
77
|
-
scopes: missingScopes,
|
|
78
|
-
category: requirement.category,
|
|
79
|
-
targetHash: requirement.targetHash,
|
|
80
|
-
}, this.now()));
|
|
81
|
-
const scopeText = missingScopes.join(", ");
|
|
82
|
-
throw new BridgeError(
|
|
83
|
-
"authorization_denied",
|
|
84
|
-
`local approval required for ${requirement.category} (${scopeText}); approve the missing scopes with: machine-mcp approval approve ${pending.id} --duration 1h; or open an explicit temporary full window with: machine-mcp approval approve ${pending.id} --full`,
|
|
85
|
-
{
|
|
86
|
-
retryable: true,
|
|
87
|
-
details: {
|
|
88
|
-
reason: "local_approval_required",
|
|
89
|
-
approval_id: pending.id,
|
|
90
|
-
scope: missingScopes[0],
|
|
91
|
-
scopes: missingScopes,
|
|
92
|
-
required_scopes: requiredScopes,
|
|
93
|
-
expires_at: pending.expires_at,
|
|
94
|
-
approve_command: `machine-mcp approval approve ${pending.id} --duration 1h`,
|
|
95
|
-
full_command: `machine-mcp approval approve ${pending.id} --full`,
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
enqueue(callback) {
|
|
102
|
-
const result = this.queue.then(callback, callback);
|
|
103
|
-
this.queue = result.then(() => undefined, () => undefined);
|
|
104
|
-
return result;
|
|
55
|
+
if (requirement) assertRequirementWithinAuthority(requirement, authority, operation.tool);
|
|
56
|
+
return { ...decision, allowed: true };
|
|
105
57
|
}
|
|
106
58
|
}
|
|
107
59
|
|
|
108
|
-
|
|
60
|
+
function operationDecision(requirement, authority, auditTargetKey) {
|
|
109
61
|
return {
|
|
110
|
-
|
|
111
|
-
|
|
62
|
+
source: authority.owner ? "trusted-owner" : "role-ceiling",
|
|
63
|
+
category: requirement?.category || "ordinary operation",
|
|
64
|
+
scopes: requirement ? normalizeScopes(requirement.scopes || [requirement.scope]) : [],
|
|
65
|
+
targetHash: requirement ? auditTargetFingerprint(auditTargetKey, requirement.targetHash) : "",
|
|
112
66
|
};
|
|
113
67
|
}
|
|
114
68
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return withOperationStateLock(root, async () => {
|
|
118
|
-
const pendingState = readPendingState(root);
|
|
119
|
-
const leaseState = readLeaseState(root);
|
|
120
|
-
const current = epochSeconds(now);
|
|
121
|
-
const pending = pendingState.pending.find((entry) => entry.id === String(pendingId || "") && entry.expires_at > current);
|
|
122
|
-
if (!pending) throw new Error("pending approval was not found or has expired");
|
|
123
|
-
const scopes = scopeOverride ? [scopeOverride] : pending.scopes;
|
|
124
|
-
let lease = leaseState.leases.find((entry) => entry.source_approval_id === pending.id && entry.expires_at > current);
|
|
125
|
-
if (!lease) {
|
|
126
|
-
lease = appendLease(leaseState, {
|
|
127
|
-
accountId: pending.account_id,
|
|
128
|
-
clientId: pending.client_id,
|
|
129
|
-
scopes,
|
|
130
|
-
duration,
|
|
131
|
-
sourceApprovalId: pending.id,
|
|
132
|
-
}, now);
|
|
133
|
-
writeJson(leasePath(root), leaseState);
|
|
134
|
-
}
|
|
135
|
-
pendingState.pending = pendingState.pending.filter((entry) => entry.id !== pending.id && entry.expires_at > current);
|
|
136
|
-
writeJson(pendingPath(root), pendingState);
|
|
137
|
-
return lease;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function grantOperationLease(root, { accountId = "*", clientId, scope, duration = "1h" }, now = Date.now()) {
|
|
142
|
-
if (!/^mcp_client_[A-Za-z0-9_-]{43}$/.test(String(clientId || "")) && clientId !== "*") {
|
|
143
|
-
throw new Error("approval grant requires a valid OAuth client id or *");
|
|
144
|
-
}
|
|
145
|
-
if (accountId !== "*" && !/^acct_[A-Za-z0-9_-]{20,96}$/.test(String(accountId || ""))) {
|
|
146
|
-
throw new Error("approval account id is invalid");
|
|
147
|
-
}
|
|
148
|
-
return withOperationStateLock(root, async () => {
|
|
149
|
-
const state = readLeaseState(root);
|
|
150
|
-
const lease = appendLease(state, { accountId, clientId, scopes: [scope], duration }, now);
|
|
151
|
-
writeJson(leasePath(root), state);
|
|
152
|
-
return lease;
|
|
153
|
-
});
|
|
69
|
+
function auditTargetFingerprint(key, value) {
|
|
70
|
+
return createHmac("sha256", key).update(String(value || "")).digest("hex");
|
|
154
71
|
}
|
|
155
72
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const current = epochSeconds(now);
|
|
160
|
-
const requested = String(leaseId || "");
|
|
161
|
-
const removed = state.leases.some((lease) => lease.id === requested && lease.expires_at > current);
|
|
162
|
-
const before = state.leases.length;
|
|
163
|
-
state.leases = state.leases.filter((lease) => lease.expires_at > current && lease.id !== requested);
|
|
164
|
-
if (state.leases.length !== before) writeJson(leasePath(root), state);
|
|
165
|
-
return removed;
|
|
166
|
-
});
|
|
73
|
+
function normalizeScopes(scopes) {
|
|
74
|
+
const requested = new Set((Array.isArray(scopes) ? scopes : []).map((scope) => String(scope || "")));
|
|
75
|
+
return OPERATION_APPROVAL_SCOPES.filter((scope) => requested.has(scope));
|
|
167
76
|
}
|
|
168
77
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
});
|
|
78
|
+
function isWithinRoot(root, target) {
|
|
79
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
80
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
173
81
|
}
|
|
174
82
|
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const maximum = normalizedScopes.includes("full") ? FULL_MAX_LEASE_SECONDS : MAX_LEASE_SECONDS;
|
|
182
|
-
if (!Number.isSafeInteger(seconds) || seconds < 60 || seconds > maximum) {
|
|
183
|
-
throw new Error(`approval duration must be between 1m and ${maximum / 3600}h`);
|
|
83
|
+
function assertRequirementWithinAuthority(requirement, authority, tool) {
|
|
84
|
+
const scopes = normalizeScopes(requirement.scopes || [requirement.scope]);
|
|
85
|
+
const policy = authority.effectivePolicy;
|
|
86
|
+
const role = authority.principal.role;
|
|
87
|
+
if (scopes.includes("full") && !authority.owner) {
|
|
88
|
+
throw new BridgeError("authorization_denied", "full authority is reserved for the owner account");
|
|
184
89
|
}
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return withOperationStateLock(root, async () => {
|
|
190
|
-
const state = readPendingState(root);
|
|
191
|
-
const current = epochSeconds(now);
|
|
192
|
-
const scopes = normalizeScopes(input.scopes);
|
|
193
|
-
state.pending = state.pending.filter((entry) => entry.expires_at > current);
|
|
194
|
-
const existing = state.pending.find((entry) => (
|
|
195
|
-
entry.account_id === input.accountId
|
|
196
|
-
&& entry.client_id === input.clientId
|
|
197
|
-
&& sameScopes(entry.scopes, scopes)
|
|
198
|
-
&& entry.target_hash === input.targetHash
|
|
199
|
-
));
|
|
200
|
-
if (existing) return existing;
|
|
201
|
-
const pending = {
|
|
202
|
-
id: `approval_${randomBytes(18).toString("base64url")}`,
|
|
203
|
-
account_id: input.accountId,
|
|
204
|
-
client_id: input.clientId,
|
|
205
|
-
tool: String(input.tool).slice(0, 128),
|
|
206
|
-
scopes,
|
|
207
|
-
category: String(input.category).slice(0, 160),
|
|
208
|
-
target_hash: input.targetHash,
|
|
209
|
-
created_at: current,
|
|
210
|
-
expires_at: current + PENDING_TTL_SECONDS,
|
|
211
|
-
};
|
|
212
|
-
state.pending.push(pending);
|
|
213
|
-
state.pending = state.pending.slice(-MAX_PENDING);
|
|
214
|
-
writeJson(pendingPath(root), state);
|
|
215
|
-
return pending;
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function appendLease(state, input, now) {
|
|
220
|
-
const scopes = normalizeScopes(input.scopes);
|
|
221
|
-
if (!scopes.length) throw new Error("capability lease requires at least one known scope");
|
|
222
|
-
if (scopes.includes("full") && scopes.length !== 1) throw new Error("full capability lease cannot be combined with narrower scopes");
|
|
223
|
-
const seconds = parseApprovalDuration(input.duration || `${DEFAULT_LEASE_SECONDS / 3600}h`, scopes);
|
|
224
|
-
const current = epochSeconds(now);
|
|
225
|
-
state.leases = state.leases.filter((lease) => lease.expires_at > current);
|
|
226
|
-
if (state.leases.length >= MAX_LEASES) throw new Error(`operation capability leases exceed the maximum of ${MAX_LEASES}`);
|
|
227
|
-
const lease = {
|
|
228
|
-
id: `lease_${randomBytes(18).toString("base64url")}`,
|
|
229
|
-
account_id: String(input.accountId || "*"),
|
|
230
|
-
client_id: String(input.clientId || ""),
|
|
231
|
-
scopes,
|
|
232
|
-
created_at: current,
|
|
233
|
-
expires_at: current + seconds,
|
|
234
|
-
...(input.sourceApprovalId ? { source_approval_id: String(input.sourceApprovalId) } : {}),
|
|
235
|
-
};
|
|
236
|
-
state.leases.push(lease);
|
|
237
|
-
return lease;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function matchingLeases(root, input, now) {
|
|
241
|
-
const current = epochSeconds(now);
|
|
242
|
-
const candidates = currentLeases(root, now).filter((lease) => (
|
|
243
|
-
(lease.account_id === "*" || lease.account_id === input.accountId)
|
|
244
|
-
&& (lease.client_id === "*" || lease.client_id === input.clientId)
|
|
245
|
-
&& lease.expires_at > current
|
|
246
|
-
));
|
|
247
|
-
const byScope = new Map();
|
|
248
|
-
for (const scope of input.scopes) {
|
|
249
|
-
const lease = candidates.find((entry) => entry.scopes.includes("full") || entry.scopes.includes(scope));
|
|
250
|
-
if (lease) byScope.set(scope, lease);
|
|
90
|
+
if ((scopes.includes("external-read") || scopes.includes("external-write")) && !policy.unrestrictedPaths) {
|
|
91
|
+
throw new BridgeError("authorization_denied", `account role ${role} is confined to the selected workspace`, {
|
|
92
|
+
details: { reason: "account_role_path_ceiling", tool, role },
|
|
93
|
+
});
|
|
251
94
|
}
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const current = epochSeconds(now);
|
|
257
|
-
return readLeaseState(root).leases.filter((lease) => lease.expires_at > current);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function currentPending(root, now) {
|
|
261
|
-
const current = epochSeconds(now);
|
|
262
|
-
return readPendingState(root).pending.filter((entry) => entry.expires_at > current);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function readLeaseState(root) {
|
|
266
|
-
return readState(leasePath(root), emptyLeaseState, "leases", validLease);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function readPendingState(root) {
|
|
270
|
-
return readState(pendingPath(root), emptyPendingState, "pending", validPending);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function readState(file, empty, arrayKey, validEntry) {
|
|
274
|
-
if (!existsSync(file)) return empty();
|
|
275
|
-
const raw = readBoundedRegularFileSync(file, MAX_STATE_BYTES, "operation approval state");
|
|
276
|
-
let parsed;
|
|
277
|
-
try { parsed = JSON.parse(raw.toString("utf8")); } catch (error) {
|
|
278
|
-
throw new Error("operation approval state is not valid JSON", { cause: error });
|
|
95
|
+
if ((scopes.includes("sensitive-read") || scopes.includes("sensitive-write") || scopes.includes("credential-operation")) && !authority.owner) {
|
|
96
|
+
throw new BridgeError("authorization_denied", `account role ${role} cannot access credential or persistence-sensitive targets`, {
|
|
97
|
+
details: { reason: "account_role_sensitive_ceiling", tool, role },
|
|
98
|
+
});
|
|
279
99
|
}
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
100
|
+
if (scopes.includes("shell") && !policy.allowExec) {
|
|
101
|
+
throw new BridgeError("authorization_denied", `account role ${role} cannot execute local processes`, {
|
|
102
|
+
details: { reason: "account_role_execution_ceiling", tool, role },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if ((scopes.includes("browser-session") || scopes.includes("application-control") || scopes.includes("data-export")) && !authority.owner) {
|
|
106
|
+
throw new BridgeError("authorization_denied", `account role ${role} cannot control the owner browser or desktop session`, {
|
|
107
|
+
details: { reason: "account_role_interactive_ceiling", tool, role },
|
|
108
|
+
});
|
|
289
109
|
}
|
|
290
|
-
return parsed;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function validLease(value) {
|
|
294
|
-
if (!plainRecord(value)) return false;
|
|
295
|
-
if (!/^lease_[A-Za-z0-9_-]{24}$/.test(value.id)) return false;
|
|
296
|
-
if (!validAccountBinding(value.account_id) || !validClientBinding(value.client_id)) return false;
|
|
297
|
-
if (!validScopes(value.scopes) || !validTimestampRange(value.created_at, value.expires_at)) return false;
|
|
298
|
-
const maximum = value.scopes.includes("full") ? FULL_MAX_LEASE_SECONDS : MAX_LEASE_SECONDS;
|
|
299
|
-
if (value.expires_at - value.created_at > maximum) return false;
|
|
300
|
-
return value.source_approval_id === undefined || /^approval_[A-Za-z0-9_-]{24}$/.test(value.source_approval_id);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function validPending(value) {
|
|
304
|
-
if (!plainRecord(value)) return false;
|
|
305
|
-
return /^approval_[A-Za-z0-9_-]{24}$/.test(value.id)
|
|
306
|
-
&& validAccountBinding(value.account_id, false)
|
|
307
|
-
&& validClientBinding(value.client_id, false)
|
|
308
|
-
&& /^[a-z][a-z0-9_]{0,127}$/.test(value.tool)
|
|
309
|
-
&& validScopes(value.scopes, false)
|
|
310
|
-
&& typeof value.category === "string"
|
|
311
|
-
&& value.category.length > 0
|
|
312
|
-
&& value.category.length <= 160
|
|
313
|
-
&& !/[\u0000-\u001f\u007f]/.test(value.category)
|
|
314
|
-
&& /^[a-f0-9]{64}$/.test(value.target_hash)
|
|
315
|
-
&& validTimestampRange(value.created_at, value.expires_at)
|
|
316
|
-
&& value.expires_at - value.created_at === PENDING_TTL_SECONDS;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function validScopes(value, allowFull = true) {
|
|
320
|
-
if (!Array.isArray(value) || value.length < 1 || value.length > OPERATION_APPROVAL_SCOPES.length) return false;
|
|
321
|
-
if (value.some((scope) => !SCOPE_SET.has(scope))) return false;
|
|
322
|
-
if (!sameScopes(value, normalizeScopes(value))) return false;
|
|
323
|
-
if (!allowFull && value.includes("full")) return false;
|
|
324
|
-
return !value.includes("full") || value.length === 1;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function normalizeScopes(scopes) {
|
|
328
|
-
const requested = new Set((Array.isArray(scopes) ? scopes : []).map((scope) => String(scope || "")));
|
|
329
|
-
return OPERATION_APPROVAL_SCOPES.filter((scope) => requested.has(scope));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function sameScopes(left, right) {
|
|
333
|
-
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((scope, index) => scope === right[index]);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function validAccountBinding(value, wildcard = true) {
|
|
337
|
-
return (wildcard && value === "*") || /^acct_[A-Za-z0-9_-]{20,96}$/.test(String(value || ""));
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function validClientBinding(value, wildcard = true) {
|
|
341
|
-
return (wildcard && value === "*") || /^mcp_client_[A-Za-z0-9_-]{43}$/.test(String(value || ""));
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function validTimestampRange(createdAt, expiresAt) {
|
|
345
|
-
return Number.isSafeInteger(createdAt)
|
|
346
|
-
&& Number.isSafeInteger(expiresAt)
|
|
347
|
-
&& createdAt > 0
|
|
348
|
-
&& expiresAt > createdAt;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function plainRecord(value) {
|
|
352
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function writeJson(file, value) {
|
|
356
|
-
ensureOwnerOnlyDirectorySync(path.dirname(file));
|
|
357
|
-
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
358
|
-
if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("operation approval state exceeds its size limit");
|
|
359
|
-
replaceFileAtomicallySync(file, content, { mode: 0o600 });
|
|
360
110
|
}
|
|
361
|
-
|
|
362
|
-
function leasePath(root) { return path.join(path.resolve(root), "operation-leases.json"); }
|
|
363
|
-
function pendingPath(root) { return path.join(path.resolve(root), "operation-pending.json"); }
|
|
364
|
-
function emptyLeaseState() { return { schemaVersion: SCHEMA_VERSION, leases: [] }; }
|
|
365
|
-
function emptyPendingState() { return { schemaVersion: SCHEMA_VERSION, pending: [] }; }
|
|
366
|
-
function epochSeconds(value) { return Math.floor(Number(value) / 1000); }
|
|
@@ -18,6 +18,7 @@ export const OPERATION_APPROVAL_SCOPES = Object.freeze([
|
|
|
18
18
|
const SHELL_TOOLS = new Set(["exec_command", "run_process", "start_process", "run_local_command", "read_process", "write_process", "kill_process"]);
|
|
19
19
|
const PERSISTENT_TOOLS = new Set(["stage_job", "start_job", "list_jobs", "read_job", "cancel_job"]);
|
|
20
20
|
const APPLICATION_CONTROL_TOOLS = new Set(["open_local_application", "inspect_local_application", "operate_local_application"]);
|
|
21
|
+
const COMPUTER_USE_TOOLS = new Set(["computer_observe", "computer_act"]);
|
|
21
22
|
const CREDENTIAL_TOOLS = new Set(["generate_ssh_key_resource"]);
|
|
22
23
|
const BROWSER_PROFILE_TOOLS = new Set([
|
|
23
24
|
"pair_browser_extension", "browser_list_tabs", "browser_manage_tabs", "browser_get_source",
|
|
@@ -27,16 +28,17 @@ const AUTOMATIC_TOOLS = new Set([
|
|
|
27
28
|
"server_info", "project_overview", "list_local_applications", "browser_status", "list_roots",
|
|
28
29
|
"diagnose_runtime", "list_local_resources",
|
|
29
30
|
]);
|
|
30
|
-
const FILE_WRITE_TOOLS = new Set(["write_file", "edit_file"]);
|
|
31
|
+
const FILE_WRITE_TOOLS = new Set(["write_file", "edit_file", "git_commit"]);
|
|
31
32
|
const FILE_READ_TOOLS = new Set([
|
|
32
33
|
"session_bootstrap", "resolve_task_capabilities", "agent_context", "list_local_skills", "load_local_skill", "list_local_commands",
|
|
33
34
|
"list_dir", "list_files", "read_file", "view_image", "search_text",
|
|
34
35
|
"git_status", "git_diff", "git_log", "git_show",
|
|
35
36
|
]);
|
|
36
37
|
const SENSITIVE_SEGMENTS = new Set([
|
|
37
|
-
".ssh", ".aws", ".azure", ".gnupg", ".kube", ".docker", ".npmrc", ".pypirc",
|
|
38
|
+
".git", ".ssh", ".aws", ".azure", ".gnupg", ".kube", ".docker", ".npmrc", ".pypirc",
|
|
38
39
|
"keychains", "cookies", "login data", "credentials", "secrets",
|
|
39
40
|
]);
|
|
41
|
+
const RESOURCE_TOKEN = /\{\{resource:[a-z][a-z0-9._-]{0,63}\}\}/;
|
|
40
42
|
|
|
41
43
|
export function reviewedOperationToolNames() {
|
|
42
44
|
return new Set([
|
|
@@ -44,6 +46,7 @@ export function reviewedOperationToolNames() {
|
|
|
44
46
|
...SHELL_TOOLS,
|
|
45
47
|
...PERSISTENT_TOOLS,
|
|
46
48
|
...APPLICATION_CONTROL_TOOLS,
|
|
49
|
+
...COMPUTER_USE_TOOLS,
|
|
47
50
|
...CREDENTIAL_TOOLS,
|
|
48
51
|
...BROWSER_PROFILE_TOOLS,
|
|
49
52
|
...FILE_WRITE_TOOLS,
|
|
@@ -55,8 +58,24 @@ export function reviewedOperationToolNames() {
|
|
|
55
58
|
|
|
56
59
|
export async function classifyOperation(tool, args = {}, options = {}) {
|
|
57
60
|
const name = String(tool || "");
|
|
61
|
+
if (name === "list_local_resources") return requirement("sensitive-read", "protected local resource inventory", name, {});
|
|
58
62
|
if (SHELL_TOOLS.has(name)) return requirement("shell", "remote shell or process control", name, args);
|
|
63
|
+
if ((name === "stage_job" || name === "start_job") && jobUsesProtectedResources(args)) {
|
|
64
|
+
return requirement(["persistent-job", "sensitive-read"], "managed job using protected local resources", name, protectedJobProjection(args));
|
|
65
|
+
}
|
|
59
66
|
if (PERSISTENT_TOOLS.has(name)) return requirement("persistent-job", "persistent managed job", name, args);
|
|
67
|
+
if (COMPUTER_USE_TOOLS.has(name)) {
|
|
68
|
+
const surface = String(args.surface || "");
|
|
69
|
+
const scopes = surface === "browser" ? ["browser-session"] : surface === "application" ? ["application-control"] : [];
|
|
70
|
+
if (!scopes.length) return null;
|
|
71
|
+
if (name === "computer_act" && args.value_resource) scopes.push("data-export");
|
|
72
|
+
return requirement(scopes, surface === "browser" ? "verified computer use in the existing browser profile" : "verified desktop computer use", name, {
|
|
73
|
+
surface,
|
|
74
|
+
snapshot_id: args.snapshot_id,
|
|
75
|
+
action: args.action,
|
|
76
|
+
value_resource: args.value_resource,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
60
79
|
if (name === "operate_local_application" && args.value_resource) {
|
|
61
80
|
return requirement(["application-control", "data-export"], "desktop application control using protected local data", name, {
|
|
62
81
|
application: args.application,
|
|
@@ -113,17 +132,45 @@ export async function classifyOperation(tool, args = {}, options = {}) {
|
|
|
113
132
|
return null;
|
|
114
133
|
}
|
|
115
134
|
|
|
135
|
+
function jobUsesProtectedResources(args) {
|
|
136
|
+
const steps = [...(Array.isArray(args?.steps) ? args.steps : []), ...(Array.isArray(args?.finally_steps) ? args.finally_steps : [])];
|
|
137
|
+
return steps.some((step) => Boolean(step?.stdin_resource)
|
|
138
|
+
|| (step?.env_resources && Object.keys(step.env_resources).length > 0)
|
|
139
|
+
|| (Array.isArray(step?.argv) && step.argv.some((value) => RESOURCE_TOKEN.test(String(value)))));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function protectedJobProjection(args) {
|
|
143
|
+
const steps = [...(Array.isArray(args?.steps) ? args.steps : []), ...(Array.isArray(args?.finally_steps) ? args.finally_steps : [])];
|
|
144
|
+
return {
|
|
145
|
+
step_count: steps.length,
|
|
146
|
+
resource_reference_count: steps.reduce((count, step) => count + (step?.stdin_resource ? 1 : 0)
|
|
147
|
+
+ Object.keys(step?.env_resources || {}).length
|
|
148
|
+
+ (Array.isArray(step?.argv) ? step.argv.reduce((total, value) => total + (String(value).match(/\{\{resource:[a-z][a-z0-9._-]{0,63}\}\}/g)?.length || 0), 0) : 0), 0),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
116
152
|
function requirement(scopes, category, tool, target) {
|
|
117
153
|
const normalized = normalizeScopes(Array.isArray(scopes) ? scopes : [scopes]);
|
|
118
|
-
if (!normalized.length) throw new Error("operation
|
|
154
|
+
if (!normalized.length) throw new Error("operation authorization requirement is missing a scope");
|
|
119
155
|
return {
|
|
120
156
|
scope: normalized[0],
|
|
121
157
|
scopes: normalized,
|
|
122
158
|
category,
|
|
123
159
|
targetHash: createHash("sha256").update(JSON.stringify({ tool, target: redactTarget(target) })).digest("hex"),
|
|
160
|
+
canonicalTargets: canonicalTargetPaths(target),
|
|
124
161
|
};
|
|
125
162
|
}
|
|
126
163
|
|
|
164
|
+
function canonicalTargetPaths(target) {
|
|
165
|
+
if (!target || typeof target !== "object") return [];
|
|
166
|
+
const values = [];
|
|
167
|
+
if (typeof target.path === "string" && path.isAbsolute(target.path)) values.push(path.resolve(target.path));
|
|
168
|
+
if (Array.isArray(target.paths)) {
|
|
169
|
+
for (const value of target.paths) if (typeof value === "string" && path.isAbsolute(value)) values.push(path.resolve(value));
|
|
170
|
+
}
|
|
171
|
+
return [...new Set(values)];
|
|
172
|
+
}
|
|
173
|
+
|
|
127
174
|
function normalizeScopes(scopes) {
|
|
128
175
|
const requested = new Set(scopes.map((scope) => String(scope || "")));
|
|
129
176
|
return OPERATION_APPROVAL_SCOPES.filter((scope) => requested.has(scope));
|
|
@@ -154,7 +201,7 @@ function pathReadCategory(scopes) {
|
|
|
154
201
|
|
|
155
202
|
function redactTarget(value) {
|
|
156
203
|
if (!value || typeof value !== "object") return String(value || "");
|
|
157
|
-
const out =
|
|
204
|
+
const out = Object.create(null);
|
|
158
205
|
for (const [key, item] of Object.entries(value)) {
|
|
159
206
|
if (["content", "command", "stdin", "value", "old_text", "new_text", "patch"].includes(key)) {
|
|
160
207
|
out[key] = `<sha256:${createHash("sha256").update(String(item || "")).digest("hex")}>`;
|