machine-bridge-mcp 2.0.0 → 3.0.0-beta.11

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.
Files changed (147) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/CONTRIBUTING.md +20 -25
  3. package/README.md +21 -14
  4. package/SECURITY.md +147 -123
  5. package/browser-extension/manifest.json +3 -2
  6. package/browser-extension/service-worker.js +31 -12
  7. package/docs/ARCHITECTURE.md +28 -13
  8. package/docs/AUDIT.md +101 -2
  9. package/docs/CLIENTS.md +2 -2
  10. package/docs/ENGINEERING.md +9 -3
  11. package/docs/GETTING_STARTED.md +10 -9
  12. package/docs/LOCAL_AUTHORIZATION.md +115 -72
  13. package/docs/LOGGING.md +14 -3
  14. package/docs/MANAGED_JOBS.md +24 -29
  15. package/docs/MULTI_ACCOUNT.md +98 -37
  16. package/docs/OPERATIONS.md +34 -23
  17. package/docs/OVERVIEW.md +7 -7
  18. package/docs/PROJECT_STANDARDS.md +9 -5
  19. package/docs/RELEASING.md +171 -64
  20. package/docs/TESTING.md +21 -9
  21. package/docs/THREAT_MODEL.md +138 -67
  22. package/docs/TOOL_REFERENCE.md +2 -2
  23. package/docs/UPGRADING.md +102 -18
  24. package/native/macos/MachineBridgeTrustBroker.swift +233 -0
  25. package/package.json +37 -6
  26. package/scripts/candidate-runtime-store.mjs +38 -0
  27. package/scripts/check-plan.mjs +19 -0
  28. package/scripts/check-runner.mjs +4 -1
  29. package/scripts/commit-message-check.mjs +5 -2
  30. package/scripts/coverage-check.mjs +30 -2
  31. package/scripts/github-push.mjs +11 -0
  32. package/scripts/github-release.mjs +38 -7
  33. package/scripts/install-published-prerelease.mjs +167 -0
  34. package/scripts/local-release-acceptance.mjs +23 -12
  35. package/scripts/npm-publication-policy.mjs +16 -0
  36. package/scripts/prerelease-activation.mjs +79 -0
  37. package/scripts/privacy-check.mjs +7 -5
  38. package/scripts/promotion-digest.mjs +121 -0
  39. package/scripts/publish-npm.mjs +51 -0
  40. package/scripts/published-release.mjs +87 -0
  41. package/scripts/release-acceptance.mjs +30 -1
  42. package/scripts/release-candidate-manifest.mjs +40 -0
  43. package/scripts/release-channel.mjs +106 -0
  44. package/scripts/release-impact-check.mjs +12 -22
  45. package/scripts/release-soak.mjs +237 -0
  46. package/scripts/start-release-candidate.mjs +113 -7
  47. package/src/local/account-access.mjs +6 -0
  48. package/src/local/account-admin.mjs +34 -16
  49. package/src/local/agent-context.mjs +45 -42
  50. package/src/local/app-automation.mjs +9 -4
  51. package/src/local/authority-context.mjs +106 -0
  52. package/src/local/browser-bridge-http.mjs +4 -1
  53. package/src/local/browser-bridge.mjs +14 -9
  54. package/src/local/browser-broker-routes.mjs +4 -0
  55. package/src/local/browser-broker-server.mjs +5 -4
  56. package/src/local/browser-extension-identity.mjs +50 -0
  57. package/src/local/browser-extension-protocol.mjs +9 -4
  58. package/src/local/browser-operation-service.mjs +7 -0
  59. package/src/local/browser-pairing-http.mjs +36 -0
  60. package/src/local/browser-pairing-store.mjs +56 -47
  61. package/src/local/cli-account-admin.mjs +41 -7
  62. package/src/local/cli-activate.mjs +90 -0
  63. package/src/local/cli-approval.mjs +14 -58
  64. package/src/local/cli-local-admin.mjs +5 -15
  65. package/src/local/cli-options.mjs +11 -7
  66. package/src/local/cli-service.mjs +35 -5
  67. package/src/local/cli.mjs +119 -58
  68. package/src/local/daemon-process.mjs +6 -0
  69. package/src/local/delegated-process-sandbox.mjs +147 -0
  70. package/src/local/device-identity.mjs +157 -48
  71. package/src/local/device-root-provider.mjs +87 -0
  72. package/src/local/errors.mjs +25 -3
  73. package/src/local/git-service.mjs +16 -15
  74. package/src/local/job-runner.mjs +35 -27
  75. package/src/local/log.mjs +18 -44
  76. package/src/local/macos-trust-broker.mjs +320 -0
  77. package/src/local/managed-job-lock.mjs +18 -0
  78. package/src/local/managed-job-projection.mjs +9 -1
  79. package/src/local/managed-job-runner.mjs +13 -1
  80. package/src/local/managed-job-storage.mjs +2 -1
  81. package/src/local/managed-job-terminal.mjs +138 -0
  82. package/src/local/managed-jobs.mjs +88 -32
  83. package/src/local/operation-authorization.mjs +75 -239
  84. package/src/local/operation-risk.mjs +28 -0
  85. package/src/local/operation-state-lock.mjs +7 -86
  86. package/src/local/owner-state-lock.mjs +104 -0
  87. package/src/local/policy.mjs +19 -0
  88. package/src/local/process-execution.mjs +21 -9
  89. package/src/local/process-sessions.mjs +27 -17
  90. package/src/local/process-tracker.mjs +30 -6
  91. package/src/local/process-tree-ownership.mjs +59 -0
  92. package/src/local/process-tree.mjs +22 -18
  93. package/src/local/relay-connection.mjs +47 -35
  94. package/src/local/relay-diagnostics.mjs +65 -0
  95. package/src/local/remote-configuration.mjs +48 -0
  96. package/src/local/runtime-activation.mjs +141 -0
  97. package/src/local/runtime-diagnostics.mjs +21 -0
  98. package/src/local/runtime-relay.mjs +9 -8
  99. package/src/local/runtime-reporting.mjs +8 -3
  100. package/src/local/runtime-resource-service.mjs +77 -0
  101. package/src/local/runtime-tool-handlers.mjs +8 -8
  102. package/src/local/runtime.mjs +85 -95
  103. package/src/local/secure-file.mjs +5 -1
  104. package/src/local/security-audit-log.mjs +204 -0
  105. package/src/local/service-convergence.mjs +17 -1
  106. package/src/local/service-ownership.mjs +17 -0
  107. package/src/local/service-restart-handoff.mjs +50 -0
  108. package/src/local/service-restart-scheduler.mjs +49 -0
  109. package/src/local/service-status.mjs +47 -0
  110. package/src/local/service.mjs +142 -43
  111. package/src/local/state-inventory.mjs +21 -3
  112. package/src/local/state.mjs +155 -17
  113. package/src/local/stdio.mjs +5 -0
  114. package/src/local/tool-executor.mjs +39 -6
  115. package/src/local/tool-result-boundary.mjs +51 -0
  116. package/src/local/tools.mjs +1 -0
  117. package/src/local/trusted-executable.mjs +53 -0
  118. package/src/local/trusted-git-executable.mjs +34 -0
  119. package/src/local/trusted-github-cli.mjs +24 -0
  120. package/src/local/windows-service.mjs +20 -0
  121. package/src/local/worker-deployment.mjs +3 -4
  122. package/src/local/worker-secret-file.mjs +2 -2
  123. package/src/local/workspace-file-service.mjs +34 -31
  124. package/src/shared/admin-auth.d.mts +2 -1
  125. package/src/shared/admin-auth.mjs +3 -3
  126. package/src/shared/device-session-auth.d.mts +20 -0
  127. package/src/shared/device-session-auth.mjs +51 -0
  128. package/src/shared/log-redaction.d.mts +9 -0
  129. package/src/shared/log-redaction.mjs +51 -0
  130. package/src/shared/server-metadata.json +1 -1
  131. package/src/shared/tool-catalog.json +2 -2
  132. package/src/worker/account-admin.ts +23 -22
  133. package/src/worker/authority.ts +23 -4
  134. package/src/worker/daemon-auth.ts +45 -67
  135. package/src/worker/daemon-sockets.ts +12 -2
  136. package/src/worker/device-session-verifier.ts +129 -0
  137. package/src/worker/dpop.ts +151 -0
  138. package/src/worker/http.ts +47 -16
  139. package/src/worker/index.ts +53 -16
  140. package/src/worker/nonce-store.ts +1 -5
  141. package/src/worker/oauth-client-admin.ts +45 -0
  142. package/src/worker/oauth-controller.ts +55 -6
  143. package/src/worker/oauth-refresh-families.ts +21 -7
  144. package/src/worker/oauth-state.ts +6 -0
  145. package/src/worker/oauth-tokens.ts +28 -8
  146. package/src/worker/observability.ts +4 -2
  147. package/src/worker/websocket-protocol.ts +8 -2
