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

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 (143) hide show
  1. package/CHANGELOG.md +101 -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 +27 -13
  8. package/docs/AUDIT.md +85 -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 +12 -3
  14. package/docs/MANAGED_JOBS.md +24 -29
  15. package/docs/MULTI_ACCOUNT.md +98 -37
  16. package/docs/OPERATIONS.md +33 -22
  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 +19 -7
  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 +36 -5
  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 +80 -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 +112 -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 +14 -1
  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-tool-handlers.mjs +8 -8
  101. package/src/local/runtime.mjs +73 -39
  102. package/src/local/secure-file.mjs +5 -1
  103. package/src/local/security-audit-log.mjs +204 -0
  104. package/src/local/service-convergence.mjs +17 -1
  105. package/src/local/service-ownership.mjs +17 -0
  106. package/src/local/service-restart-handoff.mjs +50 -0
  107. package/src/local/service-restart-scheduler.mjs +49 -0
  108. package/src/local/service-status.mjs +47 -0
  109. package/src/local/service.mjs +142 -43
  110. package/src/local/state-inventory.mjs +21 -3
  111. package/src/local/state.mjs +155 -17
  112. package/src/local/stdio.mjs +5 -0
  113. package/src/local/tool-executor.mjs +39 -6
  114. package/src/local/tool-result-boundary.mjs +51 -0
  115. package/src/local/tools.mjs +1 -0
  116. package/src/local/trusted-executable.mjs +53 -0
  117. package/src/local/trusted-git-executable.mjs +34 -0
  118. package/src/local/trusted-github-cli.mjs +24 -0
  119. package/src/local/windows-service.mjs +20 -0
  120. package/src/local/worker-deployment.mjs +3 -4
  121. package/src/local/worker-secret-file.mjs +2 -2
  122. package/src/local/workspace-file-service.mjs +34 -31
  123. package/src/shared/admin-auth.d.mts +2 -1
  124. package/src/shared/admin-auth.mjs +3 -3
  125. package/src/shared/device-session-auth.d.mts +20 -0
  126. package/src/shared/device-session-auth.mjs +51 -0
  127. package/src/shared/server-metadata.json +1 -1
  128. package/src/shared/tool-catalog.json +2 -2
  129. package/src/worker/account-admin.ts +23 -22
  130. package/src/worker/authority.ts +9 -2
  131. package/src/worker/daemon-auth.ts +45 -67
  132. package/src/worker/daemon-sockets.ts +12 -2
  133. package/src/worker/device-session-verifier.ts +129 -0
  134. package/src/worker/dpop.ts +151 -0
  135. package/src/worker/http.ts +47 -16
  136. package/src/worker/index.ts +53 -16
  137. package/src/worker/nonce-store.ts +1 -5
  138. package/src/worker/oauth-client-admin.ts +45 -0
  139. package/src/worker/oauth-controller.ts +55 -6
  140. package/src/worker/oauth-refresh-families.ts +21 -7
  141. package/src/worker/oauth-state.ts +6 -0
  142. package/src/worker/oauth-tokens.ts +28 -8
  143. package/src/worker/websocket-protocol.ts +8 -2
@@ -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.9",
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,7 +136,34 @@
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",
@@ -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
+ "workerd@1.20260714.1": true,
211
+ "sharp@0.35.3": 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
  }
