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,31 @@
|
|
|
1
|
+
const DEFAULT_WARNING_INTERVAL_MS = 60_000;
|
|
2
|
+
|
|
3
|
+
export function createSecurityAuditFailureReporter(logger, options = {}) {
|
|
4
|
+
const now = typeof options.now === "function" ? options.now : Date.now;
|
|
5
|
+
const intervalMs = positiveInteger(options.intervalMs, DEFAULT_WARNING_INTERVAL_MS);
|
|
6
|
+
const state = new Map();
|
|
7
|
+
return {
|
|
8
|
+
report(event, fields, message) {
|
|
9
|
+
if (typeof logger?.event !== "function") return false;
|
|
10
|
+
const key = String(event || "security.audit.failure");
|
|
11
|
+
const current = Number(now()) || 0;
|
|
12
|
+
const previous = state.get(key);
|
|
13
|
+
if (previous && current - previous.lastAt < intervalMs) {
|
|
14
|
+
previous.suppressed += 1;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const suppressed = previous?.suppressed || 0;
|
|
18
|
+
state.set(key, { lastAt: current, suppressed: 0 });
|
|
19
|
+
logger.event("warn", key, {
|
|
20
|
+
...(fields && typeof fields === "object" ? fields : {}),
|
|
21
|
+
...(suppressed > 0 ? { suppressed } : {}),
|
|
22
|
+
}, message);
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function positiveInteger(value, fallback) {
|
|
29
|
+
const number = Number(value);
|
|
30
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
31
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import {
|
|
3
|
+
auditErrorClass,
|
|
4
|
+
createAuditStorageSession,
|
|
5
|
+
unhealthyAuditSnapshot,
|
|
6
|
+
} from "./security-audit-storage.mjs";
|
|
7
|
+
|
|
8
|
+
const root = String(workerData?.root || "");
|
|
9
|
+
const MAX_BATCH = 128;
|
|
10
|
+
const BATCH_DELAY_MS = 5;
|
|
11
|
+
const storage = createAuditStorageSession(root);
|
|
12
|
+
const pending = [];
|
|
13
|
+
let timer = null;
|
|
14
|
+
let draining = false;
|
|
15
|
+
let closeRequested = false;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
parentPort.postMessage({ type: "ready", snapshot: storage.snapshot() });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
parentPort.postMessage({ type: "ready", snapshot: unhealthyAuditSnapshot(error) });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
parentPort.on("message", (message) => {
|
|
24
|
+
if (message?.type === "record") {
|
|
25
|
+
pending.push(message);
|
|
26
|
+
scheduleDrain();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (message?.type === "flush") {
|
|
30
|
+
pending.push({ type: "barrier", id: message.id });
|
|
31
|
+
scheduleDrain(true);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (message?.type === "close") {
|
|
35
|
+
closeRequested = true;
|
|
36
|
+
pending.push({ type: "barrier", id: message.id, close: true });
|
|
37
|
+
scheduleDrain(true);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function scheduleDrain(immediate = false) {
|
|
42
|
+
if (draining || timer) return;
|
|
43
|
+
timer = setTimeout(() => {
|
|
44
|
+
timer = null;
|
|
45
|
+
void drain();
|
|
46
|
+
}, immediate ? 0 : BATCH_DELAY_MS);
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function drain() {
|
|
51
|
+
if (draining) return;
|
|
52
|
+
draining = true;
|
|
53
|
+
try {
|
|
54
|
+
while (pending.length > 0) {
|
|
55
|
+
const records = [];
|
|
56
|
+
const barriers = [];
|
|
57
|
+
while (pending.length > 0 && records.length < MAX_BATCH) {
|
|
58
|
+
const item = pending.shift();
|
|
59
|
+
if (item.type === "record") records.push(item);
|
|
60
|
+
else barriers.push(item);
|
|
61
|
+
if (barriers.length > 0 && records.length === 0) break;
|
|
62
|
+
}
|
|
63
|
+
if (records.length > 0) {
|
|
64
|
+
try {
|
|
65
|
+
const snapshot = await storage.recordBatch(records.map(({ input, nowMs }) => ({ input, nowMs })));
|
|
66
|
+
parentPort.postMessage({
|
|
67
|
+
type: "record_batch_result", ids: records.map((item) => item.id), recorded: true, snapshot,
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const errorClass = auditErrorClass(error);
|
|
71
|
+
const snapshot = unhealthyAuditSnapshot(error);
|
|
72
|
+
parentPort.postMessage({
|
|
73
|
+
type: "record_batch_result", ids: records.map((item) => item.id), recorded: false,
|
|
74
|
+
error_class: errorClass, snapshot,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const barrier of barriers) {
|
|
79
|
+
parentPort.postMessage({ type: barrier.close ? "closed" : "flushed", id: barrier.id });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
draining = false;
|
|
84
|
+
if (pending.length > 0) scheduleDrain(true);
|
|
85
|
+
else if (closeRequested) parentPort.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,14 +1,55 @@
|
|
|
1
1
|
const DEFAULT_ATTEMPTS = 20;
|
|
2
2
|
const DEFAULT_DELAY_MS = 100;
|
|
3
3
|
|
|
4
|
+
export async function waitForStableActiveStatus(
|
|
5
|
+
readStatus,
|
|
6
|
+
{ attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, stableSamples = 5, sleep = delay, identity = stableIdentity } = {},
|
|
7
|
+
) {
|
|
8
|
+
if (typeof readStatus !== "function") throw new TypeError("readStatus must be a function");
|
|
9
|
+
if (typeof identity !== "function") throw new TypeError("identity must be a function");
|
|
10
|
+
const maximum = Number.isInteger(attempts) && attempts > 0 ? attempts : DEFAULT_ATTEMPTS;
|
|
11
|
+
const required = Number.isInteger(stableSamples) && stableSamples > 0 ? stableSamples : 5;
|
|
12
|
+
let status = null;
|
|
13
|
+
let consecutive = 0;
|
|
14
|
+
let priorIdentity = null;
|
|
15
|
+
for (let attempt = 1; attempt <= maximum; attempt += 1) {
|
|
16
|
+
status = await readStatus();
|
|
17
|
+
const currentIdentity = status?.active === true ? identity(status) : null;
|
|
18
|
+
const identityChanged = consecutive > 0
|
|
19
|
+
&& (priorIdentity !== null || currentIdentity !== null)
|
|
20
|
+
&& currentIdentity !== priorIdentity;
|
|
21
|
+
if (status?.active === true && !identityChanged) consecutive += 1;
|
|
22
|
+
else consecutive = status?.active === true ? 1 : 0;
|
|
23
|
+
priorIdentity = currentIdentity;
|
|
24
|
+
if (consecutive >= required) return { stable: true, status, samples: consecutive, attempts: attempt };
|
|
25
|
+
if (attempt < maximum) await sleep(delayMs);
|
|
26
|
+
}
|
|
27
|
+
return { stable: false, status, samples: consecutive, attempts: maximum };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stableIdentity(status) {
|
|
31
|
+
if (Number.isInteger(status?.pid) && status.pid > 0) return `pid:${status.pid}`;
|
|
32
|
+
if (typeof status?.last_run_time === "string" && status.last_run_time) return `run:${status.last_run_time}`;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
4
36
|
export async function waitForInactiveStatus(
|
|
5
37
|
readStatus,
|
|
6
38
|
{ attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = {},
|
|
39
|
+
) {
|
|
40
|
+
return waitForStatus(readStatus, (status) => status?.active !== true, { attempts, delayMs, sleep });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function waitForStatus(
|
|
44
|
+
readStatus,
|
|
45
|
+
predicate,
|
|
46
|
+
{ attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = {},
|
|
7
47
|
) {
|
|
8
48
|
if (typeof readStatus !== "function") throw new TypeError("readStatus must be a function");
|
|
49
|
+
if (typeof predicate !== "function") throw new TypeError("predicate must be a function");
|
|
9
50
|
const maximum = Number.isInteger(attempts) && attempts > 0 ? attempts : DEFAULT_ATTEMPTS;
|
|
10
51
|
let status = await readStatus();
|
|
11
|
-
for (let attempt = 1; status
|
|
52
|
+
for (let attempt = 1; !predicate(status) && attempt < maximum; attempt += 1) {
|
|
12
53
|
await sleep(delayMs);
|
|
13
54
|
status = await readStatus();
|
|
14
55
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { lstatSync } from "node:fs";
|
|
2
|
+
import { readBoundedRegularFileWithInfoSync, unlinkRegularFileIfIdentitySync } from "./secure-file.mjs";
|
|
3
|
+
|
|
4
|
+
const MAX_SERVICE_DEFINITION_BYTES = 256 * 1024;
|
|
5
|
+
|
|
6
|
+
export function snapshotServiceDefinition(file, label = "service definition") {
|
|
7
|
+
try {
|
|
8
|
+
return readBoundedRegularFileWithInfoSync(file, MAX_SERVICE_DEFINITION_BYTES, label, {
|
|
9
|
+
verifyPathIdentity: true,
|
|
10
|
+
rejectMultipleLinks: true,
|
|
11
|
+
}).identity;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error?.code === "ENOENT" || error?.cause?.code === "ENOENT") return null;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function removeServiceDefinitionIfCurrent(file, expectedIdentity, label = "service definition") {
|
|
19
|
+
if (!expectedIdentity) {
|
|
20
|
+
try { lstatSync(file, { bigint: true }); return false; }
|
|
21
|
+
catch (error) { if (error?.code === "ENOENT") return true; throw error; }
|
|
22
|
+
}
|
|
23
|
+
return unlinkRegularFileIfIdentitySync(file, expectedIdentity, label);
|
|
24
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
4
|
-
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
3
|
+
import { inspectPathIfPresentSync, readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
5
4
|
|
|
6
5
|
const SERVICE_ENVIRONMENT_SCHEMA = 1;
|
|
7
6
|
const MAX_SERVICE_ENVIRONMENT_BYTES = 64 * 1024;
|
|
@@ -38,9 +37,10 @@ export function captureServiceEnvironment(environment = process.env) {
|
|
|
38
37
|
return captured;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
export function writeServiceEnvironment(stateRoot, environment = process.env) {
|
|
40
|
+
export function writeServiceEnvironment(stateRoot, environment = process.env, options = {}) {
|
|
42
41
|
const file = serviceEnvironmentPath(stateRoot);
|
|
43
|
-
const
|
|
42
|
+
const previousPayload = serviceEnvironmentIfPresent(file, options);
|
|
43
|
+
const previous = previousPayload ? previousPayload.environment : {};
|
|
44
44
|
const captured = captureServiceEnvironment(environment);
|
|
45
45
|
const merged = { ...previous };
|
|
46
46
|
for (const [key, value] of Object.entries(captured)) {
|
|
@@ -64,8 +64,8 @@ export function writeServiceEnvironment(stateRoot, environment = process.env) {
|
|
|
64
64
|
|
|
65
65
|
export function loadServiceEnvironment(stateRoot, targetEnvironment = process.env, options = {}) {
|
|
66
66
|
const file = serviceEnvironmentPath(stateRoot);
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const payload = serviceEnvironmentIfPresent(file, options);
|
|
68
|
+
if (!payload) return { path: file, keys: [], loaded: false };
|
|
69
69
|
const platform = String(options.platform || process.platform);
|
|
70
70
|
const loaded = [];
|
|
71
71
|
for (const [key, rawValue] of Object.entries(payload.environment)) {
|
|
@@ -77,15 +77,23 @@ export function loadServiceEnvironment(stateRoot, targetEnvironment = process.en
|
|
|
77
77
|
return { path: file, keys: loaded.sort(), loaded: true };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export function serviceEnvironmentSummary(stateRoot) {
|
|
80
|
+
export function serviceEnvironmentSummary(stateRoot, options = {}) {
|
|
81
81
|
const file = serviceEnvironmentPath(stateRoot);
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
const payload = serviceEnvironmentIfPresent(file, options);
|
|
83
|
+
if (!payload) return { configured: false, keys: [] };
|
|
84
84
|
return { configured: true, keys: Object.keys(payload.environment).sort() };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function serviceEnvironmentIfPresent(file, options = {}) {
|
|
88
|
+
const inspect = options.inspectPathIfPresentSync || inspectPathIfPresentSync;
|
|
89
|
+
const info = inspect(file, "service environment file");
|
|
90
|
+
if (!info) return null;
|
|
91
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("service environment file must be a regular file and not a symbolic link");
|
|
92
|
+
return parseServiceEnvironment(file);
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
function parseServiceEnvironment(file) {
|
|
88
|
-
const content = readBoundedRegularFileSync(file, MAX_SERVICE_ENVIRONMENT_BYTES, "service environment file");
|
|
96
|
+
const content = readBoundedRegularFileSync(file, MAX_SERVICE_ENVIRONMENT_BYTES, "service environment file", { verifyPathIdentity: true, rejectMultipleLinks: true });
|
|
89
97
|
let payload;
|
|
90
98
|
try {
|
|
91
99
|
payload = JSON.parse(content.toString("utf8"));
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { realpathSync, statSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { removeOwnedJsonFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
6
|
+
import { inspectPathIfPresentSync, readBoundedRegularFileSync, readBoundedRegularFileWithInfoSync, unlinkRegularFileIfIdentitySync } from "./secure-file.mjs";
|
|
7
|
+
import { machineServiceControlRoot } from "./state.mjs";
|
|
8
|
+
import { ensureOwnerOnlyDir } from "./secure-file.mjs";
|
|
9
|
+
const SCHEMA_VERSION = 1;
|
|
10
|
+
const MAX_BYTES = 64 * 1024;
|
|
11
|
+
const FILE_NAME = "service-owner.json";
|
|
12
|
+
const VERSION = /^[0-9A-Za-z.+_-]{1,64}$/;
|
|
13
|
+
const TRANSACTION = /^[A-Za-z0-9_-]{20,128}$/;
|
|
14
|
+
/** @typedef {{ schemaVersion: 1, status: "pending" | "committed", transactionId: string, workspace: string, stateRoot: string, entryScript: string, version: string, createdAt: string, committedAt: string | null }} ServiceOwner */
|
|
15
|
+
/** @typedef {{ workspace?: unknown, stateRoot?: unknown, entryScript?: unknown, version?: unknown }} ServiceOwnerSpec */
|
|
16
|
+
/** @typedef {{ controlRoot?: string, inspectPathIfPresentSync?: typeof inspectPathIfPresentSync }} ServiceOwnerOptions */
|
|
17
|
+
/** @param {ServiceOwnerOptions} [options] */ export function serviceOwnerPath(options = {}) {
|
|
18
|
+
return path.join(machineServiceControlRoot(options), FILE_NAME);
|
|
19
|
+
}
|
|
20
|
+
/** @param {ServiceOwnerSpec} spec @param {ServiceOwnerOptions} [options] */ export function beginServiceOwnerUpdate(spec, options = {}) {
|
|
21
|
+
const file = serviceOwnerPath(options);
|
|
22
|
+
ensureOwnerOnlyDir(path.dirname(file));
|
|
23
|
+
const previous = readOwnerSnapshotIfPresent(file, options);
|
|
24
|
+
const transactionId = randomBytes(24).toString("base64url");
|
|
25
|
+
const createdAt = new Date().toISOString();
|
|
26
|
+
const pending = normalizeOwner({
|
|
27
|
+
schemaVersion: SCHEMA_VERSION,
|
|
28
|
+
status: "pending",
|
|
29
|
+
transactionId,
|
|
30
|
+
workspace: spec?.workspace,
|
|
31
|
+
stateRoot: spec?.stateRoot,
|
|
32
|
+
entryScript: spec?.entryScript,
|
|
33
|
+
version: spec?.version,
|
|
34
|
+
createdAt,
|
|
35
|
+
committedAt: null,
|
|
36
|
+
});
|
|
37
|
+
writeOwner(file, pending);
|
|
38
|
+
let closed = false;
|
|
39
|
+
return {
|
|
40
|
+
path: file,
|
|
41
|
+
owner: pending,
|
|
42
|
+
commit() {
|
|
43
|
+
if (closed) throw new Error("service owner transaction is already closed");
|
|
44
|
+
assertCurrentTransaction(file, transactionId, "pending");
|
|
45
|
+
const committed = normalizeOwner({ ...pending, status: "committed", committedAt: new Date().toISOString() });
|
|
46
|
+
writeOwner(file, committed);
|
|
47
|
+
closed = true;
|
|
48
|
+
return committed;
|
|
49
|
+
},
|
|
50
|
+
rollback() {
|
|
51
|
+
if (closed) return false;
|
|
52
|
+
assertCurrentTransaction(file, transactionId, "pending");
|
|
53
|
+
if (previous) replaceFileAtomicallySync(file, previous.raw, { mode: 0o600 });
|
|
54
|
+
else if (!removeOwnedJsonFileSync(file, { transactionId, status: "pending" }, { maxBytes: MAX_BYTES })) {
|
|
55
|
+
throw new Error("machine service owner transaction changed before rollback");
|
|
56
|
+
}
|
|
57
|
+
closed = true;
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** @param {ServiceOwnerOptions} [options] @returns {ServiceOwner | null} */ export function loadServiceOwner(options = {}) {
|
|
63
|
+
const file = serviceOwnerPath(options);
|
|
64
|
+
return readOwnerSnapshotIfPresent(file, options)?.owner || null;
|
|
65
|
+
}
|
|
66
|
+
/** @param {ServiceOwnerOptions} [options] @returns {ServiceOwner | null} */ export function loadCommittedServiceOwner(options = {}) {
|
|
67
|
+
const owner = loadServiceOwner(options);
|
|
68
|
+
if (!owner) return null;
|
|
69
|
+
if (owner.status !== "committed") {
|
|
70
|
+
throw new Error("machine service owner transition is incomplete; reinstall the service before starting it");
|
|
71
|
+
}
|
|
72
|
+
return owner;
|
|
73
|
+
}
|
|
74
|
+
/** @param {ServiceOwnerOptions} [options] */ export function removeServiceOwner(options = {}) {
|
|
75
|
+
const file = serviceOwnerPath(options);
|
|
76
|
+
const inspect = options.inspectPathIfPresentSync || inspectPathIfPresentSync;
|
|
77
|
+
const info = inspect(file, "machine service owner file");
|
|
78
|
+
if (!info) return false;
|
|
79
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("machine service owner file must be a regular file and not a symbolic link");
|
|
80
|
+
const opened = readBoundedRegularFileWithInfoSync(file, MAX_BYTES, "machine service owner file", {
|
|
81
|
+
verifyPathIdentity: true, rejectMultipleLinks: true,
|
|
82
|
+
});
|
|
83
|
+
if (!unlinkRegularFileIfIdentitySync(file, opened.identity, "machine service owner file")) {
|
|
84
|
+
throw new Error("machine service owner changed before removal");
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
/** @param {string} file @param {ServiceOwnerOptions} [options] @returns {{ owner: ServiceOwner, raw: Buffer } | null} */ function readOwnerSnapshotIfPresent(file, options = {}) {
|
|
89
|
+
const inspect = options.inspectPathIfPresentSync || inspectPathIfPresentSync;
|
|
90
|
+
const info = inspect(file, "machine service owner file");
|
|
91
|
+
if (!info) return null;
|
|
92
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("machine service owner file must be a regular file and not a symbolic link");
|
|
93
|
+
return readOwnerSnapshot(file);
|
|
94
|
+
}
|
|
95
|
+
/** @param {string} file @returns {{ owner: ServiceOwner, raw: Buffer }} */ function readOwnerSnapshot(file) {
|
|
96
|
+
const raw = readBoundedRegularFileSync(file, MAX_BYTES, "machine service owner file", { verifyPathIdentity: true, rejectMultipleLinks: true });
|
|
97
|
+
let parsed;
|
|
98
|
+
try { parsed = JSON.parse(raw.toString("utf8")); }
|
|
99
|
+
catch (error) { throw new Error("machine service owner file is not valid JSON", { cause: error }); }
|
|
100
|
+
return { owner: normalizeOwner(parsed), raw };
|
|
101
|
+
}
|
|
102
|
+
/** @param {string} file @param {ServiceOwner} owner */ function writeOwner(file, owner) {
|
|
103
|
+
const content = `${JSON.stringify(owner, null, 2)}\n`;
|
|
104
|
+
if (Buffer.byteLength(content) > MAX_BYTES) throw new Error("machine service owner record exceeds the size limit");
|
|
105
|
+
replaceFileAtomicallySync(file, content, { mode: 0o600 });
|
|
106
|
+
}
|
|
107
|
+
/** @param {string} file @param {string} transactionId @param {"pending" | "committed"} status */ function assertCurrentTransaction(file, transactionId, status) {
|
|
108
|
+
const current = readOwnerSnapshot(file).owner;
|
|
109
|
+
if (current.transactionId !== transactionId || current.status !== status) {
|
|
110
|
+
throw new Error("machine service owner transaction changed before completion");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** @param {unknown} value @returns {ServiceOwner} */ function normalizeOwner(value) {
|
|
114
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("machine service owner must be an object");
|
|
115
|
+
const record = /** @type {Record<string, unknown>} */ (value);
|
|
116
|
+
if (record.schemaVersion !== SCHEMA_VERSION) throw new Error("machine service owner schema is unsupported");
|
|
117
|
+
if (record.status !== "pending" && record.status !== "committed") throw new Error("machine service owner status is invalid");
|
|
118
|
+
if (typeof record.transactionId !== "string" || !TRANSACTION.test(record.transactionId)) throw new Error("machine service owner transaction id is invalid");
|
|
119
|
+
if (typeof record.version !== "string" || !VERSION.test(record.version)) throw new Error("machine service owner version is invalid");
|
|
120
|
+
const workspace = canonicalDirectory(record.workspace, "workspace");
|
|
121
|
+
const stateRoot = canonicalDirectory(record.stateRoot, "state root");
|
|
122
|
+
const entryScript = canonicalFile(record.entryScript, "entry script");
|
|
123
|
+
const createdAt = isoTimestamp(record.createdAt, "createdAt");
|
|
124
|
+
const committedAt = record.status === "committed" ? isoTimestamp(record.committedAt, "committedAt") : null;
|
|
125
|
+
return { schemaVersion: SCHEMA_VERSION, status: record.status, transactionId: record.transactionId,
|
|
126
|
+
workspace, stateRoot, entryScript, version: record.version, createdAt, committedAt };
|
|
127
|
+
}
|
|
128
|
+
/** @param {unknown} value @param {string} label */ function canonicalDirectory(value, label) {
|
|
129
|
+
const canonical = canonicalPath(value, label);
|
|
130
|
+
if (!statSync(canonical).isDirectory()) throw new Error(`machine service owner ${label} is not a directory`);
|
|
131
|
+
return canonical;
|
|
132
|
+
}
|
|
133
|
+
/** @param {unknown} value @param {string} label */ function canonicalFile(value, label) {
|
|
134
|
+
const canonical = canonicalPath(value, label);
|
|
135
|
+
if (!statSync(canonical).isFile()) throw new Error(`machine service owner ${label} is not a file`);
|
|
136
|
+
return canonical;
|
|
137
|
+
}
|
|
138
|
+
/** @param {unknown} value @param {string} label */ function canonicalPath(value, label) {
|
|
139
|
+
if (typeof value !== "string" || !path.isAbsolute(value)) throw new Error(`machine service owner ${label} must be absolute`);
|
|
140
|
+
try { return realpathSync.native ? realpathSync.native(value) : realpathSync(value); }
|
|
141
|
+
catch (error) { throw new Error(`machine service owner ${label} is unavailable`, { cause: error }); }
|
|
142
|
+
}
|
|
143
|
+
/** @param {unknown} value @param {string} label */ function isoTimestamp(value, label) {
|
|
144
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new Error(`machine service owner ${label} is invalid`);
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export async function stopOwnedPlatformService({
|
|
2
|
+
state,
|
|
3
|
+
inspectWorkspaceDaemon,
|
|
4
|
+
ownsPlatformAutostart,
|
|
5
|
+
stopPlatformService,
|
|
6
|
+
} = {}) {
|
|
7
|
+
if (!state) throw new TypeError("service ownership check requires state");
|
|
8
|
+
if (typeof inspectWorkspaceDaemon !== "function") throw new TypeError("service ownership check requires inspectWorkspaceDaemon");
|
|
9
|
+
if (typeof ownsPlatformAutostart !== "function") throw new TypeError("service ownership check requires ownsPlatformAutostart");
|
|
10
|
+
if (typeof stopPlatformService !== "function") throw new TypeError("service ownership check requires stopPlatformService");
|
|
11
|
+
const daemon = inspectWorkspaceDaemon(state);
|
|
12
|
+
if (!ownsPlatformAutostart(daemon)) {
|
|
13
|
+
return { owned: false, stopped: false, daemon, provider: null };
|
|
14
|
+
}
|
|
15
|
+
const provider = await stopPlatformService();
|
|
16
|
+
return { owned: true, stopped: provider?.ok !== false, daemon, provider };
|
|
17
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { restartOwnedServiceRuntime } from "./service-runtime.mjs";
|
|
4
|
+
import { createLogger } from "./log.mjs";
|
|
5
|
+
import { acquireMachineServiceLockWithWait } from "./state.mjs";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DELAY_MS = 300;
|
|
8
|
+
const MAX_DELAY_MS = 5_000;
|
|
9
|
+
|
|
10
|
+
export async function runServiceRestartHandoff(options = {}) {
|
|
11
|
+
const delayMs = boundedDelay(options.delayMs ?? process.argv[2]);
|
|
12
|
+
const restart = typeof options.restartServiceRuntime === "function"
|
|
13
|
+
? options.restartServiceRuntime
|
|
14
|
+
: typeof options.restartAutostart === "function" ? options.restartAutostart : restartOwnedServiceRuntime;
|
|
15
|
+
const sleep = typeof options.sleep === "function" ? options.sleep : delay;
|
|
16
|
+
const logger = options.logger || createLogger({ component: "service-restart", level: "warn", format: "json", stderrOnly: true });
|
|
17
|
+
const acquireLock = typeof options.acquireServiceLock === "function"
|
|
18
|
+
? options.acquireServiceLock
|
|
19
|
+
: () => acquireMachineServiceLockWithWait({ operation: "service-restart", logger, ...(options.serviceLockOptions || {}) });
|
|
20
|
+
await sleep(delayMs);
|
|
21
|
+
const lock = await acquireLock();
|
|
22
|
+
if (!lock?.acquired || typeof lock.release !== "function") {
|
|
23
|
+
throw new Error("machine-service operation lock could not be acquired for restart");
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const result = await restart({ logger });
|
|
27
|
+
if (result?.ok !== true) {
|
|
28
|
+
const error = new Error(`service restart handoff failed (${result?.reason || result?.provider || "unknown"})`);
|
|
29
|
+
error.result = result;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
} finally {
|
|
34
|
+
lock.release();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function serviceRestartHandoffMain(options = {}) {
|
|
39
|
+
const run = typeof options.run === "function"
|
|
40
|
+
? options.run
|
|
41
|
+
: () => runServiceRestartHandoff(options.handoffOptions);
|
|
42
|
+
const logger = options.logger || createLogger({ component: "service-restart", level: "error", format: "json", stderrOnly: true });
|
|
43
|
+
try {
|
|
44
|
+
await run();
|
|
45
|
+
return 0;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error("service restart handoff failed", { error_class: error?.code || error?.name || "execution_failed" });
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function boundedDelay(value) {
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
if (!Number.isFinite(parsed)) return DEFAULT_DELAY_MS;
|
|
55
|
+
return Math.min(MAX_DELAY_MS, Math.max(50, Math.floor(parsed)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function delay(milliseconds) {
|
|
59
|
+
return new Promise((resolvePromise) => { setTimeout(resolvePromise, milliseconds); });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
63
|
+
process.exitCode = await serviceRestartHandoffMain();
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { BridgeError } from "./errors.mjs";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_HANDOFF_DELAY_MS = 300;
|
|
6
|
+
|
|
7
|
+
export function scheduleServiceRestart(options = {}) {
|
|
8
|
+
const platform = String(options.platform || process.platform);
|
|
9
|
+
if (platform === "win32") {
|
|
10
|
+
throw new BridgeError("unavailable", "in-process Windows service restart is not behavior-verified; run `machine-mcp service stop` and then `machine-mcp service start` from an independent terminal");
|
|
11
|
+
}
|
|
12
|
+
const spawnProcess = typeof options.spawnProcess === "function" ? options.spawnProcess : spawn;
|
|
13
|
+
const node = String(options.node || process.execPath);
|
|
14
|
+
const helper = String(options.helper || fileURLToPath(new URL("./service-restart-handoff.mjs", import.meta.url)));
|
|
15
|
+
const delayMs = boundedPositiveInteger(options.delayMs, DEFAULT_HANDOFF_DELAY_MS);
|
|
16
|
+
const child = spawnProcess(node, [helper, String(delayMs)], {
|
|
17
|
+
detached: true,
|
|
18
|
+
stdio: "ignore",
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
shell: false,
|
|
21
|
+
env: serviceControlEnvironment(options.env || process.env),
|
|
22
|
+
});
|
|
23
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
24
|
+
let settled = false;
|
|
25
|
+
const settle = (callback) => {
|
|
26
|
+
if (settled) return;
|
|
27
|
+
settled = true;
|
|
28
|
+
callback();
|
|
29
|
+
};
|
|
30
|
+
child.once("spawn", () => settle(() => {
|
|
31
|
+
child.unref?.();
|
|
32
|
+
resolvePromise({ ok: true, scheduled: true, delay_ms: delayMs });
|
|
33
|
+
}));
|
|
34
|
+
child.once("error", (error) => settle(() => rejectPromise(error)));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function serviceControlEnvironment(source = {}) {
|
|
39
|
+
const env = {};
|
|
40
|
+
for (const key of ["PATH", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "LANG", "LC_ALL", "LC_CTYPE"]) {
|
|
41
|
+
if (typeof source[key] === "string" && source[key]) env[key] = source[key];
|
|
42
|
+
}
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function boundedPositiveInteger(value, fallback) {
|
|
47
|
+
const parsed = Number(value);
|
|
48
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
49
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {Record<string, unknown> & { alive?: boolean, verified_service_daemon?: boolean, mode?: string, startup_readiness_verified?: boolean, identity_reason?: string, pid?: number | null }} DaemonStatus */
|
|
4
|
+
/** @typedef {{ inspectDaemon?: () => DaemonStatus, attempts?: number, delayMs?: number, sleep?: (milliseconds: number) => Promise<void>, replacementPid?: number | null }} OwnedWaitOptions */
|
|
5
|
+
|
|
6
|
+
const DEFAULT_ATTEMPTS = 900;
|
|
7
|
+
const DEFAULT_DELAY_MS = 100;
|
|
8
|
+
|
|
9
|
+
/** @param {OwnedWaitOptions} [options] */
|
|
10
|
+
export async function waitForOwnedServiceDaemon(options = {}) {
|
|
11
|
+
const { inspectDaemon, attempts = DEFAULT_ATTEMPTS, delayMs = DEFAULT_DELAY_MS, sleep = delay } = options;
|
|
12
|
+
if (typeof inspectDaemon !== "function") throw new TypeError("owned service convergence requires inspectDaemon");
|
|
13
|
+
const maximum = boundedAttempts(attempts);
|
|
14
|
+
const replacementPid = positivePid(options.replacementPid);
|
|
15
|
+
let replacementPending = false;
|
|
16
|
+
/** @type {DaemonStatus | null} */
|
|
17
|
+
let daemon = null;
|
|
18
|
+
for (let attempt = 1; attempt <= maximum; attempt += 1) {
|
|
19
|
+
daemon = inspectDaemon();
|
|
20
|
+
if (readyOwnedDaemon(daemon)) {
|
|
21
|
+
replacementPending = Boolean(replacementPid && Number(daemon.pid) === replacementPid);
|
|
22
|
+
if (!replacementPending) return { ok: true, attempts: attempt, daemon, reason: null };
|
|
23
|
+
}
|
|
24
|
+
if (daemon?.alive === true && daemon.verified_service_daemon !== true) {
|
|
25
|
+
return { ok: false, attempts: attempt, daemon,
|
|
26
|
+
reason: `daemon_identity_${daemon.identity_reason || "unverified"}` };
|
|
27
|
+
}
|
|
28
|
+
if (attempt < maximum) await sleep(delayMs);
|
|
29
|
+
}
|
|
30
|
+
return { ok: false, attempts: maximum, daemon,
|
|
31
|
+
reason: replacementPending ? "daemon_replacement_not_observed"
|
|
32
|
+
: daemon?.alive ? "daemon_readiness_not_verified" : "daemon_not_running" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {DaemonStatus | null | undefined} daemon */
|
|
36
|
+
export function readyOwnedDaemon(daemon) {
|
|
37
|
+
return daemon?.alive === true
|
|
38
|
+
&& daemon.verified_service_daemon === true
|
|
39
|
+
&& daemon.mode === "service"
|
|
40
|
+
&& daemon.startup_readiness_verified === true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {unknown} value */
|
|
44
|
+
function boundedAttempts(value) {
|
|
45
|
+
const parsed = value === undefined ? DEFAULT_ATTEMPTS : Number(value);
|
|
46
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 1800) {
|
|
47
|
+
throw new Error("owned service convergence attempts must be between 1 and 1800");
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
function positivePid(/** @type {unknown} */ value) { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 ? parsed : null; }
|
|
52
|
+
function delay(/** @type {number} */ milliseconds) { return new Promise(resolve => { setTimeout(resolve, milliseconds); }); }
|