package/docs/UPGRADING.md CHANGED
@@ -2,37 +2,121 @@
2
2
 
3
3
  ## Supported upgrade contract
4
4
 
5
- Machine Bridge does not retain parallel implementations for obsolete MCP protocol dates, policy revisions, state schemas, lock formats, or browser-extension protocols. The supported path is a direct upgrade from the immediately preceding published package while its state already uses the current schema.
5
+ Machine Bridge supports direct upgrade from the immediately preceding published release. Obsolete transport, state, lock, browser-extension, and authorization implementations are not retained as hidden compatibility paths.
6
6
 
7
- Version 2.0.0 is a coordinated Worker/daemon security-protocol upgrade. It replaces the long-lived daemon bearer with a P-256 device identity, replaces network account-administration bearer requests with per-request HMAC signatures, shortens OAuth access tokens, introduces refresh-token families and replay revocation, and adds local capability leases for high-impact remote effects. The Worker and daemon must converge on 2.0.0 together; the removed daemon bearer protocol is not retained as a fallback.
7
+ Version 3.0.0 is a coordinated Worker, daemon, CLI, and browser-extension security upgrade. Mixed 2.x and 3.x components are intentionally unavailable.
8
8
 
9
- On the first 2.0.0 state load, a workspace without a device identity generates one locally, removes the legacy daemon secret, and rotates the deployment-wide OAuth token version. Existing pre-2.0 access and refresh credentials therefore fail closed and every client must authorize again. The device private JWK remains in owner-only local state; only the public JWK is deployed to the Worker. Subsequent ordinary starts preserve both the device identity and token version.
9
+ The major changes are:
10
10
 
11
- The canonical `full` profile is unchanged as a capability ceiling. Authenticated owner operations use that ceiling directly without terminal approval. High-impact operations from delegated non-owner accounts require a local account/client-bound lease, while their normal workspace-contained reads and edits and project inspection remain automatic. Because the packaged extension controls an existing browser profile, one `browser-session` lease covers profile reads and actions; registered-resource input or file upload uses the separate `data-export` scope. An operator may approve the requested scope or explicitly open a temporary `full` window for at most eight hours. Lease state is independent of the policy profile and does not migrate into OAuth credentials.
11
+ - account roles become non-escalatable hard ceilings;
12
+ - terminal approval IDs and runtime capability leases are removed from authorization;
13
+ - OAuth clients become persistently bound to one account and role version;
14
+ - process sessions, retained output, and managed jobs bind to account, client, and refresh-token family;
15
+ - account administration moves from a long-lived symmetric secret to root-certified ephemeral P-256 session signatures;
16
+ - daemon connections use a root-certified 24-hour in-memory session key;
17
+ - macOS can use a non-exportable Secure Enclave root only through an explicitly configured, provisioning-profile-validated broker; otherwise it retains the portable owner-only root;
18
+ - supported OAuth clients may use DPoP-bound access and refresh tokens;
19
+ - generic path-based remote file access to Machine Bridge control-plane state is denied even to `owner`; arbitrary owner shell execution remains local-user authority;
20
+ - delegated process execution requires a behaviorally verified OS sandbox and otherwise fails closed;
21
+ - local security events form a bounded, privacy-preserving hash chain.
12
22
 
