machine-bridge-mcp 1.0.2 → 1.0.3

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.3 - 2026-07-14
4
+
5
+ ### Code-scanning and supply-chain integrity
6
+
7
+ - 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.
8
+ - 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.
9
+ - 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.
10
+
3
11
  ## 1.0.2 - 2026-07-14
4
12
 
5
13
  ### 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/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 is enforced as a security gate, not merely uploaded as advisory output. The workflow parses generated SARIF and rejects every security-tagged result unless `.github/codeql-accepted-findings.json` contains the exact rule and path, a substantive authority-boundary rationale, and a non-expired review date. Rule-wide suppression is not an acceptable substitute.
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.2",
4
+ "version": "1.0.3",
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.2"
33
+ "version_name": "1.0.3"
34
34
  }
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, plus the version 1.0.2 elapsed-time and documentation-integrity review. It complements, but does not replace, the continuously enforced contracts in `SECURITY.md`, `docs/ENGINEERING.md`, and the test suite.
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
@@ -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 a completed successful push-triggered GitHub Actions run before a tag or release is created.
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 result; an intentional authority boundary requires an exact rule/path record with a substantive rationale and an expiry date.
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 release-CI gating;
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 the exact successful push CI run?
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
 
@@ -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 periodically audits repository supply-chain posture.
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 explicitly disables setup-node's automatic package-manager cache and upgrades npm from the runner temporary directory 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, fail on unaccepted security-tagged SARIF results, and publish OpenSSF Scorecard results. 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.
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 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
@@ -4,6 +4,6 @@ DIR=$(CDPATH= cd "$(dirname "$0")" && pwd)
4
4
  cd "$DIR"
5
5
  if [ ! -d node_modules ] || [ ! -x node_modules/.bin/wrangler ]; then
6
6
  echo "Installing local dependencies..."
7
- npm install
7
+ npm ci
8
8
  fi
9
9
  exec node bin/machine-mcp.mjs "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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.mjs",
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 { requireSuccessfulCiRun } from "./release-ci.mjs";
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 text = outputNetwork("gh", [
130
- "run",
131
- "list",
132
- "--workflow",
133
- ".github/workflows/ci.yml",
134
- "--commit",
135
- head,
136
- "--event",
137
- "push",
138
- "--limit",
139
- "20",
140
- "--json",
141
- "databaseId,status,conclusion,headSha,event,createdAt,url",
142
- ]);
143
- let runs;
144
- try { runs = JSON.parse(text); }
145
- catch { fail("GitHub Actions did not return valid JSON"); }
146
- let run;
147
- try { run = requireSuccessfulCiRun(runs, head); }
148
- catch (error) { fail(String(error?.message || error)); }
149
- console.log(`GitHub Actions CI succeeded for ${head} (run ${run.databaseId}).`);
150
- return run;
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
+ }
@@ -1,18 +1,23 @@
1
- export function requireSuccessfulCiRun(runs, head) {
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 CI run exists for release commit ${head}; wait for GitHub Actions to register the run and retry`);
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(`CI run ${run.databaseId || "unknown"} for release commit ${head} is ${run.status || "unknown"}; wait for completion and retry`);
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(`CI run ${run.databaseId || "unknown"} for release commit ${head} concluded ${run.conclusion || "unknown"}; fix or rerun CI before release`);
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("CodeQL security gate received no SARIF files");
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 CodeQL finding: ${finding.ruleId} at ${finding.path} (${finding.reason}; expires ${finding.expires})\n`);
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 security findings omitted\n`);
32
- throw new Error(`CodeQL security gate rejected ${blocked.length} unaccepted security finding(s)`);
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(`CodeQL security gate ok (${sarifFiles.length} SARIF file(s); ${acknowledged.length} explicitly accepted finding(s))\n`);
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
- if (!isSecurityRule(rule)) continue;
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.shortDescription?.text || "security finding");
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 CodeQL finding: ${ruleId} at ${path}`);
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;
@@ -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 = spawn, terminateProcess = terminateProcessTreeWithEscalation }) {
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);
@@ -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.2";
25
+ const SERVER_VERSION = "1.0.3";
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";