machine-bridge-mcp 2.0.0 → 3.0.0-beta.10
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 +109 -0
- package/CONTRIBUTING.md +20 -25
- package/README.md +21 -14
- package/SECURITY.md +147 -123
- package/browser-extension/manifest.json +3 -2
- package/browser-extension/service-worker.js +31 -12
- package/docs/ARCHITECTURE.md +27 -13
- package/docs/AUDIT.md +85 -2
- package/docs/CLIENTS.md +2 -2
- package/docs/ENGINEERING.md +9 -3
- package/docs/GETTING_STARTED.md +10 -9
- package/docs/LOCAL_AUTHORIZATION.md +115 -72
- package/docs/LOGGING.md +12 -3
- package/docs/MANAGED_JOBS.md +24 -29
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +33 -22
- package/docs/OVERVIEW.md +7 -7
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +171 -64
- package/docs/TESTING.md +19 -7
- package/docs/THREAT_MODEL.md +138 -67
- package/docs/TOOL_REFERENCE.md +2 -2
- package/docs/UPGRADING.md +102 -18
- package/native/macos/MachineBridgeTrustBroker.swift +233 -0
- package/package.json +36 -5
- package/scripts/candidate-runtime-store.mjs +38 -0
- package/scripts/check-plan.mjs +19 -0
- package/scripts/check-runner.mjs +4 -1
- package/scripts/commit-message-check.mjs +5 -2
- package/scripts/coverage-check.mjs +30 -2
- package/scripts/github-push.mjs +11 -0
- package/scripts/github-release.mjs +38 -7
- package/scripts/install-published-prerelease.mjs +167 -0
- package/scripts/local-release-acceptance.mjs +23 -12
- package/scripts/npm-publication-policy.mjs +16 -0
- package/scripts/prerelease-activation.mjs +79 -0
- package/scripts/privacy-check.mjs +7 -5
- package/scripts/promotion-digest.mjs +121 -0
- package/scripts/publish-npm.mjs +51 -0
- package/scripts/published-release.mjs +87 -0
- package/scripts/release-acceptance.mjs +30 -1
- package/scripts/release-candidate-manifest.mjs +40 -0
- package/scripts/release-channel.mjs +106 -0
- package/scripts/release-impact-check.mjs +12 -22
- package/scripts/release-soak.mjs +237 -0
- package/scripts/start-release-candidate.mjs +113 -7
- package/src/local/account-access.mjs +6 -0
- package/src/local/account-admin.mjs +34 -16
- package/src/local/agent-context.mjs +45 -42
- package/src/local/app-automation.mjs +9 -4
- package/src/local/authority-context.mjs +106 -0
- package/src/local/browser-bridge-http.mjs +4 -1
- package/src/local/browser-bridge.mjs +14 -9
- package/src/local/browser-broker-routes.mjs +4 -0
- package/src/local/browser-broker-server.mjs +5 -4
- package/src/local/browser-extension-identity.mjs +50 -0
- package/src/local/browser-extension-protocol.mjs +9 -4
- package/src/local/browser-operation-service.mjs +7 -0
- package/src/local/browser-pairing-http.mjs +36 -0
- package/src/local/browser-pairing-store.mjs +56 -47
- package/src/local/cli-account-admin.mjs +41 -7
- package/src/local/cli-activate.mjs +90 -0
- package/src/local/cli-approval.mjs +14 -58
- package/src/local/cli-local-admin.mjs +5 -15
- package/src/local/cli-options.mjs +11 -7
- package/src/local/cli-service.mjs +35 -5
- package/src/local/cli.mjs +112 -58
- package/src/local/daemon-process.mjs +6 -0
- package/src/local/delegated-process-sandbox.mjs +147 -0
- package/src/local/device-identity.mjs +157 -48
- package/src/local/device-root-provider.mjs +87 -0
- package/src/local/errors.mjs +25 -3
- package/src/local/git-service.mjs +16 -15
- package/src/local/job-runner.mjs +35 -27
- package/src/local/log.mjs +14 -1
- package/src/local/macos-trust-broker.mjs +320 -0
- package/src/local/managed-job-lock.mjs +18 -0
- package/src/local/managed-job-projection.mjs +9 -1
- package/src/local/managed-job-runner.mjs +13 -1
- package/src/local/managed-job-storage.mjs +2 -1
- package/src/local/managed-job-terminal.mjs +138 -0
- package/src/local/managed-jobs.mjs +88 -32
- package/src/local/operation-authorization.mjs +75 -239
- package/src/local/operation-risk.mjs +28 -0
- package/src/local/operation-state-lock.mjs +7 -86
- package/src/local/owner-state-lock.mjs +104 -0
- package/src/local/policy.mjs +19 -0
- package/src/local/process-execution.mjs +21 -9
- package/src/local/process-sessions.mjs +27 -17
- package/src/local/process-tracker.mjs +30 -6
- package/src/local/process-tree-ownership.mjs +59 -0
- package/src/local/process-tree.mjs +22 -18
- package/src/local/relay-connection.mjs +47 -35
- package/src/local/relay-diagnostics.mjs +65 -0
- package/src/local/remote-configuration.mjs +48 -0
- package/src/local/runtime-activation.mjs +141 -0
- package/src/local/runtime-diagnostics.mjs +21 -0
- package/src/local/runtime-relay.mjs +9 -8
- package/src/local/runtime-reporting.mjs +8 -3
- package/src/local/runtime-tool-handlers.mjs +8 -8
- package/src/local/runtime.mjs +73 -39
- package/src/local/secure-file.mjs +5 -1
- package/src/local/security-audit-log.mjs +204 -0
- package/src/local/service-convergence.mjs +17 -1
- package/src/local/service-ownership.mjs +17 -0
- package/src/local/service-restart-handoff.mjs +50 -0
- package/src/local/service-restart-scheduler.mjs +49 -0
- package/src/local/service-status.mjs +47 -0
- package/src/local/service.mjs +142 -43
- package/src/local/state-inventory.mjs +21 -3
- package/src/local/state.mjs +155 -17
- package/src/local/stdio.mjs +5 -0
- package/src/local/tool-executor.mjs +39 -6
- package/src/local/tool-result-boundary.mjs +51 -0
- package/src/local/tools.mjs +1 -0
- 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.mjs +20 -0
- package/src/local/worker-deployment.mjs +3 -4
- package/src/local/worker-secret-file.mjs +2 -2
- package/src/local/workspace-file-service.mjs +34 -31
- package/src/shared/admin-auth.d.mts +2 -1
- package/src/shared/admin-auth.mjs +3 -3
- package/src/shared/device-session-auth.d.mts +20 -0
- package/src/shared/device-session-auth.mjs +51 -0
- package/src/shared/server-metadata.json +1 -1
- package/src/shared/tool-catalog.json +2 -2
- package/src/worker/account-admin.ts +23 -22
- package/src/worker/authority.ts +9 -2
- package/src/worker/daemon-auth.ts +45 -67
- package/src/worker/daemon-sockets.ts +12 -2
- package/src/worker/device-session-verifier.ts +129 -0
- package/src/worker/dpop.ts +151 -0
- package/src/worker/http.ts +47 -16
- package/src/worker/index.ts +53 -16
- package/src/worker/nonce-store.ts +1 -5
- package/src/worker/oauth-client-admin.ts +45 -0
- package/src/worker/oauth-controller.ts +55 -6
- package/src/worker/oauth-refresh-families.ts +21 -7
- package/src/worker/oauth-state.ts +6 -0
- package/src/worker/oauth-tokens.ts +28 -8
- 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": "
|
|
3
|
+
"version": "3.0.0-beta.10",
|
|
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
|
+
}
|
package/scripts/check-plan.mjs
CHANGED
|
@@ -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",
|
package/scripts/check-runner.mjs
CHANGED
|
@@ -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: {
|
|
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(
|
|
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(
|
|
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": [
|
|
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/
|
|
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],
|
package/scripts/github-push.mjs
CHANGED
|
@@ -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(
|
|
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,
|
|
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
|
}
|