13
- Version 1.2.0 could accept prototype-shaped account roles through malformed administration input. On the first 1.2.1 or later Worker access, such an account is preserved for recovery but repaired fail-closed: its role becomes `reviewer`, it is disabled, its account version advances, and its authorization codes and tokens are removed. An operator can then assign a valid role, enable the account, and rotate its password through the normal account administration flow. A local policy record with an unknown profile label is normalized to `custom` while retaining its explicit capability fields; an invalid explicit `--profile` is rejected.
23
+ ## Version 2 to version 3
14
24
 
15
- A state file from an older unsupported schema is rejected rather than guessed or silently rewritten. Upgrade an old installation through the last release that understands its schema, or initialize a new workspace and re-register resources. Do not edit schema numbers by hand.
25
+ ### Device-root migration
26
+
27
+ `3.0.0-beta.1`, `3.0.0-beta.2`, `3.0.0-beta.3`, `3.0.0-beta.4`, `3.0.0-beta.5`, and `3.0.0-beta.6` are blocked and must not be activated, accepted, published, or promoted. Beta.1 changed the canonical JWK member order used by version 2 device identifiers. Beta.2 repaired that compatibility issue but attempted to create a persistent Secure Enclave key from an ad-hoc-signed runtime helper, which macOS rejected with `errSecMissingEntitlement` (`-34018`). Beta.3 completed owner activation but exposed delegated project/Git authority defects. Beta.4 corrected those defects, yet continued owner-machine auditing found service-control self-termination, timestamp-less mixed-format logs, system-VPN relay outages amplified by a sixty-second reconnect cap, and additional state/browser/job/process boundaries. Beta.5 corrected those findings and passed live Worker/daemon/start/restart verification, but its launchd definition captured npm lifecycle PATH injection and the prior candidate runtime that activation then removed. Beta.6 removed stale candidate paths but selected only the first npm marker; nested npm activation retained an inner project-bin prefix. None of the blocked candidates intentionally rotated or invalidated the existing portable device root.
28
+
29
+ `3.0.0-beta.7` retains the portable P-256 root by default and is the next supported candidate path. It includes the beta.5 authority, relay, state, browser, managed-job, process-tree, audit-lock, hard-link, and service-lifecycle corrections, then makes the persistent service PATH independent of nested npm lifecycle injection and inactive candidate runtimes. Normal startup performs no Keychain operation and requests no user presence unless an explicitly provisioned broker is configured.
30
+
31
+ Secure Enclave enrollment is optional and explicit. Set `MBM_MACOS_TRUST_BROKER` to the absolute executable path inside an app-like broker that is signed by an Apple development or distribution identity and whose data-protection Keychain entitlements are validated by an embedded provisioning profile. Machine Bridge then verifies that the path is a non-symlink regular executable, rejects group/other-writable files, verifies the strict code signature, records the signing identifier and Team ID, and performs an end-to-end create/delete probe with a temporary Secure Enclave key. A source-built or ad-hoc-signed helper is intentionally rejected.
32
+
33
+ When a provisioned broker is configured, migration remains two-phase:
34
+
35
+ 1. the broker creates the non-exportable key and Machine Bridge stores only its canonical broker binding, public JWK, key ID, and Keychain tag as `pendingDeviceIdentity`;
36
+ 2. the Worker is deployed with that pending public key;
37
+ 3. Worker health and exact-version convergence are verified;
38
+ 4. only then is the pending root promoted to active state.
39
+
40
+ A failed probe, upload, interrupted process, or failed health check leaves the old active root intact. `--daemon-only` refuses to activate an undeployed pending root. Every later public-key check and signature revalidates the canonical broker path, code-signing identifier, and Team ID.
41
+
42
+ Other platforms, and macOS installations without a provisioned broker, use the portable P-256 provider. Its private JWK remains owner-only local state and is explicitly reported as exportable.
43
+
44
+ ### OAuth clients and tokens
45
+
46
+ Existing clients may need to authorize again so the Worker can record the new trusted account binding. A refresh token whose client record is not bound to the current account version and role fails closed.
47
+
48
+ Access tokens remain short-lived. Refresh tokens remain one-time rotating and family-bound. Reuse of an already-rotated refresh token revokes the complete family, including active access tokens.
49
+
50
+ DPoP is optional. A client that supports DPoP may bind its token family to a P-256 key; clients without DPoP continue with Bearer tokens under the same account, client, role, and family checks.
51
+
52
+ ### Removed authorization workflows
53
+
54
+ Version 3 does not create or consume pending approval IDs. The following workflows are obsolete:
55
+
56
+ ```text
57
+ machine-mcp approval approve ...
58
+ machine-mcp approval grant ...
59
+ machine-mcp job approve ...
60
+ ```
61
+
62
+ Legacy version 2 lease state may be listed, revoked, or cleared for cleanup, but it has no effect on runtime authority.
63
+
64
+ `stage_job` is a non-executing draft and cannot be promoted by a terminal approval command. A trusted owner uses `start_job`; a local machine operator may submit a reviewed plan with `machine-mcp job submit PLAN.json`.
65
+
66
+ ### Removed administration secret
67
+
68
+ `ACCOUNT_ADMIN_SECRET` is deleted from local state and is no longer deployed to the Worker. Account and OAuth-client administration uses the same root-certified ephemeral session established for daemon startup or an independently authorized local administration command.
16
69
 
