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
|
@@ -3,17 +3,39 @@ import { lstatSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
5
5
|
import { currentProcessStartTimeMs, inspectProcessInstance, processStartTimeMs } from "./process-identity.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { ownerOnlyFile, readBoundedRegularFileWithInfoSync, retryTransientMultipleLinksSync } from "./secure-file.mjs";
|
|
7
|
+
import { exactFilesystemInteger, filesystemIdentity, filesystemTimeMs, sameFilesystemIdentity } from "./filesystem-identity.mjs";
|
|
8
8
|
|
|
9
9
|
export function acquireRecoveryLock(dir) {
|
|
10
10
|
return acquirePidLock(join(dir, "recovery.lock"), { allowHandoff: true });
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export function acquireJobCapacityLock(jobRoot) {
|
|
14
|
+
return acquirePidLock(join(jobRoot, "capacity.lock"));
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
export function acquireJobTransitionLock(dir) {
|
|
14
18
|
return acquirePidLock(join(dir, "transition.lock"));
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
export function activeManagedJobLock(file) {
|
|
22
|
+
let snapshot;
|
|
23
|
+
try { snapshot = readPidLockSnapshot(file); } catch {
|
|
24
|
+
return { active: true, pid: null, reason: "invalid_or_unreadable_lock" };
|
|
25
|
+
}
|
|
26
|
+
if (!snapshot) return null;
|
|
27
|
+
const age = Date.now() - snapshot.info.mtimeMs;
|
|
28
|
+
if (!snapshot.owner || !Number.isInteger(snapshot.owner.pid) || snapshot.owner.pid <= 0
|
|
29
|
+
|| !Number.isFinite(Date.parse(String(snapshot.owner.startedAt || "")))) {
|
|
30
|
+
return age < 60_000 ? { active: true, pid: null, reason: "recent_malformed_lock" } : null;
|
|
31
|
+
}
|
|
32
|
+
const identity = inspectProcessInstance(snapshot.owner, { maxAgeMs: 5 * 60_000 });
|
|
33
|
+
if (identity.current || (identity.alive && !identity.reclaimable)) {
|
|
34
|
+
return { active: true, pid: snapshot.owner.pid, reason: identity.reason };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
function acquirePidLock(file, { allowHandoff = false } = {}) {
|
|
18
40
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
19
41
|
const owner = pidLockOwner(process.pid, currentProcessStartTimeMs());
|
|
@@ -54,17 +76,22 @@ function pidLockOwner(pid, startedAtMs) {
|
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
function readPidLockSnapshot(file) {
|
|
57
|
-
let
|
|
58
|
-
try { info = lstatSync(file); } catch (error) { if (error?.code === "ENOENT") return null; throw error; }
|
|
59
|
-
if (info.isSymbolicLink() || !info.isFile()) throw new Error("job lock must be a regular non-symbolic-link file");
|
|
60
|
-
let owner = null;
|
|
79
|
+
let opened;
|
|
61
80
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
opened = retryTransientMultipleLinksSync(() => readBoundedRegularFileWithInfoSync(file, 1024, "job lock", {
|
|
82
|
+
verifyPathIdentity: true,
|
|
83
|
+
rejectMultipleLinks: true,
|
|
84
|
+
}));
|
|
64
85
|
} catch (error) {
|
|
65
|
-
if (error?.code === "ENOENT") return null;
|
|
86
|
+
if (error?.code === "ENOENT" || error?.cause?.code === "ENOENT") return null;
|
|
87
|
+
throw error;
|
|
66
88
|
}
|
|
67
|
-
|
|
89
|
+
let owner = null;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(opened.buffer.toString("utf8").trim());
|
|
92
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) owner = parsed;
|
|
93
|
+
} catch { /* Successfully read malformed JSON may be reclaimed only after the stale grace. */ }
|
|
94
|
+
return { owner, info: pidLockIdentity(opened.identityInfo, opened.identity) };
|
|
68
95
|
}
|
|
69
96
|
|
|
70
97
|
function removePidLockOwnedBy(file, token) {
|
|
@@ -75,7 +102,7 @@ function removePidLockOwnedBy(file, token) {
|
|
|
75
102
|
|
|
76
103
|
function removePidLockSnapshot(file, snapshot) {
|
|
77
104
|
let current;
|
|
78
|
-
try { current = lstatSync(file); } catch (error) { return error?.code === "ENOENT"; }
|
|
105
|
+
try { current = lstatSync(file, { bigint: true }); } catch (error) { return error?.code === "ENOENT"; }
|
|
79
106
|
if (current.isSymbolicLink() || !current.isFile()) return false;
|
|
80
107
|
if (!samePidLockIdentity(snapshot.info, pidLockIdentity(current))) return false;
|
|
81
108
|
if (snapshot.owner?.token) {
|
|
@@ -85,12 +112,17 @@ function removePidLockSnapshot(file, snapshot) {
|
|
|
85
112
|
try { rmSync(file); return true; } catch (error) { return error?.code === "ENOENT"; }
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
function pidLockIdentity(info) {
|
|
89
|
-
return {
|
|
115
|
+
function pidLockIdentity(info, identity = filesystemIdentity(info, "managed-job lock")) {
|
|
116
|
+
return {
|
|
117
|
+
...identity,
|
|
118
|
+
size: exactFilesystemInteger(info.size, "managed-job lock size"),
|
|
119
|
+
nlink: exactFilesystemInteger(info.nlink, "managed-job lock link count"),
|
|
120
|
+
mtimeMs: filesystemTimeMs(info.mtimeMs, "managed-job lock modification time"),
|
|
121
|
+
};
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
function samePidLockIdentity(left, right) {
|
|
93
|
-
return left
|
|
125
|
+
return sameFilesystemIdentity(left, right) && left.size === right.size && left.nlink === right.nlink && left.mtimeMs === right.mtimeMs;
|
|
94
126
|
}
|
|
95
127
|
|
|
96
128
|
function replacePrivateTextFile(file, content) {
|
|
@@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstatSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { readBoundedRegularFileWithInfoSync } from "./secure-file.mjs";
|
|
5
|
-
import { clampInteger } from "./numbers.mjs";
|
|
6
5
|
|
|
7
6
|
const RESOURCE_NAME = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
8
7
|
const RESOURCE_TOKEN = /\{\{resource:([a-z][a-z0-9._-]{0,63})\}\}/g;
|
|
@@ -17,10 +16,10 @@ export function validateResourceName(value) {
|
|
|
17
16
|
return name;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export function inspectResourceFile(inputPath, { allowInsecurePermissions = false, includeHash = false } = {}) {
|
|
19
|
+
export function inspectResourceFile(inputPath, { allowInsecurePermissions = false, includeHash = false, includeContent = false } = {}) {
|
|
21
20
|
const path = resolve(String(inputPath || ""));
|
|
22
21
|
const canonical = realpathFile(path);
|
|
23
|
-
const { buffer: content, info } = readBoundedRegularFileWithInfoSync(canonical, MAX_RESOURCE_BYTES);
|
|
22
|
+
const { buffer: content, info } = readBoundedRegularFileWithInfoSync(canonical, MAX_RESOURCE_BYTES, "resource file", { verifyPathIdentity: true });
|
|
24
23
|
if (process.platform !== "win32" && !allowInsecurePermissions && (info.mode & 0o077) !== 0) {
|
|
25
24
|
throw new Error("resource file is readable by group or others; restrict permissions or use --allow-insecure-permissions");
|
|
26
25
|
}
|
|
@@ -33,6 +32,7 @@ export function inspectResourceFile(inputPath, { allowInsecurePermissions = fals
|
|
|
33
32
|
updatedAt: new Date().toISOString(),
|
|
34
33
|
allowInsecurePermissions: allowInsecurePermissions === true,
|
|
35
34
|
...(includeHash ? { sha256: createHash("sha256").update(content).digest("hex") } : {}),
|
|
35
|
+
...(includeContent ? { content } : {}),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -53,10 +53,10 @@ export function validatePlan(args, context) {
|
|
|
53
53
|
if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error("job arguments must be an object");
|
|
54
54
|
const allowed = new Set(["name", "steps", "finally_steps", "temporary_files"]);
|
|
55
55
|
for (const key of Object.keys(args)) if (!allowed.has(key)) throw new Error(`job contains unknown field: ${key}`);
|
|
56
|
-
const name =
|
|
56
|
+
const name = args.name === undefined ? "managed job" : boundedString(args.name, 128, "name").trim() || "managed job";
|
|
57
57
|
const steps = validateSteps(args.steps, "steps", context);
|
|
58
|
-
const finallySteps = validateSteps(args.finally_steps
|
|
59
|
-
const temporaryFiles = validateTemporaryFiles(args.temporary_files
|
|
58
|
+
const finallySteps = validateSteps(args.finally_steps === undefined ? [] : args.finally_steps, "finally_steps", context, true);
|
|
59
|
+
const temporaryFiles = validateTemporaryFiles(args.temporary_files === undefined ? [] : args.temporary_files);
|
|
60
60
|
if (!steps.length) throw new Error("steps must contain at least one step");
|
|
61
61
|
return {
|
|
62
62
|
version: 1,
|
|
@@ -83,7 +83,7 @@ function validateTemporaryFiles(value) {
|
|
|
83
83
|
const content = boundedString(item.content, 256 * 1024, `temporary_files[${index}].content`);
|
|
84
84
|
totalBytes += Buffer.byteLength(content);
|
|
85
85
|
if (totalBytes > MAX_TEMPORARY_FILE_BYTES) throw new Error(`temporary file contents exceed ${MAX_TEMPORARY_FILE_BYTES} bytes`);
|
|
86
|
-
return { name, content, executable: item.executable
|
|
86
|
+
return { name, content, executable: optionalBoolean(item.executable, `temporary_files[${index}].executable`) };
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -113,19 +113,37 @@ function validateSteps(value, label, context, allowEmpty = false) {
|
|
|
113
113
|
env_resources: envResources,
|
|
114
114
|
stdin,
|
|
115
115
|
stdin_resource: stdinResource,
|
|
116
|
-
timeout_seconds:
|
|
117
|
-
allow_failure: input.allow_failure
|
|
118
|
-
capture_output: input.capture_output
|
|
116
|
+
timeout_seconds: optionalInteger(input.timeout_seconds, 600, 1, 3600, `${label}[${index}].timeout_seconds`),
|
|
117
|
+
allow_failure: optionalBoolean(input.allow_failure, `${label}[${index}].allow_failure`),
|
|
118
|
+
capture_output: validateCaptureOutput(input.capture_output, `${label}[${index}].capture_output`),
|
|
119
119
|
};
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function optionalBoolean(value, label) {
|
|
124
|
+
if (value === undefined) return false;
|
|
125
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function optionalInteger(value, defaultValue, min, max, label) {
|
|
130
|
+
if (value === undefined) return defaultValue;
|
|
131
|
+
if (!Number.isInteger(value) || value < min || value > max) throw new Error(`${label} must be an integer between ${min} and ${max}`);
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateCaptureOutput(value, label) {
|
|
136
|
+
if (value === undefined || value === "redacted") return "redacted";
|
|
137
|
+
if (value === "discard") return "discard";
|
|
138
|
+
throw new Error(`${label} must be redacted or discard`);
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
function validateEnv(value, label) {
|
|
124
142
|
if (value === undefined) return {};
|
|
125
143
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
126
144
|
const entries = Object.entries(value);
|
|
127
145
|
if (entries.length > 64) throw new Error(`${label} has too many entries`);
|
|
128
|
-
const out =
|
|
146
|
+
const out = Object.create(null);
|
|
129
147
|
for (const [key, raw] of entries) {
|
|
130
148
|
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key)) throw new Error(`${label} contains invalid variable name: ${key}`);
|
|
131
149
|
out[key] = boundedString(raw, 16 * 1024, `${label}.${key}`);
|
|
@@ -138,7 +156,7 @@ function validateEnvResources(value, label) {
|
|
|
138
156
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
139
157
|
const entries = Object.entries(value);
|
|
140
158
|
if (entries.length > 32) throw new Error(`${label} has too many entries`);
|
|
141
|
-
const out =
|
|
159
|
+
const out = Object.create(null);
|
|
142
160
|
for (const [key, raw] of entries) {
|
|
143
161
|
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key)) throw new Error(`${label} contains invalid variable name: ${key}`);
|
|
144
162
|
out[key] = validateResourceName(raw);
|
|
@@ -175,8 +193,14 @@ function referencedResources(steps, registry) {
|
|
|
175
193
|
|
|
176
194
|
export function normalizeResourceRegistry(resources) {
|
|
177
195
|
const out = Object.create(null);
|
|
178
|
-
if (
|
|
179
|
-
|
|
196
|
+
if (resources === undefined || resources === null) return out;
|
|
197
|
+
if (typeof resources !== "object" || Array.isArray(resources)) throw new Error("local resource registry must be an object");
|
|
198
|
+
let count = 0;
|
|
199
|
+
for (const rawName in resources) {
|
|
200
|
+
if (!Object.hasOwn(resources, rawName)) continue;
|
|
201
|
+
count += 1;
|
|
202
|
+
if (count > MAX_RESOURCES) throw new Error(`local resource registry limit exceeded (${MAX_RESOURCES})`);
|
|
203
|
+
const rawValue = resources[rawName];
|
|
180
204
|
const name = validateResourceName(rawName);
|
|
181
205
|
if (!rawValue || rawValue.kind !== "file" || typeof rawValue.path !== "string") continue;
|
|
182
206
|
out[name] = {
|
|
@@ -31,23 +31,27 @@ export function reviewablePlan(plan) {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Project owner-only managed-job timing out of delegated result reads.
|
|
36
|
+
* @param {Record<string, unknown> | null | undefined} result
|
|
37
|
+
* @param {{includeResourceAdmissionTiming?: boolean}} [options]
|
|
38
|
+
*/
|
|
39
|
+
export function projectManagedJobResult(result, { includeResourceAdmissionTiming = false } = {}) {
|
|
40
|
+
if (!result || includeResourceAdmissionTiming) return result;
|
|
41
|
+
const projectSteps = (/** @type {unknown} */ steps) => Array.isArray(steps) ? steps.map((/** @type {unknown} */ step) => {
|
|
42
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) return step;
|
|
43
|
+
const { resource_admission_ms: _resourceAdmissionMs, ...projected } = /** @type {Record<string, unknown>} */ (step);
|
|
44
|
+
return projected;
|
|
45
|
+
}) : steps;
|
|
46
|
+
return { ...result, steps: projectSteps(result.steps), finally_steps: projectSteps(result.finally_steps) };
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
/**
|
|
35
50
|
* Return the stable public status shape without runner identity or internal paths.
|
|
36
|
-
* @param {{
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* created_at?: unknown,
|
|
41
|
-
* started_at?: unknown,
|
|
42
|
-
* finished_at?: unknown,
|
|
43
|
-
* current_phase?: unknown,
|
|
44
|
-
* current_step?: unknown,
|
|
45
|
-
* approval?: unknown,
|
|
46
|
-
* plan_sha256?: unknown,
|
|
47
|
-
* cleanup_guarantee?: unknown,
|
|
48
|
-
* error_class?: unknown,
|
|
49
|
-
* recovery_attempts?: unknown
|
|
50
|
-
* }} status
|
|
51
|
+
* @param {{job_id?: unknown, name?: unknown, status?: unknown, created_at?: unknown, started_at?: unknown,
|
|
52
|
+
* finished_at?: unknown, current_phase?: unknown, current_step?: unknown, approval?: unknown, plan_sha256?: unknown,
|
|
53
|
+
* cleanup_guarantee?: unknown, error_class?: unknown, recovery_attempts?: unknown, result_persisted?: unknown,
|
|
54
|
+
* terminal_record_error_class?: unknown, artifact_cleanup_pending?: unknown, artifact_cleanup_error_class?: unknown}} status
|
|
51
55
|
*/
|
|
52
56
|
export function publicStatus(status) {
|
|
53
57
|
return {
|
|
@@ -64,5 +68,9 @@ export function publicStatus(status) {
|
|
|
64
68
|
cleanup_guarantee: status.cleanup_guarantee ?? "best-effort-finally-and-recovery",
|
|
65
69
|
error_class: status.error_class ?? null,
|
|
66
70
|
recovery_attempts: Number(status.recovery_attempts || 0),
|
|
71
|
+
result_persisted: typeof status.result_persisted === "boolean" ? status.result_persisted : null,
|
|
72
|
+
terminal_record_error_class: status.terminal_record_error_class ?? null,
|
|
73
|
+
artifact_cleanup_pending: status.artifact_cleanup_pending === true,
|
|
74
|
+
artifact_cleanup_error_class: status.artifact_cleanup_error_class ?? null,
|
|
67
75
|
};
|
|
68
76
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { MANAGED_JOB_ID } from "./managed-job-directory.mjs";
|
|
4
|
+
import { inspectManagedJobDirectoryGeneration, pruneRetiredManagedJobDirectories, removeManagedJobDirectoryIfCurrent } from "./managed-job-directory-generation.mjs";
|
|
5
|
+
import { managedJobCapacitySnapshot, MAX_JOBS } from "./managed-job-capacity.mjs";
|
|
6
|
+
import { runnerProcessIsCurrent } from "./managed-job-runner.mjs";
|
|
7
|
+
import { acquireJobTransitionLock } from "./managed-job-lock.mjs";
|
|
8
|
+
import { atomicWriteJson, readJson, resourceErrorClass, safeReadDir } from "./managed-job-storage.mjs";
|
|
9
|
+
import { ACTIVE_JOB_STATES, isTerminalManagedJobStatus, persistManagedJobTerminal } from "./managed-job-terminal.mjs";
|
|
10
|
+
import { scrubTerminalJobArtifacts } from "./managed-job-terminal-maintenance.mjs";
|
|
11
|
+
const JOB_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
12
|
+
const STAGED_PLAN_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
export const PLAN_RETAINING_STATES = new Set(["staged", ...ACTIVE_JOB_STATES]);
|
|
14
|
+
export function pruneManagedJobs({ jobRoot, logger = console, reserveSlots = 0 }) {
|
|
15
|
+
const reserved = Math.max(0, Math.min(MAX_JOBS, Math.floor(Number(reserveSlots) || 0)));
|
|
16
|
+
const targetMaximum = MAX_JOBS - reserved;
|
|
17
|
+
pruneRetiredManagedJobDirectories(jobRoot, logger);
|
|
18
|
+
const entries = [];
|
|
19
|
+
for (const entry of safeReadDir(jobRoot)) {
|
|
20
|
+
if (!MANAGED_JOB_ID.test(entry.name)) continue;
|
|
21
|
+
if (!entry.isDirectory()) { logger.warn?.("managed job pruning retained wrong-type job state", { error_class: "integrity_error" }); continue; }
|
|
22
|
+
const dir = join(jobRoot, entry.name);
|
|
23
|
+
let status; let mtime;
|
|
24
|
+
let generation;
|
|
25
|
+
try {
|
|
26
|
+
status = readJson(join(dir, "status.json"), 256 * 1024, "job status");
|
|
27
|
+
const observed = inspectManagedJobDirectoryGeneration(dir);
|
|
28
|
+
mtime = observed.mtimeMs;
|
|
29
|
+
generation = observed.identity;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
logger.warn?.("managed job pruning skipped unreadable state; retaining it for inspection", { error_class: resourceErrorClass(error) });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!status) {
|
|
35
|
+
const fallbackOwner = { startedAt: new Date(mtime).toISOString() };
|
|
36
|
+
let runnerAlive;
|
|
37
|
+
try { runnerAlive = runnerProcessIsCurrent(fallbackOwner, dir, { ownerOnly: true }); }
|
|
38
|
+
catch (error) {
|
|
39
|
+
logger.warn?.("managed job pruning skipped unreadable runner ownership; retaining it for inspection", { error_class: resourceErrorClass(error) });
|
|
40
|
+
entries.push({ dir, status, mtime });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!runnerAlive && Date.now() - mtime > 60_000) {
|
|
44
|
+
removeManagedJobDirectoryIfCurrent(dir, generation);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (status && status.job_id !== entry.name) {
|
|
49
|
+
logger.warn?.("managed job pruning skipped mismatched directory identity; retaining it for inspection", { error_class: "integrity_error" });
|
|
50
|
+
entries.push({ dir, status: null, mtime });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (status?.status === "staged" && stagedPlanExpired(status, mtime)) {
|
|
54
|
+
const transition = acquireJobTransitionLock(dir);
|
|
55
|
+
if (!transition) {
|
|
56
|
+
entries.push({ dir, status, mtime });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
try { status = readJson(join(dir, "status.json"), 256 * 1024, "job status"); }
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.warn?.("managed job pruning skipped unreadable staged state; retaining it for inspection", { error_class: resourceErrorClass(error) });
|
|
63
|
+
entries.push({ dir, status, mtime });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (status?.status === "staged" && stagedPlanExpired(status, mtime)) status = expireStagedJob(dir, status);
|
|
67
|
+
} finally {
|
|
68
|
+
transition.release();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (status && !PLAN_RETAINING_STATES.has(status.status) && !isTerminalManagedJobStatus(status.status)) {
|
|
72
|
+
logger.warn?.("managed job pruning skipped invalid status; retaining it for inspection", { error_class: "integrity_error" }); entries.push({ dir, status, mtime }); continue;
|
|
73
|
+
}
|
|
74
|
+
if (status && isTerminalManagedJobStatus(status.status)) {
|
|
75
|
+
try {
|
|
76
|
+
scrubTerminalJobArtifacts(dir, status);
|
|
77
|
+
generation = inspectManagedJobDirectoryGeneration(dir).identity;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.warn?.("managed job pruning retained inconsistent terminal state", { error_class: error?.code === "integrity_error" ? "integrity_error" : resourceErrorClass(error) });
|
|
80
|
+
entries.push({ dir, status: null, mtime }); continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
entries.push({ dir, status, mtime, generation });
|
|
84
|
+
}
|
|
85
|
+
const finished = entries
|
|
86
|
+
.filter(({ status }) => status && isTerminalManagedJobStatus(status.status))
|
|
87
|
+
.sort((a, b) => terminalRetentionTime(b.status, b.mtime) - terminalRetentionTime(a.status, a.mtime));
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
for (const item of finished) {
|
|
90
|
+
if (now - terminalRetentionTime(item.status, item.mtime) > JOB_RETENTION_MS) {
|
|
91
|
+
removeManagedJobDirectoryIfCurrent(item.dir, item.generation);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const retainedCount = () => managedJobCapacitySnapshot(jobRoot).retained_state;
|
|
95
|
+
if (retainedCount() <= targetMaximum) return;
|
|
96
|
+
const removable = entries
|
|
97
|
+
.filter(({ status }) => status && isTerminalManagedJobStatus(status.status))
|
|
98
|
+
.sort((a, b) => terminalRetentionTime(a.status, a.mtime) - terminalRetentionTime(b.status, b.mtime));
|
|
99
|
+
for (const item of removable) {
|
|
100
|
+
if (retainedCount() <= targetMaximum) break;
|
|
101
|
+
removeManagedJobDirectoryIfCurrent(item.dir, item.generation);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function stagedPlanExpired(status, fallbackMtime) {
|
|
105
|
+
const createdAt = Date.parse(String(status?.created_at || ""));
|
|
106
|
+
const baseline = Number.isFinite(createdAt) ? createdAt : fallbackMtime;
|
|
107
|
+
return Number.isFinite(baseline) && Date.now() - baseline > STAGED_PLAN_RETENTION_MS;
|
|
108
|
+
}
|
|
109
|
+
function terminalRetentionTime(status, fallbackMtime) {
|
|
110
|
+
const finishedAt = Date.parse(String(status?.finished_at || ""));
|
|
111
|
+
return Number.isFinite(finishedAt) ? finishedAt : fallbackMtime;
|
|
112
|
+
}
|
|
113
|
+
function expireStagedJob(dir, status) {
|
|
114
|
+
const finishedAt = new Date().toISOString();
|
|
115
|
+
const result = {
|
|
116
|
+
job_id: status.job_id, name: status.name, status: "expired_before_start",
|
|
117
|
+
steps: [], finally_steps: [], error_class: "expired", cleanup_error_class: null, finished_at: finishedAt,
|
|
118
|
+
};
|
|
119
|
+
const terminal = persistManagedJobTerminal({
|
|
120
|
+
statusFile: join(dir, "status.json"), resultFile: join(dir, "result.json"),
|
|
121
|
+
artifacts: [join(dir, "runtime"), join(dir, "plan.json"), join(dir, "runner.pid"), join(dir, "cancel")],
|
|
122
|
+
status: { ...status, cleanup_guarantee: "not-started" }, result,
|
|
123
|
+
writeJson: atomicWriteJson, removeFile: (file) => rmSync(file, { recursive: true, force: true }),
|
|
124
|
+
maxStatusBytes: 256 * 1024, maxResultBytes: 4 * 1024 * 1024, classifyPersistenceError: resourceErrorClass,
|
|
125
|
+
});
|
|
126
|
+
if (!terminal.statusPersisted) throw new Error(`managed job staged expiry status persistence failed: ${terminal.statusErrorClass}`);
|
|
127
|
+
return terminal.status;
|
|
128
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
3
|
+
import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
4
|
+
import { inspectPathIfPresentSync, ownerOnlyFile, readBoundedRegularFileSync, retryTransientMultipleLinksSync } from "./secure-file.mjs";
|
|
5
|
+
|
|
6
|
+
const RUNNER_CLAIM_BYTES = 1024;
|
|
7
|
+
const RUNNER_CLAIM_WAIT_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
export function publishProvisionalRunnerClaim(dir, pid, launchToken) {
|
|
10
|
+
if (!/^[a-f0-9]{32}$/.test(String(launchToken || ""))) throw new Error("runner launch token is invalid");
|
|
11
|
+
const file = join(dir, "runner.pid");
|
|
12
|
+
const claim = { pid, startedAt: new Date().toISOString(), launchToken, committed: false };
|
|
13
|
+
try {
|
|
14
|
+
createExclusiveFileSync(file, `${JSON.stringify(claim)}\n`, { mode: 0o600 });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error?.code !== "EEXIST") throw error;
|
|
17
|
+
const existing = readRunnerClaim(file, "managed job runner claim already exists but is unreadable");
|
|
18
|
+
if (Number(existing?.pid) !== pid || existing?.launchToken !== launchToken) {
|
|
19
|
+
throw new Error("managed job runner claim is owned by another process or launch");
|
|
20
|
+
}
|
|
21
|
+
if (existing.committed === true) return;
|
|
22
|
+
if (existing.committed !== false) throw new Error("managed job runner claim has an invalid provisional state");
|
|
23
|
+
claim.startedAt = typeof existing.startedAt === "string" && existing.startedAt ? existing.startedAt : claim.startedAt;
|
|
24
|
+
}
|
|
25
|
+
ownerOnlyFile(file);
|
|
26
|
+
replaceFileAtomicallySync(file, `${JSON.stringify({ ...claim, committed: true })}\n`, { mode: 0o600 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function confirmRunnerClaim({
|
|
30
|
+
file, pid, processStartedAt, launchToken, inspectPath = inspectPathIfPresentSync,
|
|
31
|
+
waitMs = RUNNER_CLAIM_WAIT_MS,
|
|
32
|
+
}) {
|
|
33
|
+
const exact = { pid, startedAt: new Date().toISOString(), processStartedAt };
|
|
34
|
+
if (!launchToken) {
|
|
35
|
+
createExclusiveFileSync(file, `${JSON.stringify(exact)}\n`, { mode: 0o600 });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!/^[a-f0-9]{32}$/.test(launchToken)) throw new Error("runner launch token is invalid");
|
|
39
|
+
const deadline = createMonotonicDeadline(waitMs);
|
|
40
|
+
while (!deadline.expired()) {
|
|
41
|
+
if (!inspectPath(file, "managed job runner claim")) {
|
|
42
|
+
await new Promise((resolvePromise) => { setTimeout(resolvePromise, 10); });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const provisional = readRunnerClaim(file, "runner ownership claim is unreadable");
|
|
46
|
+
if (Number(provisional?.pid) !== pid || provisional?.launchToken !== launchToken) {
|
|
47
|
+
throw new Error("runner ownership claim does not match the spawned process");
|
|
48
|
+
}
|
|
49
|
+
if (provisional.committed !== true) {
|
|
50
|
+
await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(10, Math.max(1, deadline.remainingMs()))); });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
exact.startedAt = typeof provisional.startedAt === "string" && provisional.startedAt
|
|
54
|
+
? provisional.startedAt
|
|
55
|
+
: exact.startedAt;
|
|
56
|
+
replaceFileAtomicallySync(file, `${JSON.stringify(exact)}\n`, { mode: 0o600 });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw new Error("runner ownership claim was not published before startup deadline");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readRunnerClaim(file, message) {
|
|
63
|
+
try {
|
|
64
|
+
const bytes = retryTransientMultipleLinksSync(() => readBoundedRegularFileSync(file, RUNNER_CLAIM_BYTES, "managed job runner claim", {
|
|
65
|
+
verifyPathIdentity: true,
|
|
66
|
+
rejectMultipleLinks: true,
|
|
67
|
+
}));
|
|
68
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
69
|
+
} catch (error) { throw new Error(message, { cause: error }); }
|
|
70
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import { closeSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { inspectProcessInstance } from "./process-identity.mjs";
|
|
6
7
|
import { classifyOperationalError } from "./log.mjs";
|
|
7
|
-
import { ownerOnlyFile } from "./
|
|
8
|
+
import { ownerOnlyFile } from "./secure-file.mjs";
|
|
8
9
|
import { openPrivateAppendFile, readBoundedFile, trimDiagnosticFile } from "./managed-job-storage.mjs";
|
|
10
|
+
import { publishProvisionalRunnerClaim } from "./managed-job-runner-claim.mjs";
|
|
11
|
+
import { EXECUTION_SURFACE, withExecutionSurface } from "./execution-surface.mjs";
|
|
9
12
|
|
|
10
13
|
const RUNNER_PATH = fileURLToPath(new URL("./job-runner.mjs", import.meta.url));
|
|
11
14
|
|
|
12
15
|
export function launchRunner(dir, recover = false, recoveryToken = "", options = {}) {
|
|
16
|
+
const launchToken = randomBytes(16).toString("hex");
|
|
13
17
|
const args = [RUNNER_PATH, "--job-dir", dir];
|
|
14
18
|
if (recover) args.push("--recover");
|
|
15
19
|
const stdoutFile = join(dir, "runner.out.log");
|
|
@@ -28,7 +32,7 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
|
|
|
28
32
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
29
33
|
windowsHide: true,
|
|
30
34
|
shell: false,
|
|
31
|
-
env:
|
|
35
|
+
env: managedRunnerEnvironment({ fullEnv: options.fullEnv === true, recoveryToken, launchToken, source: options.env || process.env }),
|
|
32
36
|
});
|
|
33
37
|
} finally {
|
|
34
38
|
if (stdoutFd !== undefined) closeSync(stdoutFd);
|
|
@@ -39,13 +43,19 @@ export function launchRunner(dir, recover = false, recoveryToken = "", options =
|
|
|
39
43
|
const logger = options.logger || console;
|
|
40
44
|
child.once?.("error", (error) => {
|
|
41
45
|
logger.error?.("managed job runner process reported an asynchronous failure", {
|
|
42
|
-
job_id: basename(dir),
|
|
43
46
|
recovery: recover,
|
|
44
47
|
error_class: classifyOperationalError(error),
|
|
45
48
|
});
|
|
46
49
|
});
|
|
47
50
|
const pid = Number(child.pid);
|
|
48
51
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("managed job runner did not receive a process id");
|
|
52
|
+
try {
|
|
53
|
+
publishProvisionalRunnerClaim(dir, pid, launchToken);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
try { child.kill?.("SIGKILL"); }
|
|
56
|
+
catch { /* An uncommitted two-phase runner claim prevents this child from executing even if local kill delivery fails. */ }
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
49
59
|
child.unref();
|
|
50
60
|
return pid;
|
|
51
61
|
}
|
|
@@ -63,11 +73,26 @@ export function runnerProcessIsCurrent(status, dir, { ownerOnly = false } = {})
|
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
function readRunnerOwner(dir, fallback = {}) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
let text;
|
|
77
|
+
try { text = readBoundedFile(join(dir, "runner.pid"), 1024).toString("utf8"); }
|
|
78
|
+
catch (error) { if (error?.code === "ENOENT") return { ...fallback }; throw error; }
|
|
79
|
+
let parsed;
|
|
80
|
+
try { parsed = JSON.parse(text); }
|
|
81
|
+
catch (error) { throw new Error("managed job runner claim is invalid", { cause: error }); }
|
|
82
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("managed job runner claim is invalid");
|
|
83
|
+
return { ...fallback, ...parsed };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function managedRunnerEnvironment({ fullEnv = false, recoveryToken = "", launchToken = "", source = process.env } = {}) {
|
|
87
|
+
const env = fullEnv ? { ...source } : {};
|
|
88
|
+
if (!fullEnv) {
|
|
89
|
+
for (const key of ["PATH", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TEMP", "TMP", "AGENT_RESOURCE_COORDINATOR_ROOT", "AGENT_BUILD_ROOT"]) {
|
|
90
|
+
if (typeof source[key] === "string" && source[key]) env[key] = source[key];
|
|
91
|
+
}
|
|
72
92
|
}
|
|
93
|
+
if (recoveryToken) env.MBM_RECOVERY_LOCK_TOKEN = recoveryToken;
|
|
94
|
+
else delete env.MBM_RECOVERY_LOCK_TOKEN;
|
|
95
|
+
if (launchToken) env.MBM_RUNNER_LAUNCH_TOKEN = launchToken;
|
|
96
|
+
else delete env.MBM_RUNNER_LAUNCH_TOKEN;
|
|
97
|
+
return withExecutionSurface(env, EXECUTION_SURFACE.managedJob);
|
|
73
98
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { closeSync, constants as fsConstants, ftruncateSync, readSync, readdirSync, writeSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
const MANAGED_STATE_READ_ATTEMPTS = 4;
|
|
3
5
|
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
4
6
|
import { openRegularFileSync, readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
5
|
-
import { ensureOwnerOnlyDir, ownerOnlyFile } from "./
|
|
7
|
+
import { ensureOwnerOnlyDir, ownerOnlyFile } from "./secure-file.mjs";
|
|
6
8
|
|
|
7
9
|
export function atomicWriteJson(file, value, maxBytes) {
|
|
8
10
|
ensureOwnerOnlyDir(dirname(file));
|
|
@@ -12,11 +14,21 @@ export function atomicWriteJson(file, value, maxBytes) {
|
|
|
12
14
|
ownerOnlyFile(file);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export function readJson(file, maxBytes, label = "JSON") {
|
|
17
|
+
export function readJson(file, maxBytes, label = "JSON", options = {}) {
|
|
18
|
+
const readFile = typeof options.readFile === "function" ? options.readFile : readBoundedRegularFileSync;
|
|
16
19
|
let buffer;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
for (let attempt = 1; attempt <= MANAGED_STATE_READ_ATTEMPTS; attempt += 1) {
|
|
21
|
+
try {
|
|
22
|
+
buffer = readFile(file, maxBytes, "managed job state", {
|
|
23
|
+
verifyPathIdentity: true,
|
|
24
|
+
rejectMultipleLinks: true,
|
|
25
|
+
});
|
|
26
|
+
break;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code === "ENOENT") return null;
|
|
29
|
+
if (error?.code === "MBM_IDENTITY_CHANGED" && attempt < MANAGED_STATE_READ_ATTEMPTS) continue;
|
|
30
|
+
throw new Error(`${label} is unavailable (${resourceErrorClass(error)})`, { cause: error });
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
let text;
|
|
22
34
|
try { text = new TextDecoder("utf-8", { fatal: true }).decode(buffer); } catch {
|
|
@@ -41,7 +53,7 @@ export function openPrivateAppendFile(file) {
|
|
|
41
53
|
return openRegularFileSync(
|
|
42
54
|
file,
|
|
43
55
|
Number(fsConstants.O_WRONLY) | Number(fsConstants.O_CREAT) | Number(fsConstants.O_APPEND),
|
|
44
|
-
{ label: "runner diagnostic path", mode: 0o600, chmod: 0o600 },
|
|
56
|
+
{ label: "runner diagnostic path", mode: 0o600, chmod: 0o600, rejectMultipleLinks: true },
|
|
45
57
|
).fd;
|
|
46
58
|
}
|
|
47
59
|
|
|
@@ -51,6 +63,7 @@ export function trimDiagnosticFile(file, maxBytes = 64 * 1024, keepBytes = 32 *
|
|
|
51
63
|
const opened = openRegularFileSync(file, fsConstants.O_RDWR, {
|
|
52
64
|
label: "runner diagnostic path",
|
|
53
65
|
chmod: 0o600,
|
|
66
|
+
rejectMultipleLinks: true,
|
|
54
67
|
});
|
|
55
68
|
fd = opened.fd;
|
|
56
69
|
if (opened.info.size <= maxBytes) return;
|
|
@@ -79,10 +92,13 @@ export function trimDiagnosticFile(file, maxBytes = 64 * 1024, keepBytes = 32 *
|
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
export function resourceErrorClass(error) {
|
|
95
|
+
if (error?.code === "integrity_error") return "integrity_error";
|
|
96
|
+
if (error?.code === "MBM_IDENTITY_CHANGED" || error?.cause?.code === "MBM_IDENTITY_CHANGED") return "identity_changed";
|
|
82
97
|
const message = String(error?.message || error || "");
|
|
83
98
|
if (/permission|EACCES|EPERM/i.test(message)) return "permission_denied";
|
|
84
99
|
if (/not found|ENOENT/i.test(message)) return "not_found";
|
|
85
100
|
if (/symbolic link/i.test(message)) return "symbolic_link_denied";
|
|
101
|
+
if (/multiple hard links/i.test(message)) return "insecure_links";
|
|
86
102
|
if (/readable by group|permissions/i.test(message)) return "insecure_permissions";
|
|
87
103
|
if (/exceeds/i.test(message)) return "size_limit";
|
|
88
104
|
return "resource_unavailable";
|