Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ci/source-shape-test-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,11 @@
"test": "%s keeps the full plan in build processes but not final runtime environments (#5896)",
"category": "security"
},
{
"file": "test/mcp-tool-discovery-image-contract.test.ts",
"test": "pins reviewed packages and retains the production audit command (#8177)",
"category": "security"
},
{
"file": "test/mcp-openshell-workflow.test.ts",
"test": "keeps the setup docs aligned with the stable default",
Expand Down
15 changes: 8 additions & 7 deletions src/lib/onboard/managed-image-registry-fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,10 @@ afterEach(async () => {

describe("managed image registry transport", () => {
it("routes the default registry fetch through the bounded host proxy", async () => {
const tunnels: string[] = [];
const proxy = createServer();
proxy.on("connect", (request, socket) => {
tunnels.push(request.url ?? "");
socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
const requests: string[] = [];
const proxy = createServer((request, response) => {
requests.push(request.url ?? "");
response.end("proxied");
});
const proxyPort = await listen(proxy);
const session = createManagedImageRegistryFetchSession({
Expand All @@ -52,8 +51,10 @@ describe("managed image registry transport", () => {
});

try {
await expect(session.fetchImpl("http://registry.invalid/v2/")).rejects.toThrow();
expect(tunnels).toEqual(["registry.invalid:80"]);
const response = await session.fetchImpl("http://registry.invalid/v2/");
expect(response.status).toBe(200);
expect(await response.text()).toBe("proxied");
expect(requests).toEqual(["http://registry.invalid/v2/"]);
} finally {
await session.close();
}
Expand Down
73 changes: 73 additions & 0 deletions test/mcp-tool-discovery-image-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,79 @@ const dockerfiles = [
] as const;

describe("MCP tool discovery image contract", () => {
// source-shape-contract: security -- Exact package pins and the production audit command protect the shipped runtime graph
it("pins reviewed packages and retains the production audit command (#8177)", () => {
const packageRoot = path.join(repoRoot, "tools", "mcp-tool-discovery-runtime");
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
const lock = JSON.parse(fs.readFileSync(path.join(packageRoot, "package-lock.json"), "utf8"));
const review = fs.readFileSync(path.join(packageRoot, "dependency-review.md"), "utf8");
const installer = fs.readFileSync(
path.join(packageRoot, "install-reviewed-runtime.sh"),
"utf8",
);
const reviewedSdk = {
name: "@modelcontextprotocol/sdk",
version: "1.30.0",
resolved: "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
integrity:
"sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
} as const;
const reviewedPackages = {
"@hono/node-server": {
version: "2.0.12",
resolved: "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz",
integrity:
"sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==",
},
"fast-uri": {
version: "3.1.5",
resolved: "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
integrity:
"sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
},
hono: {
version: "4.12.34",
resolved: "https://registry.npmjs.org/hono/-/hono-4.12.34.tgz",
integrity:
"sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==",
},
"ip-address": {
version: "10.3.1",
resolved: "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
integrity:
"sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
},
} as const;

expect(manifest.dependencies[reviewedSdk.name]).toBe(reviewedSdk.version);
expect(lock.packages[`node_modules/${reviewedSdk.name}`]).toMatchObject({
version: reviewedSdk.version,
resolved: reviewedSdk.resolved,
integrity: reviewedSdk.integrity,
});
expect(review).toContain(`\`${reviewedSdk.name}@${reviewedSdk.version}\``);
expect(review).toContain(`\`${reviewedSdk.integrity}\``);
expect(manifest.overrides).toEqual(
Object.fromEntries(
Object.entries(reviewedPackages).map(([packageName, metadata]) => [
packageName,
metadata.version,
]),
),
);
for (const [packageName, metadata] of Object.entries(reviewedPackages)) {
expect(lock.packages[`node_modules/${packageName}`]).toMatchObject(metadata);
expect(review).toContain(`\`${packageName}@${metadata.version}\``);
expect(review).toContain(`\`${metadata.integrity}\``);
}
expect(
installer
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.startsWith("npm audit")),
).toEqual(["npm audit signatures", "npm audit --omit=dev --audit-level=low"]);
});

it.each(
dockerfiles,
)("%s installs and probes the bundled runtime at its canonical path (#6901)", (relativePath) => {
Expand Down
53 changes: 46 additions & 7 deletions tools/mcp-tool-discovery-runtime/dependency-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,54 @@ The shared image runtime uses the official `@modelcontextprotocol/sdk` client so
- License: MIT
- Locked production graph: `package-lock.json` (lockfile version 3)
- Build-only tools: `typescript@6.0.3`, `@types/node@25.5.2`, and `esbuild@0.27.4` (not copied into the final image)
- Security overrides: `@hono/node-server@2.0.11` and `fast-uri@3.1.4`
- Security overrides:
- `@hono/node-server@2.0.12`: `sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==`
- `fast-uri@3.1.5`: `sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==`
- `hono@4.12.34`: `sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==`
- `ip-address@10.3.1`: `sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==`

OpenClaw's `mcporter` dependency graph also resolves the official SDK but remains separately locked. This runtime keeps a direct lock because Hermes and LangChain Deep Agents Code must not depend on OpenClaw's adapter package.
The client bundle includes the SDK's AJV validation path, including `ajv-formats` and `fast-uri`, plus `content-type` for standards-compliant response media-type parsing; the `fast-uri` override and `content-type` license are therefore runtime-relevant. It does not include the SDK's Hono server adapter. The build enforces the exact reviewed bundle-package allowlist and emits `BUNDLED_PACKAGES.json` alongside the generated third-party license notice. The exact overrides keep the install and runtime graphs clear of `GHSA-frvp-7c67-39w9` and `GHSA-v2hh-gcrm-f6hx` without changing the SDK client pin.
The client bundle includes the SDK's AJV validation path, including `ajv-formats` and `fast-uri`, plus `content-type` for standards-compliant response media-type parsing. The `fast-uri` override and `content-type` license are therefore runtime-relevant. The bundle does not include the SDK's Hono server adapter or its `hono` and `ip-address` dependencies, but those packages remain part of the installed production graph that the image build audits. The build enforces the exact reviewed bundle-package allowlist and emits `BUNDLED_PACKAGES.json` alongside the generated third-party license notice. The exact overrides keep the installed graph outside the affected ranges for `GHSA-7p8r-x3mc-p8w7`, `GHSA-8j4g-w8fx-2239`, `GHSA-mwp4-54f8-5fhr`, `GHSA-4xrf-jv44-h6hh`, and `GHSA-22jq-vg5j-6vgg` without changing the SDK client pin.

## 2026-08-03 security refresh

The strict image producer for NemoClaw `v0.0.100` failed before image creation because the committed runtime lock resolved three packages in newly reported advisory ranges. NemoClaw `main` at `3f3eb6139e089c24397d6a499a10fcde4bdc84da` reproduced the same failure. The audit boundary worked as designed and remains unchanged. Issue #8177 records the exact source, producer run, failure receipt, and resume condition.

Registry metadata binds each audited range:

- `fast-uri`: `3.1.4` at `6aeece669e4166b2446a89f17c07a3b15dfb7ed4` to `3.1.5` at `5e179cbb4636d5f773ed21126e5bd3068e87e94e`, one patch release
- Hono: `4.12.30` at `b2ae3a2204a48ce15a26448fd746d39745eb1837` to `4.12.34` at `734755ace341607628219ea1dd8ca17f01bf1a5c`, four patch releases
- `ip-address`: `10.2.0` at `80fccaae984618f35dc941efab55cf2440ab37e8` to `10.3.1` at `be7e626c0d49fccb518899f520a3fb64ee189741`, four release increments that cross the `10.3.0` minor boundary

Each target commit descends from its outgoing commit. The target npm package integrities match the committed lock.

Concern ledger:

- `MCP-AUDIT-1` — `fast-uri@3.1.4` accepts malformed or whitespace-smuggled authority introducers that can produce host confusion. Surface: executable AJV format validation in the bundled client. Resolution: pin `fast-uri@3.1.5`, which rejects the ambiguous forms and adds regression tests for them. Validation: exact lock metadata, `npm test`, bundle verification, and the production audit.
- `MCP-AUDIT-2` — `hono@4.12.30` uses a regular expression that can cause excessive work for a large CORS request-header value. Surface: installed SDK server dependency; excluded from the client bundle. Resolution: pin `hono@4.12.34`, which replaces the split expression and adds a regression test for a large request header. Validation: exact lock metadata, bundle-input exclusion, and the production audit.
- `MCP-AUDIT-3` — `ip-address@10.2.0` accepts address forms whose classification can differ across parsers. Surface: installed SDK server dependency; excluded from the client bundle. Resolution: pin `ip-address@10.3.1`, which rejects leading-zero IPv4 octets and stacked subnet suffixes and adds regression tests for IPv4 and IPv6 parsing. Validation: exact lock metadata, bundle-input exclusion, and the production audit.
- `MCP-AUDIT-4` — A dependency-only remediation could weaken the image build's fail-closed audit boundary. Surface: `install-reviewed-runtime.sh`. Resolution: retain `npm audit --omit=dev --audit-level=low` and bind it with the reviewed manifest and lock in the image contract test. Validation: `test/mcp-tool-discovery-image-contract.test.ts`.

## 1.29.0 to 1.30.0 migration review

The audited adjacent range contains 10 upstream commits. The published `1.30.0` tag resolves to commit `2d889f2b329e46680ec9bdd565de4616c497825a`, descends from the published `v1.29.0` tag at `e12cbd7078db388152f6e839abdbe09ba01f3f32`, and contains the required client media-type fix at `69749aa5081ddfe675d36da8d96c7e27d83742b8`. The npm publication's `gitHead` matches the target tag, and its registry signature and build provenance verify.

The required client change replaces case-sensitive substring checks with parsed, normalized media types when selecting JSON or SSE response handling. This fixes standards-valid case variants such as `Text/Event-Stream; Charset=UTF-8`. The remaining commits affect SDK server error formatting, server SSE keepalive lifecycle, stdio buffering, upstream tests and workflows, Zod type compatibility, the server-only Hono version range, and the release version. NemoClaw's bundled client does not include the server or stdio implementations, and the committed Hono override remains `2.0.11`.
The required client change replaces case-sensitive substring checks with parsed, normalized media types when selecting JSON or SSE response handling. This fixes standards-valid case variants such as `Text/Event-Stream; Charset=UTF-8`. The remaining commits affect SDK server error formatting, server SSE keepalive lifecycle, stdio buffering, upstream tests and workflows, Zod type compatibility, the server-only Hono version range, and the release version. NemoClaw's bundled client does not include the server or stdio implementations. The committed `@hono/node-server` override is `2.0.12`.

Concern ledger:

- `MCP-SDK-130-1` — Client response dispatch rejected case-variant SSE media types. Surface: managed MCP tool discovery initialization and `tools/list`. Resolution: migrate to the official parsed-media-type implementation and cover the full session with a case-variant SSE fixture. Validation: `npm test`.
- `MCP-SDK-130-2` — `content-type@1.0.5` becomes executable bundle input. Surface: response media-type parsing and bundled notices. Resolution: add it to the exact bundle allowlist and verify its MIT text in the generated notice. Validation: `npm run bundle`.
- `MCP-SDK-130-3` — The upstream package widens its Hono server range. Surface: resolved install graph only; the Hono server adapter is absent from the client bundle. Resolution: retain the existing exact `@hono/node-server@2.0.11` security override. Validation: the lock diff and `BUNDLED_PACKAGES.json`.
- `MCP-SDK-130-4` — Other adjacent commits could alter unrelated transports or server behavior. Surface: upstream stdio and server entry points. Resolution: no migration because NemoClaw imports only `client/index.js` and `client/streamableHttp.js`; classify those commits as no runtime impact. Validation: esbuild's exact input graph.
- `MCP-SDK-130-1`: Client response dispatch rejected case-variant SSE media types. Surface: managed MCP tool discovery initialization and `tools/list`. Resolution: migrate to the official parsed-media-type implementation and cover the full session with a case-variant SSE fixture. Validation: `npm test`.
- `MCP-SDK-130-2`: `content-type@1.0.5` becomes executable bundle input. Surface: response media-type parsing and bundled notices. Resolution: add it to the exact bundle allowlist and verify its MIT text in the generated notice. Validation: `npm run bundle`.
- `MCP-SDK-130-3`: The upstream package widens its Hono server range. Surface: resolved install graph only; the Hono server adapter is absent from the client bundle. Resolution: use the reviewed `@hono/node-server@2.0.12` patch release. Validation: the lock diff and `BUNDLED_PACKAGES.json`.
- `MCP-SDK-130-4`: Other adjacent commits could alter unrelated transports or server behavior. Surface: upstream stdio and server entry points. Resolution: no migration because NemoClaw imports only `client/index.js` and `client/streamableHttp.js`; classify those commits as no runtime impact. Validation: esbuild's exact input graph.

## `@hono/node-server` 2.0.12 review

Version `2.0.12` is the next patch release and remains within the SDK's declared `^1.19.9 || ^2.0.5` range. It keeps the MIT license, Node.js `>=20` engine, `hono@^4` peer dependency, package exports, and lack of install scripts.

The `v2.0.11..v2.0.12` source range contains three commits: a test transport replacement, a response-header fix for foreign `Response` objects, and the release commit. The server adapter remains outside NemoClaw's executable client bundle. The annotated tag and release commit are unsigned. During the 2026-08-03 security refresh, `npm audit signatures` verified the exact package's registry signature and Supply-chain Levels for Software Artifacts (SLSA) provenance against release commit `a813b6cdaa15baac3ead84e9e6ed5b72b2353c96`. Upstream Node.js 20, 22, and 24 checks, Windows checks, build checks, and the npm publication check passed.

The reviewed archive is `https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz` with integrity `sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==`. This patch keeps the fail-closed signature check and adds no exception.

## Build and audit contract

Expand Down Expand Up @@ -57,6 +88,14 @@ SDK 1.30.0 migration evidence on 2026-07-28:
- Exact bundle: 11 packages matching the reviewed allowlist in `BUNDLED_PACKAGES.json`, including `content-type@1.0.5`
- `npm run typecheck` and `npm run bundle`: passed

Security refresh evidence on 2026-08-03:

- `npm ci --ignore-scripts`: installed the exact 98-package lock
- `npm audit signatures`: verified 98 registry signatures and 12 provenance attestations
- `npm test` and `npm run typecheck`: passed
- `npm run bundle`: emitted the same 11-package client bundle with `fast-uri@3.1.5`; `hono` and `ip-address` remain outside the executable bundle
- `npm audit --omit=dev --audit-level=low`: 0 vulnerabilities

## Updating

Regenerate and review the graph explicitly:
Expand Down
24 changes: 12 additions & 12 deletions tools/mcp-tool-discovery-runtime/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions tools/mcp-tool-discovery-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"typescript": "6.0.3"
},
"overrides": {
"@hono/node-server": "2.0.11",
"fast-uri": "3.1.4"
"@hono/node-server": "2.0.12",
"fast-uri": "3.1.5",
"hono": "4.12.34",
"ip-address": "10.3.1"
},
"engines": {
"node": ">=22.19.0"
Expand Down
Loading