17
70
  ## Normal upgrade
18
71
 
19
- 1. Finish or cancel ordinary interactive process sessions. Accepted managed jobs may continue independently, but inspect them before replacing the daemon.
20
- 2. Install the new package with the pinned npm procedure in [Getting started](GETTING_STARTED.md).
21
- 3. Run `machine-mcp doctor`.
22
- 4. Start `machine-mcp` normally in the foreground for each workspace. Startup generates the device identity if needed, rotates the old deployment token version, deploys the matching public key and 2.0.0 Worker, verifies end-to-end readiness, and only then may replace the prior service daemon.
23
- 5. Reauthorize every remote MCP client. Pre-2.0 access and refresh tokens are intentionally invalid.
24
- 6. Reload the unpacked browser extension. Protocol and packaged-version equality are mandatory; an old extension cannot replace a working compatible connection.
25
- 7. Exercise one safe read and one high-impact operation. Approve the resulting scope or an explicit temporary `--full` window through the local CLI, then retry the operation.
26
- 8. Restore or reinstall background service operation only after the foreground path is healthy.
72
+ 1. Inspect or cancel interactive processes and managed jobs that should not survive daemon replacement.
73
+ 2. Back up the owner-only state directory with an operating-system tool that preserves permissions. Do not upload or publish the backup.
74
+ 3. Install the version 3 package.
75
+ 4. Run `machine-mcp doctor`.
76
+ 5. Start each workspace normally in the foreground. Without a provisioned broker, macOS reuses the portable root and does not prompt.
77
+ 6. When `MBM_MACOS_TRUST_BROKER` is intentionally configured, verify the broker deployment first and complete the single user-presence request used to sign the daemon session certificate. Do not start with `--daemon-only` during this optional root migration.
78
+ 7. Allow Machine Bridge to deploy and verify the matching version 3 Worker. A pending root is promoted only after health convergence.
79
+ 8. Reconnect each hosted MCP client and complete OAuth authorization when requested.
80
+ 9. Inspect trusted clients with `machine-mcp account clients` and revoke stale or duplicate records.
81
+ 10. Reload the version 3 browser extension.
82
+ 11. Verify one safe workspace read, one ordinary edit, and one owner-only action appropriate for the deployment.
83
+ 12. Restore background service operation only after the foreground path is healthy.
84
+
85
+ ## Verification
86
+
87
+ After upgrade, `server_info` should report:
88
+
89
+ - matching Worker and daemon version;
90
+ - end-to-end relay readiness;
91
+ - the authenticated account role and effective policy;
92
+ - a current OAuth client and refresh family;
93
+ - device-root provider and exportability;
94
+ - ephemeral session use for reconnect signing;
95
+ - delegated sandbox status;
96
+ - security-audit chain health.
97
+
98
+ A full daemon policy is not proof that a delegated account has full authority. Use `authorization.effective_policy` and `authorization.effective_tools` for the current request.
27
99
 
28
100
  ## Upgrade safety
29
101
 
30
- Before replacing live software, copy the owner-only state directory using operating-system tools that preserve permissions. Do not publish or attach that backup: it contains credentials and private resource metadata.
102
+ Machine Bridge rejects unreadable, malformed, foreign-schema, or ambiguous state rather than silently initializing replacement state.
103
+
104
+ Worker deployment records upload success separately from health convergence. A post-upload network failure does not trigger an uncontrolled repeated write, and a pending root is not promoted merely because Wrangler returned success.
31
105
 
32
- Machine Bridge never treats an unreadable or foreign-schema state file as empty state. It also records a successful Worker upload before secondary health verification, so a network or proxy failure after deployment does not trigger an uncontrolled repeated write.
106
+ The packaged Swift broker source is a development and protocol-conformance fixture only. The local build is ad-hoc signed and is deliberately rejected by the production validator because it cannot obtain a provisioning-profile-validated data-protection Keychain access group. A production Secure Enclave broker must be shipped as an app-like, correctly signed and provisioned component outside the npm runtime build.
33
107
 
34
108
  ## Rollback
35
109
 
36
- Rollback is supported only from a complete pre-upgrade backup. Version 2.0.0 changes Worker/daemon authentication, OAuth refresh state, administration authentication, and local credential material. An older package cannot use the new device protocol or refresh-family state. Rolling back package files alone therefore produces an unavailable or incorrectly authenticated mixed system. Restore the complete owner-only profile backup, prior Worker secret set/build, package version, service definition, and browser extension as one unit, or fix forward.
110
+ Rollback is supported only as a complete unit from a verified pre-upgrade backup.
111
+
112
+ Version 3 changes daemon authentication, account administration, OAuth client trust, object ownership, local authorization, and device-root storage. Copying an older package over version 3 state produces a mixed system that should fail closed.
113
+
114
+ A rollback must restore together:
115
+
116
+ - the complete prior owner-only state root;
117
+ - the prior package;
118
+ - the prior Worker build and secrets;
119
+ - the prior service definition;
120
+ - the prior browser extension.
37
121
 