@@ -16,6 +16,7 @@ const tests = [
16
16
  "tests/runtime-handler-matrix-test.mjs",
17
17
  "tests/cli-entrypoint-test.mjs",
18
18
  "tests/cli-service-test.mjs",
19
+ "tests/service-restart-handoff-test.mjs",
19
20
  "tests/local-self-test.mjs",
20
21
  "tests/runtime-self-test.mjs",
21
22
  "tests/numbers-test.mjs",
@@ -31,6 +32,12 @@ const tests = [
31
32
  "tests/managed-jobs-test.mjs",
32
33
  "tests/account-admin-test.mjs",
33
34
  "tests/monotonic-deadline-test.mjs",
35
+ "tests/device-auth-test.mjs",
36
+ "tests/operation-authorization-test.mjs",
37
+ "tests/security-audit-log-test.mjs",
38
+ "tests/delegated-process-sandbox-test.mjs",
39
+ "tests/dpop-test.mjs",
40
+ "tests/worker-security-boundaries-test.mjs",
34
41
  ];
35
42
 
36
43
  try {
@@ -50,17 +57,31 @@ try {
50
57
 
51
58
  const coverage = collectCoverage(coverageDir);
52
59
  const thresholds = {
60
+ "src/local/authority-context.mjs": [85, 55],
61
+ "src/local/device-identity.mjs": [90, null],
62
+ "src/local/operation-authorization.mjs": [85, 45],
63
+ "src/local/operation-risk.mjs": [85, 60],
64
+ "src/local/operation-state-lock.mjs": [100, 7],
65
+ "src/local/security-audit-log.mjs": [85, 55],
66
+ "src/local/delegated-process-sandbox.mjs": [80, 45],
67
+ "src/shared/device-session-auth.mjs": [100, null],
53
68
  "src/local/policy.mjs": [90, 65],
54
69
  "src/local/errors.mjs": [70, 50],
55
70
  "src/local/call-registry.mjs": [85, 55],
56
71
  "src/local/tool-executor.mjs": [90, 40],
72
+ "src/local/tool-result-boundary.mjs": [100, 75],
57
73
  "src/local/observability.mjs": [95, 40],
58
74
  "src/local/process-tracker.mjs": [65, 35],
59
75
  "src/local/log.mjs": [60, 40],
60
76
  "src/local/runtime.mjs": [75, 55],
61
77
  "src/local/runtime-paths.mjs": [90, 50],
62
78
  "src/local/cli.mjs": [48, 21.9],
63
- "src/local/cli-service.mjs": [90, 65],
79
+ "src/local/cli-service.mjs": [100, 75],
80
+ "src/local/service-convergence.mjs": [100, 75],
81
+ "src/local/service-status.mjs": [100, 75],
82
+ "src/local/service-ownership.mjs": [100, 75],
83
+ "src/local/service-restart-scheduler.mjs": [100, 70],
84
+ "src/local/service-restart-handoff.mjs": [100, 60],
64
85
  "src/local/cli-options.mjs": [65, 35],
65
86
  "src/local/cli-policy.mjs": [70, 35],
66
87
  "src/local/numbers.mjs": [100, 100],
@@ -91,7 +112,14 @@ try {
91
112
  "src/local/browser-request-registry.mjs": [95, 35],
92
113
  "src/local/browser-broker-routes.mjs": [85, 55],
93
114
  "src/local/browser-broker-server.mjs": [80, 50],
94
- "src/worker/oauth-controller.ts": [85, 65],
115
+ "src/worker/account-admin.ts": [70, 35],
116
+ "src/worker/daemon-auth.ts": [85, 45],
117
+ "src/worker/dpop.ts": [90, 30],
118
+ "src/worker/nonce-store.ts": [85, 50],
119
+ "src/worker/oauth-client-admin.ts": [75, 45],
120
+ "src/worker/oauth-refresh-families.ts": [70, 35],
121
+ "src/worker/oauth-tokens.ts": [70, 35],
122
+ "src/worker/oauth-controller.ts": [84, 65],
95
123
  "src/worker/oauth-authorization-page.ts": [90, 60],
96
124
  "src/worker/pending-calls.ts": [90, 35],
97
125
  "src/worker/policy.ts": [100, 25],
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
4
5
  import { dirname, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
7
8
  import { assertGitHubBacklogReady } from "./github-backlog.mjs";
9
+ import { parseReleaseVersion, requiresSoakForStable } from "./release-channel.mjs";
10
+ import { verifyCurrentStableSoak } from "./release-soak.mjs";
8
11
 
9
12
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
13
 
@@ -25,6 +28,14 @@ try {
25
28
  console.log(`Verified interactive local candidate acceptance for ${acceptance.metadata.filename}.`);
26
29
  }
27
30
 
31
+ const releaseVersion = parseReleaseVersion(JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")).version);
32
+ if (!releaseVersion.prerelease && requiresSoakForStable(releaseVersion.raw)) {
33
+ const soak = verifyCurrentStableSoak(root);
34
+ const path = `release-soak/v${releaseVersion.raw}.json`;
35
+ run("git", ["ls-files", "--error-unmatch", path], { capture: true });
36
+ console.log(`Verified stable promotion from soaked ${soak.record.prerelease_version}.`);
37
+ }
38
+
28
39
  run("git", ["push", "--set-upstream", "origin", "HEAD"]);
29
40
  console.log(`Pushed accepted branch ${branch} to origin.`);
30
41
  } catch (error) {
@@ -13,6 +13,8 @@ import { runNetworkCommand } from "./network-retry.mjs";
13
13
  import { requireSuccessfulWorkflowRun } from "./release-ci.mjs";
14
14
  import { tagSyncError } from "./release-state.mjs";
15
15
  import { verifyCurrentReleaseAcceptance } from "./release-acceptance.mjs";
16
+ import { parseReleaseVersion, requiresSoakForStable } from "./release-channel.mjs";
17
+ import { verifyCurrentStableSoak } from "./release-soak.mjs";
16
18
  import { fileURLToPath } from "node:url";
17
19
 
18
20
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -179,7 +181,9 @@ function releaseInfo(tag) {
179
181
 
180
182
  function assertCoreSync({ requireReleaseAsset }) {
181
183
  const pkg = packageMetadata();
184
+ const parsedVersion = parseReleaseVersion(pkg.version);
182
185
  assertLocalAcceptance();
186
+ if (requiresSoakForStable(pkg.version)) assertStableSoak();
183
187
  const tag = `v${pkg.version}`;
184
188
  const head = output("git", ["rev-parse", "HEAD"]);
185
189
  const originMain = output("git", ["rev-parse", "origin/main"]);
@@ -197,8 +201,10 @@ function assertCoreSync({ requireReleaseAsset }) {
197
201
  if (remoteTagError) fail(remoteTagError);
198
202
 
199
203
  const release = releaseInfo(tag);
200
- if (!release || release.isDraft || release.isPrerelease) {
201
- fail(`published GitHub Release ${tag} is missing or not final`);
204
+ if (!release || release.isDraft || release.isPrerelease !== parsedVersion.prerelease) {
205
+ fail(parsedVersion.prerelease
206
+ ? `published GitHub prerelease ${tag} is missing or not marked as prerelease`
207
+ : `published GitHub Release ${tag} is missing or not final`);
202
208
  }
203
209
 
204
210
  if (requireReleaseAsset) {
@@ -250,7 +256,7 @@ function normalizePackRecord(value, packageName) {
250
256
  return Object.values(value).find((item) => item && typeof item === "object") ?? null;
251
257
  }
252
258
 
253
- function ensureRelease(tag, version, assetPath, latest) {
259
+ function ensureRelease(tag, version, assetPath, { latest, prerelease }) {
254
260
  const temp = dirname(assetPath);
255
261
  const notes = writeNotesFile(temp, version);
256
262
  const existing = releaseInfo(tag);
@@ -266,6 +272,7 @@ function ensureRelease(tag, version, assetPath, latest) {
266
272
  "--title",
267
273
  title,
268
274
  latest ? "--latest" : "--latest=false",
275
+ prerelease ? "--prerelease" : "--prerelease=false",
269
276
  ];
270
277
  if (notes) args.push("--notes-file", notes);
271
278
  else args.push("--generate-notes");
@@ -279,13 +286,14 @@ function ensureRelease(tag, version, assetPath, latest) {
279
286
  "--title",
280
287
  title,
281
288
  latest ? "--latest" : "--latest=false",
289
+ prerelease ? "--prerelease" : "--prerelease=false",
282
290
  ...(notes ? ["--notes-file", notes] : []),
283
291
  ]);
284
292
  runNetwork("gh", ["release", "upload", tag, assetPath, "--clobber"]);
285
293
  }
286
294
  }
287
295
 
288
- function publishCurrent() {
296
+ function publishCurrent({ prereleaseMode = false } = {}) {
289
297
  ensureClean();
290
298
  fetchRemote();
291
299
 
@@ -293,6 +301,13 @@ function publishCurrent() {
293
301
  if (branch !== "main") fail(`release must run from main, not ${branch || "detached HEAD"}`);
294
302
 
295
303
  const pkg = packageMetadata();
304
+ const parsedVersion = parseReleaseVersion(pkg.version);
305
+ if (prereleaseMode !== parsedVersion.prerelease) {
306
+ fail(parsedVersion.prerelease
307
+ ? "prerelease versions must use npm run prerelease:release"
308
+ : "stable versions must use npm run release");
309
+ }
310
+ if (!parsedVersion.prerelease && requiresSoakForStable(pkg.version)) assertStableSoak();
296
311
  const tag = `v${pkg.version}`;
297
312
  if (!changelogBody(pkg.version)) {
298
313
  fail(`CHANGELOG.md has no section for ${pkg.version}`);
@@ -329,7 +344,10 @@ function publishCurrent() {
329
344
  const temp = mkdtempSync(join(tmpdir(), "machine-bridge-mcp-release-"));
330
345
  try {
331
346
  const assetPath = packReleaseAsset(temp, pkg);
332
- ensureRelease(tag, pkg.version, assetPath, true);
347
+ ensureRelease(tag, pkg.version, assetPath, {
348
+ latest: !parsedVersion.prerelease,
349
+ prerelease: parsedVersion.prerelease,
350
+ });
333
351
  } finally {
334
352
  rmSync(temp, { recursive: true, force: true });
335
353
  }
@@ -338,6 +356,17 @@ function publishCurrent() {
338
356
  assertCoreSync({ requireReleaseAsset: true });
339
357
  }
340
358
 
359
+ function assertStableSoak() {
360
+ try {
361
+ const result = verifyCurrentStableSoak(root);
362
+ if (result.required) {
363
+ console.log(`Stable promotion matches soaked ${result.record.prerelease_version} (${result.promotionDigest}).`);
364
+ }
365
+ } catch (error) {
366
+ fail(String(error?.message || error));
367
+ }
368
+ }
369
+
341
370
  function assertLocalAcceptance() {
342
371
  try {
343
372
  const result = verifyCurrentReleaseAcceptance(root);
@@ -405,9 +434,11 @@ if (mode === "--check") {
405
434
  fetchRemote();
406
435
  assertCoreSync({ requireReleaseAsset: true });
407
436
  } else if (mode === "--publish") {
408
- publishCurrent();
437
+ publishCurrent({ prereleaseMode: false });
438
+ } else if (mode === "--publish-prerelease") {
439
+ publishCurrent({ prereleaseMode: true });
409
440
  } else if (mode === "--backfill") {
410
441
  backfillMissingReleases();
411
442
  } else {
412
- fail("usage: node scripts/github-release.mjs [--check|--publish|--backfill]");
443
+ fail("usage: node scripts/github-release.mjs [--check|--publish|--publish-prerelease|--backfill]");
413
444
  }