machine-bridge-mcp 1.0.2 → 1.0.4
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 +15 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +7 -1
- package/SECURITY.md +1 -1
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +1 -0
- package/docs/AUDIT.md +10 -1
- package/docs/ENGINEERING.md +4 -4
- package/docs/OPERATIONS.md +2 -0
- package/docs/PROJECT_STANDARDS.md +3 -3
- package/docs/TESTING.md +1 -1
- package/mbm +1 -1
- package/package.json +3 -2
- package/scripts/github-release.mjs +33 -23
- package/scripts/prepare-pinned-npm.mjs +97 -0
- package/scripts/release-ci.mjs +9 -4
- package/scripts/sarif-security-gate.mjs +12 -10
- package/src/local/cli.mjs +10 -4
- package/src/local/process-execution.mjs +14 -1
- package/src/local/state.mjs +11 -1
- package/src/worker/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.4 - 2026-07-14
|
|
4
|
+
|
|
5
|
+
### Windows first-run workspace
|
|
6
|
+
|
|
7
|
+
- Keep the first interactive workspace question on Windows, but default it to `%USERPROFILE%\MachineBridge` instead of the Command Prompt current directory. Pressing Enter creates, canonicalizes, and remembers that folder, so users do not need `cd` or directory knowledge and an elevated prompt cannot accidentally select `C:\Windows\System32`.
|
|
8
|
+
- Preserve explicit `--workspace` semantics for automation, while allowing an interactively entered Windows workspace folder to be created when it does not yet exist. The installed zero-argument startup test now verifies the remembered platform default.
|
|
9
|
+
|
|
10
|
+
## 1.0.3 - 2026-07-14
|
|
11
|
+
|
|
12
|
+
### Code-scanning and supply-chain integrity
|
|
13
|
+
|
|
14
|
+
- Fix the SARIF gate so results with omitted rule metadata fail closed instead of being silently classified as non-security. Remove stale broad CodeQL exceptions, harden the generic process boundary with a fixed-option non-shell `child_process.spawn` wrapper and behavior regression, and retain only one exact, expiring false-positive record for that intentional authority boundary.
|
|
15
|
+
- Gate OpenSSF Scorecard SARIF before upload. Replace mutable npm bootstrap commands with an exact npm 12.0.1 tarball plus pinned SHA-512 verification, make the source wrapper use `npm ci`, and convert randomized security properties to a deterministic `fast-check` `.js` suite recognized by the pinned Scorecard scanner, and separate the signed Scorecard analysis job from the failing SARIF gate job required by the action's workflow-verification rules.
|
|
16
|
+
- Record only four expiring Scorecard governance/time exceptions that cannot be repaired by source code alone, reject remediable pinning/fuzzing exceptions, and require exact-commit CI, CodeQL, Governance, and Scorecard success before creating a source release.
|
|
17
|
+
|
|
3
18
|
## 1.0.2 - 2026-07-14
|
|
4
19
|
|
|
5
20
|
### Deadline and architecture integrity
|
package/CONTRIBUTING.md
CHANGED
|
@@ -26,7 +26,7 @@ Before the reviewed code is pushed to `main`:
|
|
|
26
26
|
4. inspect the complete diff and the npm package manifest;
|
|
27
27
|
5. commit and push the reviewed change to GitHub.
|
|
28
28
|
|
|
29
|
-
After all required pull-request checks pass, repository automation completes the source release under [AGENTS.md](AGENTS.md): squash-merge through local `gh`, verify the exact `main` push CI, create the annotated version tag, and create or update the matching final GitHub Release. The release operator separately publishes the same version to npm and performs any live machine update. Automation must not publish, deprecate, or unpublish npm packages; install the CLI globally; deploy the Worker; rotate credentials; mutate live deployment state; or start/stop/install/remove the daemon or autostart service without explicit user authorization.
|
|
29
|
+
After all required pull-request checks pass, repository automation completes the source release under [AGENTS.md](AGENTS.md): squash-merge through local `gh`, verify the exact `main` push CI, CodeQL, Governance, and Scorecard runs, create the annotated version tag, and create or update the matching final GitHub Release. The release operator separately publishes the same version to npm and performs any live machine update. Automation must not publish, deprecate, or unpublish npm packages; install the CLI globally; deploy the Worker; rotate credentials; mutate live deployment state; or start/stop/install/remove the daemon or autostart service without explicit user authorization.
|
|
30
30
|
|
|
31
31
|
After npm publication, the standard machine update is:
|
|
32
32
|
|
package/README.md
CHANGED
|
@@ -97,7 +97,13 @@ npm install
|
|
|
97
97
|
|
|
98
98
|
## Remote MCP for ChatGPT
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
After a global installation, `machine-mcp` can be launched from any Command Prompt or terminal; the package installation directory is not the workspace. On the first interactive Windows start, the CLI asks:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
Workspace folder [C:\Users\Alice\MachineBridge] (press Enter to use the default):
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Press Enter to create and remember `%USERPROFILE%\MachineBridge`. No `cd` command or directory selection is required. Enter another folder when desired; an interactively selected Windows folder is created if it does not yet exist. Later starts reuse the remembered workspace. Advanced users and automation may still select an existing workspace explicitly:
|
|
101
107
|
|
|
102
108
|
```sh
|
|
103
109
|
machine-mcp --workspace /path/to/project
|
package/SECURITY.md
CHANGED
|
@@ -23,7 +23,7 @@ Tests, examples, documentation, release notes, and package metadata are publicat
|
|
|
23
23
|
|
|
24
24
|
Removing a value from the current branch does not remove it from Git history, caches, forks, or an already published npm package. Rotate any exposed credential immediately and coordinate destructive history rewriting separately when its risk is justified.
|
|
25
25
|
|
|
26
|
-
CodeQL
|
|
26
|
+
CodeQL and OpenSSF Scorecard are enforced as gates, not merely uploaded as advisory output. The shared SARIF gate rejects every security result and fails closed when a result omits the rule metadata needed to prove it non-security. Any exception requires an exact rule/path entry, a substantive rationale, and a non-expired review date; rule-wide suppression is not acceptable. CodeQL has one exact, expiring accepted result for the authorized non-shell direct-process boundary; its fixed option set and metacharacter behavior are tested. Scorecard exceptions are limited to documented governance or time-dependent conditions that repository code cannot truthfully repair, while remediable dependency-pinning and fuzzing findings remain release-blocking.
|
|
27
27
|
|
|
28
28
|
## Core trust model
|
|
29
29
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Machine Bridge Browser",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.4",
|
|
5
5
|
"description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"action": {
|
|
31
31
|
"default_title": "Machine Bridge Browser"
|
|
32
32
|
},
|
|
33
|
-
"version_name": "1.0.
|
|
33
|
+
"version_name": "1.0.4"
|
|
34
34
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -19,6 +19,7 @@ No transport is treated as a sandbox. Both transports invoke the same local runt
|
|
|
19
19
|
### CLI and state layer
|
|
20
20
|
|
|
21
21
|
The CLI canonicalizes workspaces, resolves policy profiles, maintains per-workspace state and credentials, serializes startup/deploy/rotation with process-identity locks, deploys the Worker, installs optional platform-native autostart, and starts either remote daemon or stdio mode.
|
|
22
|
+
On a first interactive Windows start, workspace selection remains explicit but offers `%USERPROFILE%\MachineBridge` as a dedicated default; accepting it creates and remembers the folder instead of inheriting the shell current directory. Non-Windows interactive starts retain the current-directory default, and explicit `--workspace` paths remain strict existing-path inputs.
|
|
22
23
|
|
|
23
24
|
A canonical workspace receives an independent profile, Worker name, secret set, resource registry, managed-job directory, daemon/startup locks, and state file. State schema version 6 records named accounts, policy origin/revision, and local resource metadata in addition to the capability fields. `exclusive-file.mjs` owns complete-before-visible exclusive claims and flushed atomic replacement. `process-identity.mjs` owns PID liveness, process start-time comparison, bounded command-line inspection, and PID-reuse classification. `service-lifecycle.mjs` owns the fail-closed stop-daemons-before-remove state machine shared by service removal and full uninstall.
|
|
24
25
|
|
package/docs/AUDIT.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# Engineering and security audit
|
|
2
2
|
|
|
3
|
-
This document records the cross-cutting audit initiated for version 0.12.0, its later architecture, browser, lifecycle, authorization, release-gate, and version 1 follow-ups,
|
|
3
|
+
This document records the cross-cutting audit initiated for version 0.12.0, its later architecture, browser, lifecycle, authorization, release-gate, and version 1 follow-ups, including the version 1.0.2 elapsed-time review and the version 1.0.3 code-scanning inventory correction. It complements, but does not replace, the continuously enforced contracts in `SECURITY.md`, `docs/ENGINEERING.md`, and the test suite.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## 2026-07-14 version 1.0.3 code-scanning and supply-chain gate audit
|
|
7
|
+
|
|
8
|
+
After npm 1.0.2 and the live Worker/daemon upgrade succeeded, GitHub still reported nine open code-scanning alerts on the exact released `main` commit: one current CodeQL command-boundary result and eight OpenSSF Scorecard results. This disproved the earlier assumption that green CodeQL and Scorecard jobs implied a clean code-scanning inventory. The CodeQL analysis API reported one result, while the repository gate reported zero because the SARIF emitted to the post-analysis step omitted `tool.driver.rules`; the gate treated missing rule metadata as proof that a result was non-security. Scorecard had no gate at all and uploaded every low-score result while the workflow remained successful.
|
|
9
|
+
|
|
10
|
+
The process boundary now routes through an isolated `node:child_process.spawn` call that copies only four allowed options, enforces final `shell: false`, and has a production-path metacharacter regression. CodeQL 2.26.0 nevertheless classifies the environment-derived executable path as a shell-command sink even though no shell interprets either the executable or argv. After reproducing the complete flow and eliminating ambiguous option propagation, the repository retains one exact rule/path exception with a short expiry rather than broad command-execution suppressions. The SARIF gate now fails closed for every result whose rule metadata is absent. The same gate runs on Scorecard output before upload. Four remediable Scorecard findings are addressed: source bootstrap uses `npm ci`; CI obtains npm 12.0.1 from an exact registry URL, rejects redirects, verifies a pinned SHA-512 SRI, and exposes the verified CLI without a mutable global install; and the randomized security suite now uses deterministic `fast-check` properties in a `.js` file, matching the pinned Scorecard v5.3.0 detector's `*.js`/`*.jsx` scope. The Scorecard action remains in a signed analysis job containing only pinned `uses` steps; a dependent gate job downloads its SARIF, enforces the accepted inventory, and uploads the reviewed result.
|
|
11
|
+
|
|
12
|
+
Four Scorecard findings remain explicitly reviewed rather than hidden: the repository is younger than ninety days, historical commits predate mandatory CodeQL, only one human maintainer is available for review, and OpenSSF Best Practices enrollment requires external maintainer attestations. Their exact rule/path exceptions carry short expiry dates, while `PinnedDependenciesID` and `FuzzingID` are forbidden from the accepted inventory. Release orchestration now verifies successful push-triggered CI, CodeQL, Governance, and Scorecard runs for the exact `main` commit instead of checking only the CI workflow. This closes the semantic gap between “a workflow ran” and “the security/governance evidence required for release passed.”
|
|
4
13
|
|
|
5
14
|
|
|
6
15
|
## 2026-07-14 version 1.0.2 elapsed-time and documentation-integrity audit
|
package/docs/ENGINEERING.md
CHANGED
|
@@ -9,7 +9,7 @@ This document records project-wide decisions that must survive individual fixes,
|
|
|
9
9
|
3. **Machine Bridge authority and host authority are separate.** `full` removes Machine Bridge's own policy, path, shell, and environment restrictions. It cannot override an MCP host, connector gateway, operating system, endpoint-security product, cloud IAM, remote authentication, or `sudo`.
|
|
10
10
|
4. **Publication surfaces contain no real environment metadata.** Source, tests, fixtures, examples, release notes, filenames, package contents, tags, and release assets use synthetic identifiers and reserved example domains.
|
|
11
11
|
5. **Secrets are never operational log data.** Tool arguments, command text, stdin, stdout, stderr, file content, OAuth bodies, credentials, and local resource values are not logged.
|
|
12
|
-
6. **A release is one version with successful cross-platform evidence.** Package metadata, Worker version, browser-extension version/name, Git tag, GitHub Release, npm version, documentation, and deployed health version must agree, and the exact `origin/main` commit must have
|
|
12
|
+
6. **A release is one version with successful cross-platform evidence.** Package metadata, Worker version, browser-extension version/name, Git tag, GitHub Release, npm version, documentation, and deployed health version must agree, and the exact `origin/main` commit must have completed successful push-triggered CI, CodeQL, Governance, and OpenSSF Scorecard runs before a tag or release is created.
|
|
13
13
|
7. **Generic local automation is structured, not arbitrary evaluation.** Browser/application features may expose broad user authority under canonical `full`, but must not accept caller-provided JavaScript, AppleScript, JXA, or extension code.
|
|
14
14
|
8. **Daily-browser integration uses the existing profile.** The supported primary browser path is the packaged authenticated extension and machine-level loopback broker, preserving current tabs/login state; a separate automation profile is not an equivalent replacement.
|
|
15
15
|
9. **Pairing and resource secrets are not conversation or log data.** Tokens and injected local-resource values must not be returned, embedded in URLs, or written to operational logs.
|
|
@@ -17,7 +17,7 @@ This document records project-wide decisions that must survive individual fixes,
|
|
|
17
17
|
11. **Service and state removal are fail-closed state machines.** Stop the platform provider and every verified daemon before removing definitions or recursive state. An unreadable lock, failed stop, active job, or ambiguous identity retains state for diagnosis.
|
|
18
18
|
12. **Read failure is not empty state.** Permission, type, symbolic-link, size, encoding, and I/O errors must propagate. Corrupt backup/reconstruction applies only after a successful read proves that JSON content is invalid.
|
|
19
19
|
13. **The public protocol contract is current-only.** Shared metadata advertises only the current MCP protocol version. Compatibility code for obsolete protocol dates, lock formats, or state schemas is not retained in the final runtime; upgrade safety comes from explicit version negotiation, fail-closed state validation, and bounded operator convergence.
|
|
20
|
-
14. **Security analysis is a failing gate.** CodeQL execution alone is not success. Generated SARIF must contain no unaccepted security
|
|
20
|
+
14. **Security analysis is a failing gate.** CodeQL or Scorecard execution alone is not success. Generated SARIF must contain no unaccepted result, and missing rule metadata fails closed rather than being interpreted as non-security. An intentional or externally constrained finding requires an exact rule/path record with a substantive rationale and an expiry date.
|
|
21
21
|
|
|
22
22
|
A proposed change that conflicts with an invariant requires an explicit owner decision and corresponding documentation update. It must not be hidden inside an unrelated refactor.
|
|
23
23
|
|
|
@@ -127,7 +127,7 @@ Every defect fix includes a regression test that fails for the original reason.
|
|
|
127
127
|
The required matrix includes:
|
|
128
128
|
|
|
129
129
|
- Linux, macOS, and Windows on the pinned Node/npm baseline;
|
|
130
|
-
- current-tree and reachable-history privacy gates, release-impact enforcement, and exact-commit
|
|
130
|
+
- current-tree and reachable-history privacy gates, release-impact enforcement, and exact-commit CI/CodeQL/Governance/Scorecard release gating;
|
|
131
131
|
- package-manifest and sensitive-artifact inspection;
|
|
132
132
|
- generated type checks and recursively discovered JavaScript/shell syntax checks;
|
|
133
133
|
- static undefined-identifier/redeclaration checks over all production JavaScript, tests, scripts, and browser-extension code;
|
|
@@ -185,7 +185,7 @@ A thorough review asks:
|
|
|
185
185
|
- Are persistent files atomic, owner-only, size-bounded, and symlink-aware?
|
|
186
186
|
- Can browser/app automation be expressed without arbitrary evaluation, and are pairing/resource values absent from results and logs?
|
|
187
187
|
- Can a stale PID, stale socket, duplicate request, partial write, or ambiguous remote response violate integrity?
|
|
188
|
-
- Are package, CI, Worker, service, and release behavior tested on every supported platform, and does publication verify
|
|
188
|
+
- Are package, CI, Worker, service, and release behavior tested on every supported platform, and does publication verify every required exact-commit push workflow?
|
|
189
189
|
- Does the complete diff contain any real identifier, path, host, account, or credential-shaped value?
|
|
190
190
|
- Does the change require a new npm version and deployment?
|
|
191
191
|
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -99,6 +99,8 @@ machine-mcp --verbose
|
|
|
99
99
|
|
|
100
100
|
`Unknown cli config "--allow-scripts"` proves the package installation ran under npm 11 or older. `Invalid property "node"` or `Invalid property "devEngines.node"` means npm parsed an outdated `devEngines` object; inspect the npm debug log to identify its source rather than assuming it belongs to Machine Bridge. The published package declares both Node.js 26 and npm 12 in `engines`, and `machine-mcp doctor` checks both active versions. Keep `--omit=optional` in the install command. Without it npm may resolve optional `fsevents` and warn that its install script was not included in `allowScripts`; Machine Bridge does not require that development-time watcher at runtime.
|
|
101
101
|
|
|
102
|
+
After global installation, Windows users may open any `cmd.exe` window and run `machine-mcp`; they do not need to navigate to a project or package directory. On the first interactive start, the CLI asks for a workspace folder and displays `%USERPROFILE%\MachineBridge` as the default. Pressing Enter creates and remembers that folder. This avoids inheriting an arbitrary Command Prompt current directory such as `C:\Windows\System32`. Typing another folder in the prompt creates that folder when necessary. Explicit `--workspace PATH` remains a strict automation interface and requires an existing path.
|
|
103
|
+
|
|
102
104
|
## Version 1 upgrade convergence
|
|
103
105
|
|
|
104
106
|
Version 1 advertises only MCP protocol `2025-11-25`. Upgrade the MCP client/host if it cannot negotiate that version; Machine Bridge does not retain an obsolete protocol dispatcher. The current local state schema is unchanged from the final 0.18.x release, so do not delete the state root merely to upgrade.
|
|
@@ -114,9 +114,9 @@ Flaky tests are defects. A retry may diagnose environmental instability but may
|
|
|
114
114
|
|
|
115
115
|
- GitHub workflow permissions default to read-only and are expanded per job only when required.
|
|
116
116
|
- Third-party Actions are pinned to immutable commit SHAs and reviewed when Dependabot updates them.
|
|
117
|
-
- npm dependencies use exact versions and a committed lockfile. Registry signatures and attestations are verified in CI.
|
|
118
|
-
- Dependency review blocks newly introduced vulnerable dependencies. CodeQL performs JavaScript/TypeScript and workflow analysis. OpenSSF Scorecard
|
|
119
|
-
- CI generates and validates a CycloneDX SBOM. Release artifacts must be reproducible from a reviewed commit and tied to successful exact-commit CI evidence.
|
|
117
|
+
- npm dependencies use exact versions and a committed lockfile. Source bootstrap uses `npm ci`; the CI npm baseline itself is downloaded from an exact URL and verified against a pinned SHA-512 SRI before use. Registry signatures and attestations are verified in CI.
|
|
118
|
+
- Dependency review blocks newly introduced vulnerable dependencies. CodeQL performs JavaScript/TypeScript and workflow analysis. OpenSSF Scorecard audits supply-chain posture, and both SARIF streams are failing gates with exact, expiring exceptions rather than advisory-only uploads.
|
|
119
|
+
- CI generates and validates a CycloneDX SBOM. Release artifacts must be reproducible from a reviewed commit and tied to successful exact-commit CI, CodeQL, Governance, and Scorecard evidence.
|
|
120
120
|
- Secret scanning and push protection are enabled. Repository examples use synthetic identities and reserved domains; reachable history is scanned before release.
|
|
121
121
|
- Long-lived publication tokens should be replaced by npm trusted publishing with GitHub OIDC. Until that external registry configuration is completed, release credentials remain an explicit operator responsibility and must never be stored in the repository.
|
|
122
122
|
- Security reports follow [SECURITY.md](../SECURITY.md), not public issue templates.
|
package/docs/TESTING.md
CHANGED
|
@@ -89,7 +89,7 @@ npm run version:check
|
|
|
89
89
|
npm run release-impact:check
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
GitHub Actions executes the main suite on Linux, macOS, and Windows using the pinned Node 26/npm 12 baseline. Because Node 26 currently bundles npm 11, CI
|
|
92
|
+
GitHub Actions executes the main suite on Linux, macOS, and Windows using the pinned Node 26/npm 12 baseline. Because Node 26 currently bundles npm 11, CI downloads the exact npm 12.0.1 tarball from the canonical registry, rejects redirects, verifies its recorded SHA-512 SRI, and exposes that verified CLI through `GITHUB_PATH` before any project-local npm command can trigger strict `devEngines`. Checkout fetches version tags so the release-impact gate can compare the branch with the latest release. A separate package-audit job scans reachable Git history, audits both the complete dependency graph and the production-only graph, verifies registry signatures and attestations, validates a CycloneDX SBOM written under the runner temporary directory, exercises the documented isolated global installation, then performs a dry-run package build. Separate pinned workflows validate governance titles, review dependency changes, run CodeQL over JavaScript/TypeScript and GitHub Actions, and run OpenSSF Scorecard. The SARIF gate fails closed when rule metadata is absent, CodeQL permits only one exact, expiring non-shell process-boundary result, and Scorecard permits only exact reviewed governance/time-dependent findings; new dependency-pinning, fuzzing, or other supply-chain results fail before upload. Scorecard's signed analysis job contains only pinned `uses` steps, while a dependent gate job downloads the SARIF, enforces the accepted inventory, and uploads it to code scanning. Security property tests use a `.js` `fast-check` suite with deterministic seeds because the pinned Scorecard scanner recognizes JavaScript property testing only in `*.js`/`*.jsx`. The installed zero-argument startup smoke test also verifies that Windows creates and remembers `%USERPROFILE%\MachineBridge` while other platforms retain the package-free current directory. The macOS matrix job also runs the installation smoke test because Wrangler's optional `fsevents` resolution is platform-specific. Third-party Actions are pinned to immutable 40-character commit SHAs; architecture tests reject movable tags, and Dependabot monitors both action and dependency updates. Source release creation additionally requires successful push-triggered CI, CodeQL, Governance, and Scorecard runs for the exact `main` commit.
|
|
93
93
|
|
|
94
94
|
## Test design rules
|
|
95
95
|
|
package/mbm
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"tool-docs:check": "node scripts/generate-tool-reference.mjs --check",
|
|
103
103
|
"account:test": "node tests/account-admin-test.mjs",
|
|
104
104
|
"sarif-security:test": "node tests/sarif-security-gate-test.mjs",
|
|
105
|
-
"security-properties:test": "node tests/security-properties-test.
|
|
105
|
+
"security-properties:test": "node tests/security-properties-test.js",
|
|
106
106
|
"lint": "eslint eslint.config.mjs bin src/local scripts tests browser-extension",
|
|
107
107
|
"lint:test": "node tests/lint-gate-test.mjs",
|
|
108
108
|
"shell:test": "node tests/shell-test.mjs",
|
|
@@ -117,6 +117,7 @@
|
|
|
117
117
|
"devDependencies": {
|
|
118
118
|
"@types/node": "26.1.1",
|
|
119
119
|
"eslint": "10.7.0",
|
|
120
|
+
"fast-check": "4.9.0",
|
|
120
121
|
"globals": "17.7.0",
|
|
121
122
|
"typescript": "7.0.2"
|
|
122
123
|
},
|
|
@@ -10,7 +10,7 @@ import { tmpdir } from "node:os";
|
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { runNetworkCommand } from "./network-retry.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import { requireSuccessfulWorkflowRun } from "./release-ci.mjs";
|
|
14
14
|
import { tagSyncError } from "./release-state.mjs";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
@@ -126,28 +126,38 @@ function remoteTagCommit(tag) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
function assertSuccessfulCi(head) {
|
|
129
|
-
const
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
".github/workflows/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
129
|
+
const required = [
|
|
130
|
+
[".github/workflows/ci.yml", "CI"],
|
|
131
|
+
[".github/workflows/codeql.yml", "CodeQL"],
|
|
132
|
+
[".github/workflows/governance.yml", "Governance"],
|
|
133
|
+
[".github/workflows/scorecard.yml", "OpenSSF Scorecard"],
|
|
134
|
+
];
|
|
135
|
+
const verified = [];
|
|
136
|
+
for (const [workflow, name] of required) {
|
|
137
|
+
const text = outputNetwork("gh", [
|
|
138
|
+
"run",
|
|
139
|
+
"list",
|
|
140
|
+
"--workflow",
|
|
141
|
+
workflow,
|
|
142
|
+
"--commit",
|
|
143
|
+
head,
|
|
144
|
+
"--event",
|
|
145
|
+
"push",
|
|
146
|
+
"--limit",
|
|
147
|
+
"20",
|
|
148
|
+
"--json",
|
|
149
|
+
"databaseId,status,conclusion,headSha,event,createdAt,url",
|
|
150
|
+
]);
|
|
151
|
+
let runs;
|
|
152
|
+
try { runs = JSON.parse(text); }
|
|
153
|
+
catch { fail(`GitHub Actions did not return valid JSON for ${name}`); }
|
|
154
|
+
let run;
|
|
155
|
+
try { run = requireSuccessfulWorkflowRun(runs, head, name); }
|
|
156
|
+
catch (error) { fail(String(error?.message || error)); }
|
|
157
|
+
console.log(`GitHub Actions ${name} succeeded for ${head} (run ${run.databaseId}).`);
|
|
158
|
+
verified.push(run);
|
|
159
|
+
}
|
|
160
|
+
return verified;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
function releaseInfo(tag) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const NPM_VERSION = "12.0.1";
|
|
8
|
+
const NPM_TARBALL_URL = "https://registry.npmjs.org/npm/-/npm-12.0.1.tgz";
|
|
9
|
+
const NPM_TARBALL_INTEGRITY = "sha512-L5T9i/YAQWQWqTS/xZxJkei/9zcu99hCeE4qi41IyBVV7mRQad3qc2JfuOktwmH+qwGI/V2rbCL+/UYxb1+RQA==";
|
|
10
|
+
const MAX_TARBALL_BYTES = 20 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
const githubPath = process.env.GITHUB_PATH;
|
|
13
|
+
if (!githubPath) throw new Error("GITHUB_PATH is required; this bootstrap is intended for GitHub Actions");
|
|
14
|
+
|
|
15
|
+
const root = mkdtempSync(join(process.env.RUNNER_TEMP || tmpdir(), "mbm-npm-bootstrap-"));
|
|
16
|
+
try {
|
|
17
|
+
const archive = join(root, `npm-${NPM_VERSION}.tgz`);
|
|
18
|
+
const extracted = join(root, "extracted");
|
|
19
|
+
const bin = join(root, "bin");
|
|
20
|
+
mkdirSync(extracted, { recursive: true });
|
|
21
|
+
mkdirSync(bin, { recursive: true });
|
|
22
|
+
|
|
23
|
+
const response = await fetch(NPM_TARBALL_URL, { redirect: "error" });
|
|
24
|
+
if (!response.ok) throw new Error(`failed to download pinned npm tarball: HTTP ${response.status}`);
|
|
25
|
+
const declaredBytes = Number(response.headers.get("content-length"));
|
|
26
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > MAX_TARBALL_BYTES) throw new Error("pinned npm tarball exceeds 20 MiB");
|
|
27
|
+
const bytes = await readBoundedBody(response, MAX_TARBALL_BYTES);
|
|
28
|
+
verifyIntegrity(bytes, NPM_TARBALL_INTEGRITY);
|
|
29
|
+
writeFileSync(archive, bytes, { mode: 0o600 });
|
|
30
|
+
|
|
31
|
+
run("tar", ["-xzf", archive, "-C", extracted]);
|
|
32
|
+
const cli = join(extracted, "package", "bin", "npm-cli.js");
|
|
33
|
+
const version = run(process.execPath, [cli, "--version"], { capture: true }).trim();
|
|
34
|
+
if (version !== NPM_VERSION) throw new Error(`pinned npm tarball reported ${version}, expected ${NPM_VERSION}`);
|
|
35
|
+
|
|
36
|
+
const posixWrapper = join(bin, "npm");
|
|
37
|
+
writeFileSync(posixWrapper, `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(cli)} "$@"\n`, { mode: 0o755 });
|
|
38
|
+
chmodSync(posixWrapper, 0o755);
|
|
39
|
+
writeFileSync(join(bin, "npm.cmd"), `@echo off\r\n"${cmdQuote(process.execPath)}" "${cmdQuote(cli)}" %*\r\n`);
|
|
40
|
+
writeFileSync(githubPath, `${bin}\n`, { flag: "a" });
|
|
41
|
+
console.log(`Prepared integrity-verified npm ${NPM_VERSION} at ${bin}`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
rmSync(root, { recursive: true, force: true });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async function readBoundedBody(response, maximumBytes) {
|
|
49
|
+
if (!response.body) throw new Error("pinned npm tarball response has no body");
|
|
50
|
+
const reader = response.body.getReader();
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let total = 0;
|
|
53
|
+
try {
|
|
54
|
+
while (true) {
|
|
55
|
+
const { done, value } = await reader.read();
|
|
56
|
+
if (done) break;
|
|
57
|
+
total += value.byteLength;
|
|
58
|
+
if (total > maximumBytes) throw new Error("pinned npm tarball exceeds 20 MiB");
|
|
59
|
+
chunks.push(Buffer.from(value));
|
|
60
|
+
}
|
|
61
|
+
} finally {
|
|
62
|
+
reader.releaseLock();
|
|
63
|
+
}
|
|
64
|
+
return Buffer.concat(chunks, total);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function verifyIntegrity(bytes, integrity) {
|
|
68
|
+
const match = /^sha512-(.+)$/.exec(integrity);
|
|
69
|
+
if (!match) throw new Error("pinned npm integrity must be SHA-512 SRI");
|
|
70
|
+
const actual = createHash("sha512").update(bytes).digest();
|
|
71
|
+
const expected = Buffer.from(match[1], "base64");
|
|
72
|
+
if (actual.length !== expected.length || !actual.equals(expected)) throw new Error("pinned npm tarball failed SHA-512 verification");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function run(command, args, options = {}) {
|
|
76
|
+
const result = spawnSync(command, args, {
|
|
77
|
+
encoding: options.capture ? "utf8" : undefined,
|
|
78
|
+
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
79
|
+
shell: false,
|
|
80
|
+
});
|
|
81
|
+
if (result.error) throw result.error;
|
|
82
|
+
if (result.status !== 0) {
|
|
83
|
+
const detail = options.capture ? String(result.stderr || result.stdout || "").trim() : "";
|
|
84
|
+
throw new Error(`${command} exited ${result.status}${detail ? `: ${detail}` : ""}`);
|
|
85
|
+
}
|
|
86
|
+
return options.capture ? String(result.stdout || "") : "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function shellQuote(value) {
|
|
90
|
+
return `'${String(value).replaceAll("'", `'\\''`)}'`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cmdQuote(value) {
|
|
94
|
+
const text = String(value);
|
|
95
|
+
if (/[\0\r\n"%&|<>^!]/.test(text)) throw new Error("Windows wrapper path contains an unsupported character");
|
|
96
|
+
return text;
|
|
97
|
+
}
|
package/scripts/release-ci.mjs
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function requireSuccessfulWorkflowRun(runs, head, workflowName = "CI") {
|
|
2
2
|
if (!Array.isArray(runs)) throw new Error("GitHub Actions response is not an array");
|
|
3
3
|
if (!/^[0-9a-f]{40,64}$/i.test(String(head || ""))) throw new Error("release commit SHA is invalid");
|
|
4
|
+
const label = String(workflowName || "workflow");
|
|
4
5
|
const matching = runs
|
|
5
6
|
.filter((run) => run && run.headSha === head && run.event === "push")
|
|
6
7
|
.sort((left, right) => String(right.createdAt || "").localeCompare(String(left.createdAt || "")));
|
|
7
8
|
const run = matching[0];
|
|
8
9
|
if (!run) {
|
|
9
|
-
throw new Error(`no push-triggered
|
|
10
|
+
throw new Error(`no push-triggered ${label} run exists for release commit ${head}; wait for GitHub Actions to register the run and retry`);
|
|
10
11
|
}
|
|
11
12
|
if (run.status !== "completed") {
|
|
12
|
-
throw new Error(
|
|
13
|
+
throw new Error(`${label} run ${run.databaseId || "unknown"} for release commit ${head} is ${run.status || "unknown"}; wait for completion and retry`);
|
|
13
14
|
}
|
|
14
15
|
if (run.conclusion !== "success") {
|
|
15
|
-
throw new Error(
|
|
16
|
+
throw new Error(`${label} run ${run.databaseId || "unknown"} for release commit ${head} concluded ${run.conclusion || "unknown"}; fix or rerun the workflow before release`);
|
|
16
17
|
}
|
|
17
18
|
return run;
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
export function requireSuccessfulCiRun(runs, head) {
|
|
22
|
+
return requireSuccessfulWorkflowRun(runs, head, "CI");
|
|
23
|
+
}
|
|
@@ -12,7 +12,7 @@ if (!targets.length) throw new Error("usage: node scripts/sarif-security-gate.mj
|
|
|
12
12
|
|
|
13
13
|
const accepted = loadAcceptedFindings(allowlistPath);
|
|
14
14
|
const sarifFiles = targets.flatMap((target) => collectSarifFiles(resolve(target)));
|
|
15
|
-
if (!sarifFiles.length) throw new Error("
|
|
15
|
+
if (!sarifFiles.length) throw new Error("SARIF gate received no SARIF files");
|
|
16
16
|
|
|
17
17
|
const blocked = [];
|
|
18
18
|
const acknowledged = [];
|
|
@@ -22,28 +22,30 @@ for (const file of sarifFiles) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
for (const finding of acknowledged) {
|
|
25
|
-
process.stderr.write(`accepted
|
|
25
|
+
process.stderr.write(`accepted SARIF finding: ${finding.ruleId} at ${finding.path} (${finding.reason}; expires ${finding.expires})\n`);
|
|
26
26
|
}
|
|
27
27
|
if (blocked.length) {
|
|
28
28
|
for (const finding of blocked.slice(0, 100)) {
|
|
29
29
|
process.stderr.write(`${finding.path}:${finding.line}: ${finding.ruleId}: ${finding.message}\n`);
|
|
30
30
|
}
|
|
31
|
-
if (blocked.length > 100) process.stderr.write(`... ${blocked.length - 100} additional
|
|
32
|
-
throw new Error(`
|
|
31
|
+
if (blocked.length > 100) process.stderr.write(`... ${blocked.length - 100} additional SARIF findings omitted\n`);
|
|
32
|
+
throw new Error(`SARIF gate rejected ${blocked.length} unaccepted SARIF finding(s)`);
|
|
33
33
|
}
|
|
34
|
-
process.stderr.write(`
|
|
34
|
+
process.stderr.write(`SARIF gate ok (${sarifFiles.length} SARIF file(s); ${acknowledged.length} explicitly accepted finding(s))\n`);
|
|
35
35
|
|
|
36
36
|
function inspectRun(run, file, allowlist, blockedFindings, acceptedFindings) {
|
|
37
37
|
const rules = new Map();
|
|
38
38
|
for (const rule of run.tool?.driver?.rules || []) rules.set(rule.id, rule);
|
|
39
39
|
for (const result of run.results || []) {
|
|
40
|
-
const ruleId = String(result.ruleId || "");
|
|
41
|
-
const rule = rules.get(ruleId)
|
|
42
|
-
|
|
40
|
+
const ruleId = String(result.ruleId || "<unknown-rule>");
|
|
41
|
+
const rule = rules.get(ruleId);
|
|
42
|
+
// CodeQL's post-upload SARIF can omit rule metadata even when results exist.
|
|
43
|
+
// Missing metadata must fail closed because the result cannot be proven non-security.
|
|
44
|
+
if (rule && !isSecurityRule(rule)) continue;
|
|
43
45
|
const location = primaryLocation(result);
|
|
44
46
|
const path = normalizeSarifPath(location.path, run, file);
|
|
45
47
|
const line = Number(location.line) || 1;
|
|
46
|
-
const message = boundedMessage(result.message?.text || result.message?.markdown || rule
|
|
48
|
+
const message = boundedMessage(result.message?.text || result.message?.markdown || rule?.shortDescription?.text || "SARIF finding");
|
|
47
49
|
const exception = allowlist.get(`${ruleId}\0${path}`);
|
|
48
50
|
if (exception) acceptedFindings.push({ ruleId, path, ...exception });
|
|
49
51
|
else blockedFindings.push({ ruleId, path, line, message });
|
|
@@ -101,7 +103,7 @@ function loadAcceptedFindings(file) {
|
|
|
101
103
|
}
|
|
102
104
|
if (expires < today) throw new Error(`accepted CodeQL finding expired: ${ruleId} at ${path}`);
|
|
103
105
|
const key = `${ruleId}\0${path}`;
|
|
104
|
-
if (entries.has(key)) throw new Error(`duplicate accepted
|
|
106
|
+
if (entries.has(key)) throw new Error(`duplicate accepted SARIF finding: ${ruleId} at ${path}`);
|
|
105
107
|
entries.set(key, { reason, expires });
|
|
106
108
|
}
|
|
107
109
|
return entries;
|
package/src/local/cli.mjs
CHANGED
|
@@ -26,8 +26,10 @@ import {
|
|
|
26
26
|
acquireStartupLockWithWait,
|
|
27
27
|
appName,
|
|
28
28
|
daemonLockPathForState,
|
|
29
|
+
defaultFirstRunWorkspace,
|
|
29
30
|
defaultStateRoot,
|
|
30
31
|
ensureWorkerSecrets,
|
|
32
|
+
ensureWorkspaceDirectory,
|
|
31
33
|
expandHome,
|
|
32
34
|
loadGlobalConfig,
|
|
33
35
|
loadState,
|
|
@@ -95,15 +97,19 @@ async function chooseWorkspace(args, { promptOnFirstRun, save, allowPositional =
|
|
|
95
97
|
const remembered = selectedWorkspace(stateRoot);
|
|
96
98
|
if (remembered) return resolveWorkspace(remembered);
|
|
97
99
|
|
|
98
|
-
const fallback = process.cwd();
|
|
100
|
+
const fallback = promptOnFirstRun ? defaultFirstRunWorkspace() : process.cwd();
|
|
99
101
|
if (!promptOnFirstRun || !process.stdin.isTTY) {
|
|
100
|
-
const workspace =
|
|
102
|
+
const workspace = promptOnFirstRun && process.platform === "win32"
|
|
103
|
+
? ensureWorkspaceDirectory(fallback)
|
|
104
|
+
: resolveWorkspace(fallback);
|
|
101
105
|
if (save) setSelectedWorkspace(workspace, stateRoot);
|
|
102
106
|
return workspace;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
const answer = await ask(`Workspace
|
|
106
|
-
const workspace =
|
|
109
|
+
const answer = await ask(`Workspace folder [${fallback}] (press Enter to use the default): `);
|
|
110
|
+
const workspace = process.platform === "win32"
|
|
111
|
+
? ensureWorkspaceDirectory(answer.trim() || fallback)
|
|
112
|
+
: resolveWorkspace(answer.trim() || fallback);
|
|
107
113
|
if (save) setSelectedWorkspace(workspace, stateRoot);
|
|
108
114
|
return workspace;
|
|
109
115
|
}
|
|
@@ -9,8 +9,20 @@ import { clampInteger } from "./numbers.mjs";
|
|
|
9
9
|
const MAX_STDIN_BYTES = 1024 * 1024;
|
|
10
10
|
const DEFAULT_OUTPUT_BYTES = 512 * 1024;
|
|
11
11
|
|
|
12
|
+
function spawnDirectProcess(command, args, options) {
|
|
13
|
+
// Keep the production child_process API call structurally separate from the
|
|
14
|
+
// injectable test seam and enforce non-shell execution at the final boundary.
|
|
15
|
+
return spawn(command, args, {
|
|
16
|
+
cwd: options.cwd,
|
|
17
|
+
env: options.env,
|
|
18
|
+
detached: options.detached,
|
|
19
|
+
windowsHide: options.windowsHide,
|
|
20
|
+
shell: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export class ProcessExecutionService {
|
|
13
|
-
constructor({ workspace, policy, policyGate, runtimeDir, processTracker, resolveExistingPath, resolveLocalCommand, displayPath, throwIfCancelled, spawnProcess =
|
|
25
|
+
constructor({ workspace, policy, policyGate, runtimeDir, processTracker, resolveExistingPath, resolveLocalCommand, displayPath, throwIfCancelled, spawnProcess = spawnDirectProcess, terminateProcess = terminateProcessTreeWithEscalation }) {
|
|
14
26
|
this.workspace = workspace;
|
|
15
27
|
this.policy = policy;
|
|
16
28
|
this.policyGate = policyGate;
|
|
@@ -80,6 +92,7 @@ export class ProcessExecutionService {
|
|
|
80
92
|
env: executionEnv(this.workspace, { fullEnv: this.policy.minimalEnv === false, runtimeDir: this.runtimeDir }),
|
|
81
93
|
detached: process.platform !== "win32",
|
|
82
94
|
windowsHide: true,
|
|
95
|
+
shell: false,
|
|
83
96
|
});
|
|
84
97
|
} catch (error) {
|
|
85
98
|
rejectPromise(error);
|
package/src/local/state.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync, lstatSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -35,6 +35,16 @@ export function resolveWorkspace(input = process.cwd()) {
|
|
|
35
35
|
return realpathSync.native ? realpathSync.native(resolved) : realpathSync(resolved);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function defaultFirstRunWorkspace({ platform = process.platform, home = os.homedir(), cwd = process.cwd() } = {}) {
|
|
39
|
+
return path.resolve(platform === "win32" ? path.join(home, "MachineBridge") : cwd);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ensureWorkspaceDirectory(input) {
|
|
43
|
+
const requested = path.resolve(expandHome(input));
|
|
44
|
+
mkdirSync(requested, { recursive: true });
|
|
45
|
+
return resolveWorkspace(requested);
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
export function defaultStateRoot() {
|
|
39
49
|
if (process.platform === "win32") {
|
|
40
50
|
const base = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
package/src/worker/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "./http";
|
|
23
23
|
|
|
24
24
|
const SERVER_NAME = String(serverMetadata.name);
|
|
25
|
-
const SERVER_VERSION = "1.0.
|
|
25
|
+
const SERVER_VERSION = "1.0.4";
|
|
26
26
|
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
27
27
|
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
28
28
|
const JSONRPC_VERSION = "2.0";
|