38
- Never roll back by copying only selected state files or changing version fields. Restore one complete verified state backup, package version, Worker build, and browser extension as a single operational unit.
122
+ Do not roll back by editing version or schema fields, copying selected credential files, or restoring only the Worker. Prefer fixing forward when a complete backup is unavailable.
@@ -0,0 +1,233 @@
1
+ import Foundation
2
+ import Security
3
+ import LocalAuthentication
4
+
5
+ private enum BrokerError: Error, CustomStringConvertible {
6
+ case usage(String)
7
+ case security(OSStatus, String)
8
+ case invalidKey(String)
9
+ case invalidInput(String)
10
+
11
+ var description: String {
12
+ switch self {
13
+ case .usage(let value), .invalidKey(let value), .invalidInput(let value): return value
14
+ case .security(let status, let operation):
15
+ let text = SecCopyErrorMessageString(status, nil) as String? ?? "OSStatus \(status)"
16
+ return "\(operation) failed: \(text)"
17
+ }
18
+ }
19
+ }
20
+
21
+ private struct Output: Encodable {
22
+ let ok: Bool
23
+ let provider: String
24
+ let keyTag: String
25
+ let publicJwk: PublicJwk?
26
+ let signature: String?
27
+ let secureEnclave: Bool
28
+ }
29
+
30
+ private struct PublicJwk: Codable {
31
+ let kty: String
32
+ let crv: String
33
+ let x: String
34
+ let y: String
35
+ }
36
+
37
+ private let providerName = "macos-secure-enclave-v1"
38
+
39
+ @main
40
+ private struct MachineBridgeTrustBroker {
41
+ static func main() {
42
+ do {
43
+ let arguments = CommandLine.arguments
44
+ guard arguments.count >= 4, arguments[2] == "--tag" else {
45
+ throw BrokerError.usage("usage: machine-bridge-trust-broker <ensure|public|sign|delete|status> --tag <tag> [--reason <text>]")
46
+ }
47
+ let action = arguments[1]
48
+ let tag = arguments[3]
49
+ guard tag.range(of: #"^[A-Za-z0-9._-]{8,180}$"#, options: .regularExpression) != nil else {
50
+ throw BrokerError.invalidInput("key tag is invalid")
51
+ }
52
+ let reason = option("--reason", in: arguments) ?? "Authorize Machine Bridge startup"
53
+ let result: Output
54
+ switch action {
55
+ case "ensure":
56
+ let key = try ensurePrivateKey(tag: tag)
57
+ result = try output(tag: tag, key: key, signature: nil)
58
+ case "public":
59
+ let key = try loadPrivateKey(tag: tag, prompt: nil, allowInteraction: false)
60
+ result = try output(tag: tag, key: key, signature: nil)
61
+ case "status":
62
+ let key = try? loadPrivateKey(tag: tag, prompt: nil, allowInteraction: false)
63
+ if let key {
64
+ result = try output(tag: tag, key: key, signature: nil)
65
+ } else {
66
+ result = Output(ok: false, provider: providerName, keyTag: tag, publicJwk: nil, signature: nil, secureEnclave: secureEnclaveAvailable())
67
+ }
68
+ case "sign":
69
+ let data = FileHandle.standardInput.readDataToEndOfFile()
70
+ guard !data.isEmpty, data.count <= 64 * 1024 else { throw BrokerError.invalidInput("signing input is empty or too large") }
71
+ guard String(data: data, encoding: .utf8) != nil else { throw BrokerError.invalidInput("signing input is not UTF-8") }
72
+ let key = try loadPrivateKey(tag: tag, prompt: reason, allowInteraction: true)
73
+ var error: Unmanaged<CFError>?
74
+ guard let der = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &error) as Data? else {
75
+ throw error?.takeRetainedValue() ?? BrokerError.invalidKey("Secure Enclave signing failed")
76
+ }
77
+ let raw = try derToP1363(der)
78
+ result = try output(tag: tag, key: key, signature: raw.base64URLEncodedString())
79
+ case "delete":
80
+ let status = SecItemDelete(keyQuery(tag: tag, returnRef: false, allowInteraction: false, prompt: nil) as CFDictionary)
81
+ guard status == errSecSuccess || status == errSecItemNotFound else { throw BrokerError.security(status, "delete key") }
82
+ result = Output(ok: true, provider: providerName, keyTag: tag, publicJwk: nil, signature: nil, secureEnclave: secureEnclaveAvailable())
83
+ default:
84
+ throw BrokerError.usage("unknown action: \(action)")
85
+ }
86
+ try emit(result)
87
+ } catch {
88
+ let message = String(describing: error)
89
+ FileHandle.standardError.write(Data("machine-bridge-trust-broker: \(message)\n".utf8))
90
+ exit(1)
91
+ }
92
+ }
93
+ }
94
+
95
+ private func ensurePrivateKey(tag: String) throws -> SecKey {
96
+ if let existing = try? loadPrivateKey(tag: tag, prompt: nil, allowInteraction: false) { return existing }
97
+ guard secureEnclaveAvailable() else { throw BrokerError.invalidKey("Secure Enclave is unavailable") }
98
+ var accessError: Unmanaged<CFError>?
99
+ guard let access = SecAccessControlCreateWithFlags(
100
+ nil,
101
+ kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
102
+ [.privateKeyUsage, .userPresence],
103
+ &accessError
104
+ ) else {
105
+ throw accessError?.takeRetainedValue() ?? BrokerError.invalidKey("could not create Secure Enclave access control")
106
+ }
107
+ let privateAttributes: [String: Any] = [
108
+ kSecAttrIsPermanent as String: true,
109
+ kSecAttrApplicationTag as String: Data(tag.utf8),
110
+ kSecAttrAccessControl as String: access,
111
+ ]
112
+ let attributes: [String: Any] = [
113
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
114
+ kSecAttrKeySizeInBits as String: 256,
115
+ kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
116
+ kSecPrivateKeyAttrs as String: privateAttributes,
117
+ ]
118
+ var keyError: Unmanaged<CFError>?
119
+ guard let key = SecKeyCreateRandomKey(attributes as CFDictionary, &keyError) else {
120
+ throw keyError?.takeRetainedValue() ?? BrokerError.invalidKey("could not create Secure Enclave key")
121
+ }
122
+ return key
123
+ }
124
+
125
+ private func loadPrivateKey(tag: String, prompt: String?, allowInteraction: Bool) throws -> SecKey {
126
+ let query = keyQuery(tag: tag, returnRef: true, allowInteraction: allowInteraction, prompt: prompt)
127
+ var result: CFTypeRef?
128
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
129
+ guard status == errSecSuccess, let key = result as! SecKey? else { throw BrokerError.security(status, "load Secure Enclave key") }
130
+ return key
131
+ }
132
+
133
+ private func keyQuery(tag: String, returnRef: Bool, allowInteraction: Bool, prompt: String?) -> [String: Any] {
134
+ var query: [String: Any] = [
135
+ kSecClass as String: kSecClassKey,
136
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
137
+ kSecAttrApplicationTag as String: Data(tag.utf8),
138
+ kSecReturnRef as String: returnRef,
139
+ kSecMatchLimit as String: kSecMatchLimitOne,
140
+ ]
141
+ let context = LAContext()
142
+ if allowInteraction {
143
+ context.localizedReason = prompt ?? "Authorize Machine Bridge startup"
144
+ } else {
145
+ context.interactionNotAllowed = true
146
+ }
147
+ query[kSecUseAuthenticationContext as String] = context
148
+ return query
149
+ }
150
+
151
+ private func output(tag: String, key: SecKey, signature: String?) throws -> Output {
152
+ guard let publicKey = SecKeyCopyPublicKey(key) else { throw BrokerError.invalidKey("public key is unavailable") }
153
+ var error: Unmanaged<CFError>?
154
+ guard let external = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
155
+ throw error?.takeRetainedValue() ?? BrokerError.invalidKey("public key export failed")
156
+ }
157
+ guard external.count == 65, external.first == 0x04 else { throw BrokerError.invalidKey("public key representation is invalid") }
158
+ let x = external.subdata(in: 1..<33).base64URLEncodedString()
159
+ let y = external.subdata(in: 33..<65).base64URLEncodedString()
160
+ return Output(
161
+ ok: true,
162
+ provider: providerName,
163
+ keyTag: tag,
164
+ publicJwk: PublicJwk(kty: "EC", crv: "P-256", x: x, y: y),
165
+ signature: signature,
166
+ secureEnclave: true
167
+ )
168
+ }
169
+
170
+ private func secureEnclaveAvailable() -> Bool {
171
+ let attributes: [String: Any] = [
172
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
173
+ kSecAttrKeySizeInBits as String: 256,
174
+ kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
175
+ kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false],
176
+ ]
177
+ var error: Unmanaged<CFError>?
178
+ return SecKeyCreateRandomKey(attributes as CFDictionary, &error) != nil
179
+ }
180
+
181
+ private func derToP1363(_ data: Data) throws -> Data {
182
+ var index = 0
183
+ func readByte() throws -> UInt8 {
184
+ guard index < data.count else { throw BrokerError.invalidInput("ECDSA signature is truncated") }
185
+ defer { index += 1 }
186
+ return data[index]
187
+ }
188
+ func readLength() throws -> Int {
189
+ let first = try readByte()
190
+ if first < 0x80 { return Int(first) }
191
+ let count = Int(first & 0x7f)
192
+ guard count > 0, count <= 2 else { throw BrokerError.invalidInput("ECDSA signature length is invalid") }
193
+ var length = 0
194
+ for _ in 0..<count { length = (length << 8) | Int(try readByte()) }
195
+ return length
196
+ }
197
+ guard try readByte() == 0x30 else { throw BrokerError.invalidInput("ECDSA signature sequence is invalid") }
198
+ let sequenceLength = try readLength()
199
+ guard sequenceLength == data.count - index else { throw BrokerError.invalidInput("ECDSA signature sequence length is invalid") }
200
+ func readInteger() throws -> Data {
201
+ guard try readByte() == 0x02 else { throw BrokerError.invalidInput("ECDSA signature integer is invalid") }
202
+ let length = try readLength()
203
+ guard length > 0, index + length <= data.count else { throw BrokerError.invalidInput("ECDSA signature integer is truncated") }
204
+ var value = data.subdata(in: index..<(index + length))
205
+ index += length
206
+ while value.count > 32, value.first == 0 { value.removeFirst() }
207
+ guard value.count <= 32 else { throw BrokerError.invalidInput("ECDSA signature integer is too large") }
208
+ if value.count < 32 { value = Data(repeating: 0, count: 32 - value.count) + value }
209
+ return value
210
+ }
211
+ let r = try readInteger()
212
+ let s = try readInteger()
213
+ guard index == data.count else { throw BrokerError.invalidInput("ECDSA signature has trailing bytes") }
214
+ return r + s
215
+ }
216
+
217
+ private func option(_ name: String, in arguments: [String]) -> String? {
218
+ guard let index = arguments.firstIndex(of: name), index + 1 < arguments.count else { return nil }
219
+ return arguments[index + 1]
220
+ }
221
+
222
+ private func emit<T: Encodable>(_ value: T) throws {
223
+ let encoder = JSONEncoder()
224
+ encoder.outputFormatting = [.sortedKeys]
225
+ FileHandle.standardOutput.write(try encoder.encode(value))
226
+ FileHandle.standardOutput.write(Data("\n".utf8))
227
+ }
228
+
229
+ private extension Data {
230
+ func base64URLEncodedString() -> String {
231
+ base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
232
+ }
233
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "2.0.0",
3
+ "version": "3.0.0-beta.11",
4
4
  "description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,6 +22,7 @@
22
22
  "src/shared",
23
23
  "src/worker",
24
24
  "scripts",
25
+ "native",
25
26
  "wrangler.jsonc",
26
27
  "tsconfig.json",
27
28
  "tsconfig.local.json",
@@ -43,7 +44,7 @@
43
44
  "version:check": "node scripts/sync-worker-version.mjs --check",
44
45
  "version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts browser-extension/manifest.json",
45
46
  "prepack": "npm run version:check && npm run privacy:check && npm run release-impact:check",
46
- "prepublishOnly": "npm run check && npm run version:check && npm run release:check",
47
+ "prepublishOnly": "npm run publication:check && npm run check && npm run version:check && npm run release:check",
47
48
  "worker:types": "node scripts/generate-worker-types.mjs",
48
49
  "typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit && npm run typecheck:local",
49
50
  "syntax": "node scripts/syntax-check.mjs",
@@ -135,12 +136,39 @@
135
136
  "github-backlog:test": "node tests/github-backlog-test.mjs",
136
137
  "github:backlog": "node scripts/github-backlog.mjs",
137
138
  "operation-authorization:test": "node tests/operation-authorization-test.mjs",
138
- "device-auth:test": "node tests/device-auth-test.mjs"
139
+ "device-auth:test": "node tests/device-auth-test.mjs",
140
+ "device-root-provider:test": "node tests/device-root-provider-test.mjs",
141
+ "security-audit:test": "node tests/security-audit-log-test.mjs",
142
+ "delegated-sandbox:test": "node tests/delegated-process-sandbox-test.mjs",
143
+ "dpop:test": "node tests/dpop-test.mjs",
144
+ "macos-trust:test": "node tests/macos-trust-broker-test.mjs",
145
+ "worker-security:test": "node tests/worker-security-boundaries-test.mjs",
146
+ "publication:check": "node scripts/npm-publication-policy.mjs",
147
+ "prerelease:release": "node scripts/github-release.mjs --publish-prerelease",
148
+ "release:candidate:activate": "node scripts/start-release-candidate.mjs --activate-service",
149
+ "prerelease:install": "node scripts/install-published-prerelease.mjs",
150
+ "prerelease:soak:accept": "node scripts/release-soak.mjs --record",
151
+ "release:soak:verify": "node scripts/release-soak.mjs --verify",
152
+ "release-channel:test": "node tests/release-channel-test.mjs",
153
+ "promotion-digest:test": "node tests/promotion-digest-test.mjs",
154
+ "prerelease-activation:test": "node tests/prerelease-activation-test.mjs",
155
+ "release-soak:test": "node tests/release-soak-test.mjs",
156
+ "published-release:test": "node tests/published-release-test.mjs",
157
+ "npm-publication-policy:test": "node tests/npm-publication-policy-test.mjs",
158
+ "runtime-activation:test": "node tests/runtime-activation-test.mjs",
159
+ "prerelease:publish": "node scripts/publish-npm.mjs prerelease",
160
+ "stable:publish": "node scripts/publish-npm.mjs stable",
161
+ "publish-npm:test": "node tests/publish-npm-test.mjs",
162
+ "release-candidate-manifest:test": "node tests/release-candidate-manifest-test.mjs",
163
+ "device-key-id-compatibility:test": "node tests/device-key-id-compatibility-test.mjs",
164
+ "candidate-runtime-store:test": "node tests/candidate-runtime-store-test.mjs",
165
+ "service-restart:test": "node tests/service-restart-handoff-test.mjs",
166
+ "browser-identity:test": "node tests/browser-extension-identity-test.mjs"
139
167
  },
