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.
- package/CHANGELOG.md +120 -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 +28 -13
- package/docs/AUDIT.md +101 -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 +14 -3
- package/docs/MANAGED_JOBS.md +24 -29
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +34 -23
- package/docs/OVERVIEW.md +7 -7
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +171 -64
- package/docs/TESTING.md +21 -9
- 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 +37 -6
- 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 +119 -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 +18 -44
- 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-resource-service.mjs +77 -0
- package/src/local/runtime-tool-handlers.mjs +8 -8
- package/src/local/runtime.mjs +85 -95
- 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/log-redaction.d.mts +9 -0
- package/src/shared/log-redaction.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 +23 -4
- 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/observability.ts +4 -2
- 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
|
|
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
|
|
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
|
-
|
|
9
|
+
The major changes are:
|
|
10
10
|
|
|
11
|
-
|
|
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
|
|
23
|
+
## Version 2 to version 3
|
|
14
24
|
|
|
15
|
-
|
|
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.
|
|
20
|
-
2.
|
|
21
|
-
3.
|
|
22
|
-
4.
|
|
23
|
-
5.
|
|
24
|
-
6.
|
|
25
|
-
7.
|
|
26
|
-
8.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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": "
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
}
|
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
|
}
|