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
package/src/local/state.mjs
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, realpathSync,
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
|
|
7
|
-
import { replaceFileSync } from "./atomic-fs.mjs";
|
|
8
5
|
import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
6
|
+
import { preserveFileSnapshotSync } from "./file-snapshot-preservation.mjs";
|
|
9
7
|
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
10
|
-
import { createDeviceIdentity
|
|
8
|
+
import { createDeviceIdentity } from "./device-identity.mjs";
|
|
9
|
+
import { validateDeviceRootIdentity } from "./device-root-provider.mjs";
|
|
11
10
|
import { currentProcessStartTimeMs, inspectProcessInstance } from "./process-identity.mjs";
|
|
12
|
-
import {
|
|
11
|
+
import { ensureOwnerOnlyDir, inspectPathIfPresentSync, ownerOnlyFile, readBoundedRegularFileSync, readBoundedRegularFileWithInfoSync, retryTransientMultipleLinksSync, unlinkRegularFileIfIdentitySync } from "./secure-file.mjs";
|
|
12
|
+
import { isPlainRecord } from "./records.mjs";
|
|
13
|
+
import { exactFilesystemInteger, filesystemIdentity, filesystemTimeMs, sameFilesystemIdentity } from "./filesystem-identity.mjs";
|
|
14
|
+
import { appName, packageRoot } from "./package-identity.mjs";
|
|
15
|
+
import { inspectStateRootGeneration, pruneRetiredStateRootDirectories, removeStateRootGenerationIfCurrent } from "./state-root-retirement.mjs";
|
|
16
|
+
import { validateOwnedStateNamespaces } from "./state-root-owned-namespaces.mjs";
|
|
13
17
|
|
|
14
|
-
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
15
|
-
export const appName = String(serverMetadata.name);
|
|
16
18
|
const STATE_MARKER = ".machine-bridge-mcp-state";
|
|
17
19
|
const STATE_MARKER_SCHEMA = 2;
|
|
18
20
|
export const STATE_SCHEMA_VERSION = 6;
|
|
19
21
|
const GLOBAL_CONFIG_SCHEMA = 1;
|
|
22
|
+
const RECOVERY_MARKER_SCHEMA = 1;
|
|
20
23
|
const CORRUPT_RECOVERY = Symbol("corrupt-recovery");
|
|
21
24
|
const MAX_STATE_JSON_BYTES = 2 * 1024 * 1024;
|
|
22
25
|
const MAX_LOCK_BYTES = 64 * 1024;
|
|
@@ -24,6 +27,7 @@ const MAX_MARKER_BYTES = 4096;
|
|
|
24
27
|
const STARTUP_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
25
28
|
const MALFORMED_LOCK_GRACE_MS = 60_000;
|
|
26
29
|
const MAINTENANCE_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
30
|
+
const MACHINE_SERVICE_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
27
31
|
|
|
28
32
|
export function expandHome(input = "") {
|
|
29
33
|
if (!input || input === "~") return os.homedir();
|
|
@@ -31,6 +35,12 @@ export function expandHome(input = "") {
|
|
|
31
35
|
return input;
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
function expandHomeFrom(input, home) {
|
|
39
|
+
if (!input || input === "~") return home;
|
|
40
|
+
if (input.startsWith("~/")) return path.join(home, input.slice(2));
|
|
41
|
+
return input;
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
export function resolveWorkspace(input = process.cwd()) {
|
|
35
45
|
const resolved = path.resolve(expandHome(input));
|
|
36
46
|
return realpathSync.native ? realpathSync.native(resolved) : realpathSync(resolved);
|
|
@@ -46,13 +56,16 @@ export function ensureWorkspaceDirectory(input) {
|
|
|
46
56
|
return resolveWorkspace(requested);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
export function defaultStateRoot() {
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
export function defaultStateRoot(options = {}) {
|
|
60
|
+
const platform = String(options.platform || process.platform);
|
|
61
|
+
const home = path.resolve(String(options.home || os.homedir()));
|
|
62
|
+
const environment = options.environment && typeof options.environment === "object" ? options.environment : process.env;
|
|
63
|
+
if (platform === "win32") {
|
|
64
|
+
const base = environment.APPDATA ? expandHomeFrom(String(environment.APPDATA), home) : path.join(home, "AppData", "Roaming");
|
|
52
65
|
return path.join(base, appName);
|
|
53
66
|
}
|
|
54
|
-
if (
|
|
55
|
-
return path.join(
|
|
67
|
+
if (environment.XDG_STATE_HOME) return path.join(expandHomeFrom(String(environment.XDG_STATE_HOME), home), appName);
|
|
68
|
+
return path.join(home, ".local", "state", appName);
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
|
|
@@ -60,13 +73,16 @@ function configPath(stateRoot = defaultStateRoot()) {
|
|
|
60
73
|
return path.join(expandHome(stateRoot), "config.json");
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
export function loadGlobalConfig(stateRoot = defaultStateRoot()) {
|
|
76
|
+
export function loadGlobalConfig(stateRoot = defaultStateRoot(), options = {}) {
|
|
64
77
|
const root = path.resolve(expandHome(stateRoot));
|
|
65
78
|
assertNoForeignMaintenance(root);
|
|
66
79
|
const file = configPath(root);
|
|
67
|
-
|
|
80
|
+
const inspect = options.inspectPathIfPresentSync || inspectPathIfPresentSync;
|
|
81
|
+
const info = inspect(file, "global configuration file");
|
|
82
|
+
if (!info) return { schemaVersion: GLOBAL_CONFIG_SCHEMA };
|
|
83
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("global configuration file must be a regular file and not a symbolic link");
|
|
68
84
|
ownerOnlyFile(file);
|
|
69
|
-
const config = readJsonObjectOrBackup(file);
|
|
85
|
+
const config = readJsonObjectOrBackup(file, { allowEmptyRecovery: true });
|
|
70
86
|
if (config[CORRUPT_RECOVERY]) return { schemaVersion: GLOBAL_CONFIG_SCHEMA };
|
|
71
87
|
if (config.schemaVersion !== GLOBAL_CONFIG_SCHEMA) {
|
|
72
88
|
throw new Error("global configuration schema is obsolete; remove the state root and initialize the current version");
|
|
@@ -100,16 +116,21 @@ export function selectedWorkspace(stateRoot = defaultStateRoot()) {
|
|
|
100
116
|
|
|
101
117
|
export function validateStateRootForRemoval(stateRoot = defaultStateRoot()) {
|
|
102
118
|
const root = path.resolve(expandHome(stateRoot));
|
|
103
|
-
if (!
|
|
119
|
+
if (!inspectPathIfPresentSync(root, "state root")) return { exists: false, root };
|
|
104
120
|
const canonical = assertSafeStateRootForRemoval(root);
|
|
105
121
|
return { exists: true, root: canonical };
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
export function removeStateRoot(stateRoot = defaultStateRoot()) {
|
|
109
|
-
const
|
|
125
|
+
const requested = path.resolve(expandHome(stateRoot));
|
|
126
|
+
pruneRetiredStateRootDirectories(requested, assertSafeStateRootForRemoval);
|
|
127
|
+
const validation = validateStateRootForRemoval(requested);
|
|
110
128
|
if (!validation.exists) return false;
|
|
111
|
-
|
|
112
|
-
return true;
|
|
129
|
+
const generation = inspectStateRootGeneration(validation.root);
|
|
130
|
+
if (removeStateRootGenerationIfCurrent(validation.root, generation, assertSafeStateRootForRemoval)) return true;
|
|
131
|
+
pruneRetiredStateRootDirectories(requested, assertSafeStateRootForRemoval);
|
|
132
|
+
if (!inspectPathIfPresentSync(validation.root, "state root")) return false;
|
|
133
|
+
throw new Error("state root changed during generation-bound removal; state requires inspection");
|
|
113
134
|
}
|
|
114
135
|
|
|
115
136
|
function workspaceHash(workspace) {
|
|
@@ -127,7 +148,7 @@ function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
|
|
|
127
148
|
function canonicalizePotentialPath(input) {
|
|
128
149
|
let existing = path.resolve(input);
|
|
129
150
|
const suffix = [];
|
|
130
|
-
while (!
|
|
151
|
+
while (!inspectPathIfPresentSync(existing, "path identity")) {
|
|
131
152
|
const parent = path.dirname(existing);
|
|
132
153
|
if (parent === existing) break;
|
|
133
154
|
suffix.unshift(path.basename(existing));
|
|
@@ -149,6 +170,16 @@ function assertStateRootSeparatedFromWorkspace(stateRoot, workspace) {
|
|
|
149
170
|
}
|
|
150
171
|
}
|
|
151
172
|
|
|
173
|
+
function samePotentialPathIdentity(left, right) {
|
|
174
|
+
try {
|
|
175
|
+
const a = canonicalizePotentialPath(String(left));
|
|
176
|
+
const b = canonicalizePotentialPath(String(right));
|
|
177
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
152
183
|
function sameWorkspaceIdentity(left, right) {
|
|
153
184
|
try {
|
|
154
185
|
const a = resolveWorkspace(left);
|
|
@@ -169,13 +200,24 @@ export function loadState(workspace, options = {}) {
|
|
|
169
200
|
const profileDir = profileDirForWorkspace(canonicalWorkspace, stateRoot);
|
|
170
201
|
const statePath = path.join(profileDir, "state.json");
|
|
171
202
|
ensureOwnerOnlyDir(profileDir);
|
|
203
|
+
removeObsoleteOperationLeaseState(profileDir);
|
|
204
|
+
const recoveryPath = recoveryMarkerPath(statePath);
|
|
205
|
+
const recoveryPending = Boolean(inspectPathIfPresentSync(recoveryPath, "state recovery marker"));
|
|
206
|
+
const recoverySnapshot = recoveryPending ? readRecoveryMarker(recoveryPath, statePath) : null;
|
|
207
|
+
const recoveryMarker = recoverySnapshot?.marker || null;
|
|
172
208
|
let state = {};
|
|
173
|
-
if (
|
|
209
|
+
if (inspectPathIfPresentSync(statePath, "workspace state")) {
|
|
174
210
|
ownerOnlyFile(statePath);
|
|
175
|
-
state = readJsonObjectOrBackup(statePath);
|
|
176
|
-
if (
|
|
211
|
+
state = readJsonObjectOrBackup(statePath, { recoveryPath });
|
|
212
|
+
if (state.schemaVersion !== STATE_SCHEMA_VERSION) {
|
|
177
213
|
throw new Error("workspace state schema is obsolete; remove the state root and initialize the current version");
|
|
178
214
|
}
|
|
215
|
+
assertWorkspaceStateEnvelope(state, { canonicalWorkspace, stateRoot, profileDir, statePath });
|
|
216
|
+
if (recoverySnapshot && !unlinkRegularFileIfIdentitySync(recoveryPath, recoverySnapshot.identity, "state recovery marker")) {
|
|
217
|
+
throw new Error("workspace state recovery marker changed before removal");
|
|
218
|
+
}
|
|
219
|
+
} else if (recoveryPending) {
|
|
220
|
+
throw recoveryRequiredError(recoveryMarker);
|
|
179
221
|
}
|
|
180
222
|
state.schemaVersion = STATE_SCHEMA_VERSION;
|
|
181
223
|
state.workspace = {
|
|
@@ -185,17 +227,78 @@ export function loadState(workspace, options = {}) {
|
|
|
185
227
|
};
|
|
186
228
|
state.paths = { stateRoot, profileDir, statePath };
|
|
187
229
|
state.worker ||= {};
|
|
230
|
+
delete state.worker.accountAdminSecret;
|
|
188
231
|
state.policy ||= {};
|
|
189
232
|
state.resources ||= {};
|
|
190
233
|
return state;
|
|
191
234
|
}
|
|
192
235
|
|
|
236
|
+
function removeObsoleteOperationLeaseState(profileDir) {
|
|
237
|
+
const file = path.join(profileDir, "operation-leases.json");
|
|
238
|
+
const info = inspectPathIfPresentSync(file, "obsolete operation lease state", {
|
|
239
|
+
lstatSync: (target) => lstatSync(target, { bigint: true }),
|
|
240
|
+
});
|
|
241
|
+
if (!info) return false;
|
|
242
|
+
if (info.isSymbolicLink() || !info.isFile() || info.nlink !== 1n) {
|
|
243
|
+
throw new Error("obsolete operation lease state must be a single-link regular file before migration cleanup");
|
|
244
|
+
}
|
|
245
|
+
const identity = filesystemIdentity(info, "obsolete operation lease state");
|
|
246
|
+
if (!unlinkRegularFileIfIdentitySync(file, identity, "obsolete operation lease state")) {
|
|
247
|
+
throw new Error("obsolete operation lease state changed before migration cleanup");
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
193
252
|
export function saveState(state) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
ensureOwnerOnlyDir(
|
|
198
|
-
atomicWriteJson(statePath, { ...state });
|
|
253
|
+
const envelope = assertWorkspaceStateEnvelope(state);
|
|
254
|
+
assertNoForeignMaintenance(envelope.stateRoot);
|
|
255
|
+
assertValidStateMarker(path.join(envelope.stateRoot, STATE_MARKER));
|
|
256
|
+
ensureOwnerOnlyDir(envelope.profileDir);
|
|
257
|
+
atomicWriteJson(envelope.statePath, { ...state });
|
|
258
|
+
const recoveryPath = recoveryMarkerPath(envelope.statePath);
|
|
259
|
+
if (inspectPathIfPresentSync(recoveryPath, "state recovery marker")) {
|
|
260
|
+
const recoverySnapshot = readRecoveryMarker(recoveryPath, envelope.statePath);
|
|
261
|
+
if (!unlinkRegularFileIfIdentitySync(recoveryPath, recoverySnapshot.identity, "state recovery marker")) {
|
|
262
|
+
throw new Error("workspace state recovery marker changed before removal");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function assertWorkspaceStateEnvelope(state, expected = {}) {
|
|
268
|
+
if (!isPlainRecord(state) || state.schemaVersion !== STATE_SCHEMA_VERSION) {
|
|
269
|
+
throw new Error("workspace state envelope is incomplete or uses the wrong schema");
|
|
270
|
+
}
|
|
271
|
+
if (!isPlainRecord(state.workspace) || typeof state.workspace.path !== "string" || typeof state.workspace.hash !== "string") {
|
|
272
|
+
throw new Error("workspace state envelope is missing workspace identity");
|
|
273
|
+
}
|
|
274
|
+
if (!isPlainRecord(state.paths) || !isPlainRecord(state.worker) || !isPlainRecord(state.policy) || !isPlainRecord(state.resources)) {
|
|
275
|
+
throw new Error("workspace state envelope is missing required object fields");
|
|
276
|
+
}
|
|
277
|
+
const canonicalWorkspace = resolveWorkspace(expected.canonicalWorkspace || state.workspace.path);
|
|
278
|
+
const expectedHash = workspaceHash(canonicalWorkspace);
|
|
279
|
+
if (!sameWorkspaceIdentity(state.workspace.path, canonicalWorkspace) || state.workspace.hash !== expectedHash) {
|
|
280
|
+
throw new Error("workspace state envelope does not match the selected workspace");
|
|
281
|
+
}
|
|
282
|
+
if (typeof state.paths.stateRoot !== "string" || !state.paths.stateRoot
|
|
283
|
+
|| typeof state.paths.profileDir !== "string" || !state.paths.profileDir
|
|
284
|
+
|| typeof state.paths.statePath !== "string" || !state.paths.statePath) {
|
|
285
|
+
throw new Error("workspace state envelope is missing canonical state paths");
|
|
286
|
+
}
|
|
287
|
+
const stateRoot = canonicalizePotentialPath(expected.stateRoot || state.paths.stateRoot);
|
|
288
|
+
const profileDir = canonicalizePotentialPath(expected.profileDir || state.paths.profileDir);
|
|
289
|
+
const statePath = canonicalizePotentialPath(expected.statePath || state.paths.statePath);
|
|
290
|
+
const expectedProfileDir = canonicalizePotentialPath(profileDirForWorkspace(canonicalWorkspace, stateRoot));
|
|
291
|
+
const expectedStatePath = canonicalizePotentialPath(path.join(expectedProfileDir, "state.json"));
|
|
292
|
+
if (!samePotentialPathIdentity(profileDir, expectedProfileDir) || !samePotentialPathIdentity(statePath, expectedStatePath)) {
|
|
293
|
+
throw new Error("workspace state envelope contains inconsistent state paths");
|
|
294
|
+
}
|
|
295
|
+
if (!samePotentialPathIdentity(state.paths.stateRoot, stateRoot)
|
|
296
|
+
|| !samePotentialPathIdentity(state.paths.profileDir, profileDir)
|
|
297
|
+
|| !samePotentialPathIdentity(state.paths.statePath, statePath)) {
|
|
298
|
+
throw new Error("workspace state envelope does not match the active state location");
|
|
299
|
+
}
|
|
300
|
+
assertStateRootSeparatedFromWorkspace(stateRoot, canonicalWorkspace);
|
|
301
|
+
return { canonicalWorkspace, stateRoot, profileDir, statePath };
|
|
199
302
|
}
|
|
200
303
|
|
|
201
304
|
export function daemonLockPathForState(state) {
|
|
@@ -212,9 +315,59 @@ function lockPathForState(state, name) {
|
|
|
212
315
|
return path.join(profileDir, name);
|
|
213
316
|
}
|
|
214
317
|
|
|
318
|
+
export function machineServiceControlRoot(options = {}) {
|
|
319
|
+
const home = path.resolve(String(options.home || os.homedir()));
|
|
320
|
+
if (options.controlRoot) return path.resolve(expandHomeFrom(String(options.controlRoot), home));
|
|
321
|
+
if (String(options.platform || process.platform) === "win32") {
|
|
322
|
+
const environment = options.environment && typeof options.environment === "object" ? options.environment : process.env;
|
|
323
|
+
const base = environment.APPDATA ? expandHomeFrom(String(environment.APPDATA), home) : path.join(home, "AppData", "Roaming");
|
|
324
|
+
return path.join(base, `${appName}-control`);
|
|
325
|
+
}
|
|
326
|
+
return path.join(home, ".local", "state", `${appName}-control`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function machineServiceLockPath(options = {}) {
|
|
330
|
+
return path.join(machineServiceControlRoot(options), "service-operation.lock");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function acquireMachineServiceLock(metadata = {}, options = {}) {
|
|
334
|
+
const root = machineServiceControlRoot(options);
|
|
335
|
+
ensureOwnerOnlyDir(root);
|
|
336
|
+
assertNoForeignMaintenance(root);
|
|
337
|
+
const operation = typeof metadata.operation === "string" && /^[a-z][a-z0-9._-]{0,63}$/.test(metadata.operation)
|
|
338
|
+
? metadata.operation
|
|
339
|
+
: "service-operation";
|
|
340
|
+
return acquireProcessLock(machineServiceLockPath({ controlRoot: root }), {
|
|
341
|
+
workspace: { path: "" },
|
|
342
|
+
paths: { profileDir: root, stateRoot: root },
|
|
343
|
+
}, "machine-service", { operation }, { maxAgeMs: MACHINE_SERVICE_LOCK_MAX_AGE_MS });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function acquireMachineServiceLockWithWait(options = {}) {
|
|
347
|
+
const timeoutMs = boundedPositiveInteger(options.timeoutMs, 30_000);
|
|
348
|
+
const pollMs = boundedPositiveInteger(options.pollMs, 100);
|
|
349
|
+
const logger = options.logger || { info() {} };
|
|
350
|
+
const metadata = { operation: options.operation };
|
|
351
|
+
let lock = acquireMachineServiceLock(metadata, options);
|
|
352
|
+
if (lock.acquired) return lock;
|
|
353
|
+
logger.info?.("waiting for another process to finish the current machine-service operation");
|
|
354
|
+
const deadline = createMonotonicDeadline(timeoutMs);
|
|
355
|
+
while (!deadline.expired()) {
|
|
356
|
+
await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))); });
|
|
357
|
+
lock = acquireMachineServiceLock(metadata, options);
|
|
358
|
+
if (lock.acquired) {
|
|
359
|
+
logger.info?.("the previous machine-service operation finished; continuing");
|
|
360
|
+
return lock;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
|
|
364
|
+
const operation = typeof lock.owner?.operation === "string" ? ` (${lock.owner.operation})` : "";
|
|
365
|
+
throw new Error(`another machine-service operation did not finish within ${Math.ceil(timeoutMs / 1000)} seconds (${pid}${operation}); inspect the process before retrying`);
|
|
366
|
+
}
|
|
367
|
+
|
|
215
368
|
export function acquireMaintenanceLock(stateRoot, metadata = {}) {
|
|
216
369
|
const root = path.resolve(expandHome(stateRoot));
|
|
217
|
-
if (!
|
|
370
|
+
if (!inspectPathIfPresentSync(root, "state root")) throw new Error("cannot acquire maintenance lock for a missing state root");
|
|
218
371
|
const operation = typeof metadata.operation === "string" && /^[a-z][a-z0-9._-]{0,63}$/.test(metadata.operation)
|
|
219
372
|
? metadata.operation
|
|
220
373
|
: "maintenance";
|
|
@@ -231,7 +384,6 @@ export function assertStateMaintenanceAvailable(stateRoot) {
|
|
|
231
384
|
function assertNoForeignMaintenance(stateRoot) {
|
|
232
385
|
if (!stateRoot) return;
|
|
233
386
|
const file = path.join(path.resolve(expandHome(stateRoot)), "maintenance.lock");
|
|
234
|
-
if (!existsSync(file)) return;
|
|
235
387
|
const snapshot = readProcessLockSnapshot(file);
|
|
236
388
|
if (!snapshot) return;
|
|
237
389
|
if (!snapshot.owner) {
|
|
@@ -256,7 +408,7 @@ function assertNoForeignMaintenance(stateRoot) {
|
|
|
256
408
|
|
|
257
409
|
export function acquireDaemonLock(state, metadata = {}) {
|
|
258
410
|
assertNoForeignMaintenance(state?.paths?.stateRoot);
|
|
259
|
-
const details = {};
|
|
411
|
+
const details = { startupReady: false, startupReadyAt: null, nodeVersion: process.versions.node };
|
|
260
412
|
if (metadata?.mode === "foreground" || metadata?.mode === "service") details.mode = metadata.mode;
|
|
261
413
|
if (typeof metadata?.version === "string" && /^[0-9A-Za-z.+_-]{1,64}$/.test(metadata.version)) details.version = metadata.version;
|
|
262
414
|
return acquireProcessLock(daemonLockPathForState(state), state, "daemon", details, { maxAgeMs: Number.POSITIVE_INFINITY });
|
|
@@ -276,8 +428,7 @@ export async function acquireStartupLockWithWait(state, options = {}) {
|
|
|
276
428
|
const metadata = { operation: options.operation };
|
|
277
429
|
let lock = acquireStartupLock(state, metadata);
|
|
278
430
|
if (lock.acquired) return lock;
|
|
279
|
-
|
|
280
|
-
logger.info?.(`waiting for ${ownerPid} to finish the current startup/state operation`);
|
|
431
|
+
logger.info?.("waiting for another process to finish the current startup/state operation");
|
|
281
432
|
const deadline = createMonotonicDeadline(timeoutMs);
|
|
282
433
|
while (!deadline.expired()) {
|
|
283
434
|
await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))); });
|
|
@@ -311,12 +462,17 @@ function acquireProcessLock(lockPath, state, purpose, details = {}, options = {}
|
|
|
311
462
|
createExclusiveFileSync(lockPath, `${JSON.stringify(payload, null, 2)}
|
|
312
463
|
`, { mode: 0o600 });
|
|
313
464
|
ownerOnlyFile(lockPath);
|
|
314
|
-
|
|
465
|
+
const lock = {
|
|
315
466
|
acquired: true,
|
|
316
467
|
path: lockPath,
|
|
317
468
|
owner: payload,
|
|
469
|
+
update(patch) {
|
|
470
|
+
lock.owner = updateProcessLock(lockPath, token, patch);
|
|
471
|
+
return lock.owner;
|
|
472
|
+
},
|
|
318
473
|
release() { releaseProcessLock(lockPath, token); },
|
|
319
474
|
};
|
|
475
|
+
return lock;
|
|
320
476
|
} catch (error) {
|
|
321
477
|
if (error?.code !== "EEXIST") throw error;
|
|
322
478
|
const snapshot = readProcessLockSnapshot(lockPath);
|
|
@@ -345,26 +501,31 @@ function acquireProcessLock(lockPath, state, purpose, details = {}, options = {}
|
|
|
345
501
|
}
|
|
346
502
|
|
|
347
503
|
function readProcessLockSnapshot(lockPath) {
|
|
348
|
-
let
|
|
349
|
-
try {
|
|
350
|
-
|
|
504
|
+
let opened;
|
|
505
|
+
try {
|
|
506
|
+
opened = retryTransientMultipleLinksSync(() => readBoundedRegularFileWithInfoSync(lockPath, MAX_LOCK_BYTES, "process lock", {
|
|
507
|
+
verifyPathIdentity: true,
|
|
508
|
+
rejectMultipleLinks: true,
|
|
509
|
+
}));
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (error?.code === "ENOENT" || error?.cause?.code === "ENOENT") return null;
|
|
351
512
|
throw error;
|
|
352
513
|
}
|
|
353
|
-
|
|
354
|
-
|
|
514
|
+
const info = lockIdentity(opened.identityInfo, opened.identity);
|
|
515
|
+
let text;
|
|
516
|
+
try { text = new TextDecoder("utf-8", { fatal: true }).decode(opened.buffer); }
|
|
517
|
+
catch { return { owner: null, info }; }
|
|
355
518
|
let owner = null;
|
|
356
519
|
try {
|
|
357
|
-
const parsed = JSON.parse(
|
|
520
|
+
const parsed = JSON.parse(text);
|
|
358
521
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) owner = parsed;
|
|
359
|
-
} catch
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
return { owner, info: lockIdentity(info) };
|
|
522
|
+
} catch { /* Successfully read malformed JSON remains eligible for bounded stale recovery. */ }
|
|
523
|
+
return { owner, info };
|
|
363
524
|
}
|
|
364
525
|
|
|
365
526
|
function removeLockSnapshot(lockPath, snapshot) {
|
|
366
527
|
let current;
|
|
367
|
-
try { current = lstatSync(lockPath); } catch (error) { return error?.code === "ENOENT"; }
|
|
528
|
+
try { current = lstatSync(lockPath, { bigint: true }); } catch (error) { return error?.code === "ENOENT"; }
|
|
368
529
|
if (current.isSymbolicLink() || !current.isFile()) return false;
|
|
369
530
|
if (!sameLockIdentity(snapshot.info, lockIdentity(current))) return false;
|
|
370
531
|
if (snapshot.owner?.token) {
|
|
@@ -379,17 +540,48 @@ function removeLockSnapshot(lockPath, snapshot) {
|
|
|
379
540
|
}
|
|
380
541
|
}
|
|
381
542
|
|
|
382
|
-
function lockIdentity(info) {
|
|
543
|
+
function lockIdentity(info, identity = filesystemIdentity(info, "process lock")) {
|
|
383
544
|
return {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
mtimeMs:
|
|
545
|
+
...identity,
|
|
546
|
+
size: exactFilesystemInteger(info.size, "process lock size"),
|
|
547
|
+
nlink: exactFilesystemInteger(info.nlink, "process lock link count"),
|
|
548
|
+
mtimeMs: filesystemTimeMs(info.mtimeMs, "process lock modification time"),
|
|
388
549
|
};
|
|
389
550
|
}
|
|
390
551
|
|
|
391
552
|
function sameLockIdentity(left, right) {
|
|
392
|
-
return left
|
|
553
|
+
return sameFilesystemIdentity(left, right) && left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function updateProcessLock(lockPath, token, patch) {
|
|
557
|
+
if (!isPlainRecord(patch)) throw new TypeError("process lock update must be a plain record");
|
|
558
|
+
const allowed = new Set(["startupReady", "startupReadyAt"]);
|
|
559
|
+
for (const key of Object.keys(patch)) {
|
|
560
|
+
if (!allowed.has(key)) throw new Error(`process lock field is immutable: ${key}`);
|
|
561
|
+
}
|
|
562
|
+
if (patch.startupReady !== true) {
|
|
563
|
+
throw new TypeError("process lock startup readiness can only be published as true");
|
|
564
|
+
}
|
|
565
|
+
if (typeof patch.startupReadyAt !== "string" || !Number.isFinite(Date.parse(patch.startupReadyAt))) {
|
|
566
|
+
throw new TypeError("process lock startupReadyAt update must be an ISO timestamp");
|
|
567
|
+
}
|
|
568
|
+
const snapshot = readProcessLockSnapshot(lockPath);
|
|
569
|
+
if (!snapshot?.owner || snapshot.owner.token !== token || snapshot.owner.pid !== process.pid) {
|
|
570
|
+
throw new Error("process lock ownership changed before update");
|
|
571
|
+
}
|
|
572
|
+
if (snapshot.owner.purpose !== "daemon") {
|
|
573
|
+
throw new Error("only daemon locks can publish startup readiness");
|
|
574
|
+
}
|
|
575
|
+
if (snapshot.owner.startupReady === true || snapshot.owner.startupReadyAt !== null) {
|
|
576
|
+
throw new Error("process lock startup readiness was already published");
|
|
577
|
+
}
|
|
578
|
+
const updated = { ...snapshot.owner, startupReady: true, startupReadyAt: patch.startupReadyAt };
|
|
579
|
+
const content = `${JSON.stringify(updated, null, 2)}
|
|
580
|
+
`;
|
|
581
|
+
if (Buffer.byteLength(content) > MAX_LOCK_BYTES) throw new Error("process lock update exceeds the size limit");
|
|
582
|
+
replaceFileAtomicallySync(lockPath, content, { mode: 0o600 });
|
|
583
|
+
ownerOnlyFile(lockPath);
|
|
584
|
+
return updated;
|
|
393
585
|
}
|
|
394
586
|
|
|
395
587
|
function releaseProcessLock(lockPath, token) {
|
|
@@ -400,12 +592,7 @@ function releaseProcessLock(lockPath, token) {
|
|
|
400
592
|
}
|
|
401
593
|
|
|
402
594
|
export function readDaemonLockOwner(lockPath) {
|
|
403
|
-
|
|
404
|
-
const parsed = JSON.parse(readBoundedUtf8(lockPath, MAX_LOCK_BYTES, "lock file"));
|
|
405
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
406
|
-
} catch {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
595
|
+
return readProcessLockSnapshot(lockPath)?.owner || null;
|
|
409
596
|
}
|
|
410
597
|
|
|
411
598
|
function boundedPositiveInteger(value, fallback) {
|
|
@@ -413,17 +600,23 @@ function boundedPositiveInteger(value, fallback) {
|
|
|
413
600
|
return Number.isFinite(parsed) ? Math.max(1, Math.floor(parsed)) : fallback;
|
|
414
601
|
}
|
|
415
602
|
|
|
416
|
-
function readBoundedUtf8(filePath, maxBytes, label) {
|
|
417
|
-
|
|
603
|
+
function readBoundedUtf8(filePath, maxBytes, label, options = {}) {
|
|
604
|
+
return decodeUtf8(readBoundedRegularFileSync(filePath, maxBytes, label, options), label);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function decodeUtf8(buffer, label) {
|
|
418
608
|
try {
|
|
419
609
|
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
420
|
-
} catch {
|
|
421
|
-
throw new Error(`${label} is not valid UTF-8
|
|
610
|
+
} catch (cause) {
|
|
611
|
+
throw Object.assign(new Error(`${label} is not valid UTF-8`, { cause }), { code: "ERR_INVALID_UTF8" });
|
|
422
612
|
}
|
|
423
613
|
}
|
|
424
614
|
|
|
425
|
-
function readJsonObjectOrBackup(filePath) {
|
|
426
|
-
const
|
|
615
|
+
function readJsonObjectOrBackup(filePath, options = {}) {
|
|
616
|
+
const opened = readBoundedRegularFileWithInfoSync(filePath, MAX_STATE_JSON_BYTES, "state JSON", {
|
|
617
|
+
verifyPathIdentity: true, rejectMultipleLinks: true,
|
|
618
|
+
});
|
|
619
|
+
const text = decodeUtf8(opened.buffer, "state JSON");
|
|
427
620
|
let parsed;
|
|
428
621
|
try {
|
|
429
622
|
parsed = JSON.parse(text);
|
|
@@ -431,15 +624,52 @@ function readJsonObjectOrBackup(filePath) {
|
|
|
431
624
|
return parsed;
|
|
432
625
|
} catch {
|
|
433
626
|
const backupPath = `${filePath}.corrupt-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
434
|
-
|
|
627
|
+
preserveFileSnapshotSync(filePath, backupPath, opened.buffer, opened.identity, { label: "state JSON", mode: 0o600 });
|
|
435
628
|
ownerOnlyFile(backupPath);
|
|
436
629
|
pruneBackups(filePath, 3);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
630
|
+
if (options.allowEmptyRecovery === true) {
|
|
631
|
+
const recovered = {};
|
|
632
|
+
Object.defineProperty(recovered, CORRUPT_RECOVERY, { value: true });
|
|
633
|
+
return recovered;
|
|
634
|
+
}
|
|
635
|
+
const recoveryPath = options.recoveryPath || recoveryMarkerPath(filePath);
|
|
636
|
+
const marker = { schemaVersion: RECOVERY_MARKER_SCHEMA, backup: path.basename(backupPath), detectedAt: new Date().toISOString() };
|
|
637
|
+
replaceFileAtomicallySync(recoveryPath, `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
|
|
638
|
+
ownerOnlyFile(recoveryPath);
|
|
639
|
+
throw recoveryRequiredError(marker);
|
|
440
640
|
}
|
|
441
641
|
}
|
|
442
642
|
|
|
643
|
+
function recoveryMarkerPath(statePath) { return `${statePath}.recovery-required`; }
|
|
644
|
+
|
|
645
|
+
function readRecoveryMarker(markerPath, statePath) {
|
|
646
|
+
ownerOnlyFile(markerPath);
|
|
647
|
+
let opened;
|
|
648
|
+
let marker;
|
|
649
|
+
try {
|
|
650
|
+
opened = readBoundedRegularFileWithInfoSync(markerPath, MAX_MARKER_BYTES, "state recovery marker", {
|
|
651
|
+
verifyPathIdentity: true, rejectMultipleLinks: true,
|
|
652
|
+
});
|
|
653
|
+
marker = JSON.parse(decodeUtf8(opened.buffer, "state recovery marker"));
|
|
654
|
+
} catch { throw new Error("workspace state recovery marker is invalid; inspect the profile manually before continuing"); }
|
|
655
|
+
if (marker?.schemaVersion !== RECOVERY_MARKER_SCHEMA || typeof marker.backup !== "string"
|
|
656
|
+
|| !marker.backup.startsWith(`${path.basename(statePath)}.corrupt-`) || path.basename(marker.backup) !== marker.backup
|
|
657
|
+
|| !Number.isFinite(Date.parse(String(marker.detectedAt || "")))) {
|
|
658
|
+
throw new Error("workspace state recovery marker is invalid; inspect the profile manually before continuing");
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
marker: Object.freeze({ schemaVersion: marker.schemaVersion, backup: marker.backup, detectedAt: marker.detectedAt }),
|
|
662
|
+
identity: opened.identity,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function recoveryRequiredError(marker) {
|
|
667
|
+
const backup = marker?.backup || "the bounded corrupt backup";
|
|
668
|
+
const error = new Error(`workspace state recovery is required; invalid JSON was preserved as ${backup}. Restore a valid current-schema state.json or intentionally remove this profile before restarting`);
|
|
669
|
+
error.code = "state_recovery_required";
|
|
670
|
+
return error;
|
|
671
|
+
}
|
|
672
|
+
|
|
443
673
|
function ensureStateRoot(inputRoot) {
|
|
444
674
|
const root = path.resolve(expandHome(inputRoot));
|
|
445
675
|
if (existsSync(root)) {
|
|
@@ -473,6 +703,9 @@ function assertSafeStateRootForRemoval(root) {
|
|
|
473
703
|
path.resolve(packageRoot),
|
|
474
704
|
]);
|
|
475
705
|
if (forbidden.has(canonical)) throw new Error(`refusing to remove unsafe state root: ${canonical}`);
|
|
706
|
+
if (currentEntrypointInsideStateRoot(canonical)) {
|
|
707
|
+
throw new Error("refusing to remove a state root that contains the currently executing Machine Bridge CLI; run uninstall from an installation outside that state root");
|
|
708
|
+
}
|
|
476
709
|
if (looksLikeSourceTree(canonical) || stateRootMatchesRecordedWorkspace(canonical)) {
|
|
477
710
|
throw new Error(`refusing to remove state root that appears to be a workspace: ${canonical}`);
|
|
478
711
|
}
|
|
@@ -480,6 +713,7 @@ function assertSafeStateRootForRemoval(root) {
|
|
|
480
713
|
if (!hasOnlyStateEntries(entries)) {
|
|
481
714
|
throw new Error(`refusing to remove state root containing unrelated entries: ${canonical}`);
|
|
482
715
|
}
|
|
716
|
+
validateOwnedStateNamespaces(canonical);
|
|
483
717
|
const marker = path.join(canonical, STATE_MARKER);
|
|
484
718
|
if (existsSync(marker)) {
|
|
485
719
|
assertValidStateMarker(marker);
|
|
@@ -489,7 +723,7 @@ function assertSafeStateRootForRemoval(root) {
|
|
|
489
723
|
}
|
|
490
724
|
|
|
491
725
|
function assertValidStateMarker(marker) {
|
|
492
|
-
const content = readBoundedUtf8(marker, MAX_MARKER_BYTES, "state marker");
|
|
726
|
+
const content = readBoundedUtf8(marker, MAX_MARKER_BYTES, "state marker", { verifyPathIdentity: true, rejectMultipleLinks: true });
|
|
493
727
|
let value;
|
|
494
728
|
try { value = JSON.parse(content); } catch { throw new Error(`invalid state root marker: ${marker}`); }
|
|
495
729
|
if (value?.app !== appName || value?.schema !== STATE_MARKER_SCHEMA) {
|
|
@@ -498,10 +732,26 @@ function assertValidStateMarker(marker) {
|
|
|
498
732
|
}
|
|
499
733
|
|
|
500
734
|
function hasOnlyStateEntries(entries) {
|
|
501
|
-
const allowed = new Set([STATE_MARKER, "config.json", "browser-bridge.json", "maintenance.lock", "profiles", "logs", "service-environment.json", "service-launcher.cmd"]);
|
|
735
|
+
const allowed = new Set([STATE_MARKER, "config.json", "browser-bridge.json", "maintenance.lock", "profiles", "logs", "service-environment.json", "service-launcher.cmd", "toolchains", "release-channels", "release-tasks"]);
|
|
502
736
|
return entries.every((entry) => allowed.has(entry) || /^config\.json\.corrupt-\d+(?:-[a-f0-9]{8})?$/.test(entry));
|
|
503
737
|
}
|
|
504
738
|
|
|
739
|
+
function currentEntrypointInsideStateRoot(root) {
|
|
740
|
+
const entry = String(process.argv[1] || "").trim();
|
|
741
|
+
if (!entry) return false;
|
|
742
|
+
let canonicalRoot;
|
|
743
|
+
let resolved;
|
|
744
|
+
try {
|
|
745
|
+
canonicalRoot = canonicalizePotentialPath(root);
|
|
746
|
+
resolved = canonicalizePotentialPath(entry);
|
|
747
|
+
} catch {
|
|
748
|
+
canonicalRoot = path.resolve(root);
|
|
749
|
+
resolved = path.resolve(entry);
|
|
750
|
+
}
|
|
751
|
+
const relative = path.relative(canonicalRoot, resolved);
|
|
752
|
+
return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
753
|
+
}
|
|
754
|
+
|
|
505
755
|
function looksLikeSourceTree(root) {
|
|
506
756
|
return [".git", ".hg", ".svn", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"].some((name) => existsSync(path.join(root, name)));
|
|
507
757
|
}
|
|
@@ -511,13 +761,12 @@ function stateRootMatchesRecordedWorkspace(root) {
|
|
|
511
761
|
if (typeof config?.selectedWorkspace === "string" && sameWorkspaceIdentity(config.selectedWorkspace, root)) return true;
|
|
512
762
|
|
|
513
763
|
const profiles = path.join(root, "profiles");
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (error?.code === "ENOENT") return false;
|
|
519
|
-
throw new Error("could not inspect state-root profiles before removal", { cause: error });
|
|
764
|
+
const profilesInfo = inspectPathIfPresentSync(profiles, "state-root profiles");
|
|
765
|
+
if (!profilesInfo) return false;
|
|
766
|
+
if (profilesInfo.isSymbolicLink() || !profilesInfo.isDirectory()) {
|
|
767
|
+
throw new Error("state-root profiles must be a real directory before removal");
|
|
520
768
|
}
|
|
769
|
+
const entries = readdirSync(profiles, { withFileTypes: true });
|
|
521
770
|
for (const entry of entries) {
|
|
522
771
|
if (!entry.isDirectory()) continue;
|
|
523
772
|
const profileDir = path.join(profiles, entry.name);
|
|
@@ -532,7 +781,7 @@ function stateRootMatchesRecordedWorkspace(root) {
|
|
|
532
781
|
function readOptionalRemovalJson(file, label) {
|
|
533
782
|
let text;
|
|
534
783
|
try {
|
|
535
|
-
text = readBoundedUtf8(file, MAX_STATE_JSON_BYTES, label);
|
|
784
|
+
text = readBoundedUtf8(file, MAX_STATE_JSON_BYTES, label, { verifyPathIdentity: true, rejectMultipleLinks: true });
|
|
536
785
|
} catch (error) {
|
|
537
786
|
if (error?.code === "ENOENT" || error?.cause?.code === "ENOENT") return null;
|
|
538
787
|
throw new Error(`${label} could not be verified before state removal`, { cause: error });
|
|
@@ -569,7 +818,7 @@ function cleanupStaleAtomicTemps(dir, baseName = "") {
|
|
|
569
818
|
const file = path.join(dir, name);
|
|
570
819
|
try {
|
|
571
820
|
if (now - statSync(file).mtimeMs > 60 * 60 * 1000) unlinkSync(file);
|
|
572
|
-
} catch {}
|
|
821
|
+
} catch { /* Stale-temp cleanup is opportunistic and must not block the authoritative state write. */ }
|
|
573
822
|
}
|
|
574
823
|
}
|
|
575
824
|
|
|
@@ -586,16 +835,18 @@ function pruneBackups(filePath, keep) {
|
|
|
586
835
|
return;
|
|
587
836
|
}
|
|
588
837
|
for (const backup of backups.slice(keep)) {
|
|
589
|
-
try { unlinkSync(backup.path); }
|
|
838
|
+
try { unlinkSync(backup.path); }
|
|
839
|
+
catch { /* Backup pruning is bounded housekeeping; retained backups are safer than failing state recovery. */ }
|
|
590
840
|
}
|
|
591
841
|
}
|
|
592
842
|
|
|
593
843
|
export function ensureWorkerSecrets(state, options = {}) {
|
|
594
844
|
state.worker ||= {};
|
|
845
|
+
delete state.worker.accountAdminSecret;
|
|
595
846
|
const enrollingDeviceIdentity = !state.worker.deviceIdentity;
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
|
|
847
|
+
if (enrollingDeviceIdentity || (options.rotateSecrets && !options.deferDeviceRotation)) state.worker.deviceIdentity = createDeviceIdentity();
|
|
848
|
+
else validateDeviceRootIdentity(state.worker.deviceIdentity);
|
|
849
|
+
if (state.worker.pendingDeviceIdentity) validateDeviceRootIdentity(state.worker.pendingDeviceIdentity);
|
|
599
850
|
delete state.worker.daemonSecret;
|
|
600
851
|
if (!state.worker.oauthTokenVersion || enrollingDeviceIdentity || options.rotateSecrets) {
|
|
601
852
|
state.worker.oauthTokenVersion = randomToken("token_version");
|
|
@@ -622,26 +873,53 @@ export function ensureWorkerSecrets(state, options = {}) {
|
|
|
622
873
|
delete state.worker.updatedAt;
|
|
623
874
|
}
|
|
624
875
|
|
|
625
|
-
function
|
|
626
|
-
|
|
876
|
+
export function deploymentDeviceIdentity(state) {
|
|
877
|
+
const identity = state?.worker?.pendingDeviceIdentity || state?.worker?.deviceIdentity;
|
|
878
|
+
return validateDeviceRootIdentity(identity);
|
|
627
879
|
}
|
|
628
880
|
|
|
629
|
-
function
|
|
630
|
-
|
|
881
|
+
export function promotePendingDeviceIdentity(state) {
|
|
882
|
+
const pending = state?.worker?.pendingDeviceIdentity;
|
|
883
|
+
if (!pending) return false;
|
|
884
|
+
validateDeviceRootIdentity(pending);
|
|
885
|
+
const current = state.worker.deviceIdentity;
|
|
886
|
+
if (current) {
|
|
887
|
+
const previous = Array.isArray(state.worker.previousDeviceIdentities) ? state.worker.previousDeviceIdentities : [];
|
|
888
|
+
state.worker.previousDeviceIdentities = [...previous, publicDeviceRootRecord(current)].slice(-2);
|
|
889
|
+
}
|
|
890
|
+
state.worker.deviceIdentity = pending;
|
|
891
|
+
delete state.worker.pendingDeviceIdentity;
|
|
892
|
+
return true;
|
|
631
893
|
}
|
|
632
894
|
|
|
633
|
-
|
|
634
|
-
|
|
895
|
+
function publicDeviceRootRecord(identity) {
|
|
896
|
+
validateDeviceRootIdentity(identity);
|
|
897
|
+
return {
|
|
898
|
+
scheme: identity.scheme,
|
|
899
|
+
provider: identity.provider || "portable-jwk-v1",
|
|
900
|
+
brokerProtocol: identity.brokerProtocol || undefined,
|
|
901
|
+
brokerPath: identity.brokerPath || undefined,
|
|
902
|
+
brokerIdentifier: identity.brokerIdentifier || undefined,
|
|
903
|
+
brokerTeamIdentifier: identity.brokerTeamIdentifier || undefined,
|
|
904
|
+
keyTag: identity.keyTag || undefined,
|
|
905
|
+
publicJwk: identity.publicJwk,
|
|
906
|
+
keyId: identity.keyId,
|
|
907
|
+
createdAt: identity.createdAt,
|
|
908
|
+
retiredAt: new Date().toISOString(),
|
|
909
|
+
};
|
|
635
910
|
}
|
|
636
911
|
|
|
637
|
-
|
|
638
|
-
|
|
912
|
+
function defaultWorkerName(hash) {
|
|
913
|
+
return `mbm-${String(hash || "default").slice(0, 12)}`;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function randomToken(prefix) {
|
|
917
|
+
return `${prefix}_${randomBytes(32).toString("base64url")}`;
|
|
639
918
|
}
|
|
640
919
|
|
|
641
920
|
export function redactState(state) {
|
|
642
921
|
const clone = redactHomeInValue(JSON.parse(JSON.stringify(state)));
|
|
643
|
-
|
|
644
|
-
if (clone.worker?.deviceIdentity?.privateJwk?.d) clone.worker.deviceIdentity.privateJwk.d = "<redacted>";
|
|
922
|
+
redactDeviceIdentityState(clone.worker);
|
|
645
923
|
if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = "<redacted>";
|
|
646
924
|
if (clone.resources && typeof clone.resources === "object") {
|
|
647
925
|
for (const value of Object.values(clone.resources)) {
|
|
@@ -653,6 +931,22 @@ export function redactState(state) {
|
|
|
653
931
|
return clone;
|
|
654
932
|
}
|
|
655
933
|
|
|
934
|
+
|
|
935
|
+
function redactDeviceIdentityState(worker) {
|
|
936
|
+
if (!worker || typeof worker !== "object") return;
|
|
937
|
+
for (const identity of [
|
|
938
|
+
worker.deviceIdentity,
|
|
939
|
+
worker.pendingDeviceIdentity,
|
|
940
|
+
...(Array.isArray(worker.previousDeviceIdentities) ? worker.previousDeviceIdentities : []),
|
|
941
|
+
]) {
|
|
942
|
+
if (!identity || typeof identity !== "object") continue;
|
|
943
|
+
delete identity.privateJwk;
|
|
944
|
+
delete identity.publicJwk;
|
|
945
|
+
if (identity.brokerPath) identity.brokerPath = "<local-broker-path>";
|
|
946
|
+
if (identity.keyTag) identity.keyTag = "<local-key-tag>";
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
656
950
|
function redactHomeInValue(value) {
|
|
657
951
|
const home = os.homedir();
|
|
658
952
|
if (typeof value === "string") {
|
|
@@ -665,7 +959,3 @@ function redactHomeInValue(value) {
|
|
|
665
959
|
}
|
|
666
960
|
return value;
|
|
667
961
|
}
|
|
668
|
-
|
|
669
|
-
export function previewSecret(_value) {
|
|
670
|
-
return "<redacted>";
|
|
671
|
-
}
|