140
168
  "dependencies": {
141
169
  "https-proxy-agent": "9.1.0",
142
170
  "proxy-from-env": "2.1.0",
143
- "wrangler": "4.112.0",
171
+ "wrangler": "4.113.0",
144
172
  "ws": "8.21.1"
145
173
  },
146
174
  "devDependencies": {
@@ -178,9 +206,9 @@
178
206
  "homepage": "https://github.com/YuLeiFuYun/machine-bridge-mcp#readme",
179
207
  "allowScripts": {
180
208
  "esbuild@0.28.1": true,
181
- "sharp@0.34.5": true,
182
209
  "fsevents": false,
183
- "workerd@1.20260714.1": true
210
+ "sharp@0.35.3": true,
211
+ "workerd@1.20260721.1": true
184
212
  },
185
213
  "packageManager": "npm@12.0.1",
186
214
  "devEngines": {
@@ -194,5 +222,8 @@
194
222
  "version": ">=12.0.0",
195
223
  "onFail": "error"
196
224
  }
225
+ },
226
+ "overrides": {
227
+ "sharp": "0.35.3"
197
228
  }
198
229
  }
@@ -0,0 +1,38 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, readdirSync, rmSync } from "node:fs";
3
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { parseReleaseVersion } from "./release-channel.mjs";
5
+
6
+ const RUNTIME_DIRECTORY_PATTERN = /^v[0-9A-Za-z.-]+-[0-9a-f]{12}-[0-9a-f]{12}$/;
7
+
8
+ export function candidateRuntimeContainer(stateRoot) {
9
+ return resolve(stateRoot, "release-channels", "runtimes");
10
+ }
11
+
12
+ export function createCandidateRuntimePrefix({ stateRoot, version, shasum, random = () => randomBytes(6).toString("hex") }) {
13
+ const parsed = parseReleaseVersion(version);
14
+ const digest = String(shasum || "");
15
+ if (!/^[0-9a-f]{40}$/.test(digest)) throw new Error("candidate runtime package SHA-1 is invalid");
16
+ const suffix = String(random());
17
+ if (!/^[0-9a-f]{12}$/.test(suffix)) throw new Error("candidate runtime random suffix is invalid");
18
+ return join(candidateRuntimeContainer(stateRoot), `v${parsed.raw}-${digest.slice(0, 12)}-${suffix}`);
19
+ }
20
+
21
+ export function pruneInactiveCandidateRuntimes({ stateRoot, activePrefix, remove = rmSync } = {}) {
22
+ const container = candidateRuntimeContainer(stateRoot);
23
+ const active = resolve(String(activePrefix || ""));
24
+ const activeRelative = relative(container, active);
25
+ if (!activeRelative || activeRelative === ".." || activeRelative.startsWith(`..${sep}`) || isAbsolute(activeRelative)) {
26
+ throw new Error("active candidate runtime is outside the runtime container");
27
+ }
28
+ if (!existsSync(container)) return [];
29
+ const removed = [];
30
+ for (const entry of readdirSync(container, { withFileTypes: true })) {
31
+ if (!entry.isDirectory() || !RUNTIME_DIRECTORY_PATTERN.test(entry.name)) continue;
32
+ const candidate = resolve(container, entry.name);
33
+ if (candidate === active) continue;
34
+ remove(candidate, { recursive: true, force: true });
35
+ removed.push(candidate);
36
+ }
37
+ return removed;
38
+ }
@@ -2,6 +2,16 @@ export const FAST_CHECK_TASKS = Object.freeze([
2
2
  "privacy:check",
3
3
  "release-impact:check",
4
4
  "release:acceptance:test",
5
+ "release-channel:test",
6
+ "release-candidate-manifest:test",
7
+ "promotion-digest:test",
8
+ "prerelease-activation:test",
9
+ "release-soak:test",
10
+ "published-release:test",
11
+ "npm-publication-policy:test",
12
+ "publish-npm:test",
13
+ "runtime-activation:test",
14
+ "candidate-runtime-store:test",
5
15
  "release-state:test",
6
16
  "release-ci:test",
7
17
  "network-retry:test",
@@ -11,6 +21,10 @@ export const FAST_CHECK_TASKS = Object.freeze([
11
21
  "secure-file:test",
12
22
  "worker-secret-file:test",
13
23
  "device-auth:test",
24
+ "device-root-provider:test",
25
+ "device-key-id-compatibility:test",
26
+ "dpop:test",
27
+ "security-audit:test",
14
28
  "sarif-security:test",
15
29
  "shell:test",
16
30
  "architecture:test",
@@ -24,6 +38,7 @@ export const FAST_CHECK_TASKS = Object.freeze([
24
38
  "account:test",
25
39
  "operation-authorization:test",
26
40
  "worker-oauth-controller:test",
41
+ "worker-security:test",
27
42
  "policy-docs:check",
28
43
  "tool-docs:check",
29
44
  "runtime-infrastructure:test",
@@ -31,6 +46,7 @@ export const FAST_CHECK_TASKS = Object.freeze([
31
46
  "runtime-handlers:test",
32
47
  "cli-entrypoint:test",
33
48
  "cli-service:test",
49
+ "service-restart:test",
34
50
  "lifecycle:test",
35
51
  "logging-structure:test",
36
52
  "worker-runtime-infrastructure:test",
@@ -42,6 +58,7 @@ export const FAST_CHECK_TASKS = Object.freeze([
42
58
  "catalog:test",
43
59
  "capability-ranking:test",
44
60
  "agent-boundaries:test",
61
+ "browser-identity:test",
45
62
  "browser-command:test",
46
63
  "check-plan:test",
47
64
  ]);
@@ -56,6 +73,8 @@ export const PLATFORM_ONLY_CHECK_TASKS = Object.freeze([
56
73
  "service-lifecycle:test",
57
74
  "service-environment:test",
58
75
  "service-platform:test",
76
+ "delegated-sandbox:test",
77
+ "macos-trust:test",
59
78
  "agent-context:test",
60
79
  "browser-devtools-input:test",
61
80
  "browser-service-worker:test",
@@ -46,7 +46,10 @@ function runTask({ task, npmCli, cwd, env, verbose, spawnProcess }) {
46
46
  try {
47
47
  child = spawnProcess(process.execPath, [npmCli, "run", "--silent", task], {
48
48
  cwd,
49
- env: { ...env, NO_COLOR: env.NO_COLOR || "1" },
49
+ env: {
50
+ ...env,
51
+ NO_COLOR: env.NO_COLOR || "1",
52
+ },
50
53
  stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
51
54
  windowsHide: true,
52
55
  shell: false,
@@ -1,5 +1,8 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import process from "node:process";
3
+ import { createTrustedGitResolver } from "../src/local/trusted-git-executable.mjs";
4
+
5
+ const gitExecutable = createTrustedGitResolver({ workspace: process.cwd() });
3
6
 
4
7
  const allowedTypes = Object.freeze([
5
8
  "feat",
@@ -54,10 +57,10 @@ function readTitles(args) {
54
57
  if (rangeIndex !== -1) {
55
58
  const range = args[rangeIndex + 1];
56
59
  if (!range) throw new Error("--range requires a Git revision range");
57
- return execFileSync("git", ["log", "--format=%s", range], { encoding: "utf8" })
60
+ return execFileSync(gitExecutable(), ["log", "--format=%s", range], { encoding: "utf8" })
58
61
  .split(/\r?\n/)
59
62
  .filter(Boolean);
60
63
  }
61
64
 
62
- return [execFileSync("git", ["log", "-1", "--format=%s"], { encoding: "utf8" }).trim()];
65
+ return [execFileSync(gitExecutable(), ["log", "-1", "--format=%s"], { encoding: "utf8" }).trim()];
63
66
  }