Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"@types/react-dom": "^19.2.4",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"electron": "44.8.0",
"electron": "44.9.1",
"electron-builder": "^26.15.3",
"esbuild": "^0.28.1",
"resedit": "3.1.0",
Expand Down
155 changes: 155 additions & 0 deletions apps/mobile/test/dependency-security-tooling.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ const forge = createRequire(signingPath)("node-forge");
const metroRequire = createRequire(expoRequire.resolve("metro/package.json"));
const fileMapRequire = createRequire(metroRequire.resolve("metro-file-map/package.json"));
const braces = createRequire(fileMapRequire.resolve("micromatch"))("braces");
const { z } = cliRequire("zod");
const { z: legacyZod } = cliRequire("zod/v3");
const { PluginSchema, DevToolsPluginOutputSchema } =
cliRequire("./build/src/start/server/DevToolsPlugin.schema.js");
const { createMCPDevToolsExtensionSchema } =
cliRequire("./build/src/start/server/createMCPDevToolsExtensionSchema.js");
const { addMcpCapabilities } =
cliRequire("./build/src/start/server/MCPDevToolsPluginCLIExtensions.js");
const daemonRequire = createRequire(new URL("../../../packages/daemon/package.json", import.meta.url));
const mcpSdkRequire = createRequire(daemonRequire.resolve("@modelcontextprotocol/sdk/server/mcp.js"));
const { zodToJsonSchema } = mcpSdkRequire("zod-to-json-schema");
const pem = generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: { type: "spki", format: "pem" },
Expand Down Expand Up @@ -39,6 +50,30 @@ function digestInfo({ parameters = true, extra = false } = {}) {
])).getBytes();
}

function devToolsPlugin() {
return {
packageName: "dependency-security-plugin",
packageRoot: "/fixture/plugin",
cliExtensions: {
description: "Inspect development tools",
entryPoint: "tools.js",
commands: [
{
name: "inspect",
title: "Inspect",
environments: ["mcp"],
parameters: [
{ name: "query", type: "text" },
{ name: "count", type: "number" },
{ name: "confirm", type: "confirm" },
],
},
{ name: "ping", title: "Ping", environments: ["cli", "mcp"] },
],
},
};
}

test("Expo's node-forge rejects extra nested DigestAlgorithm elements (GHSA-86w9-cpqp-85rv)", () => {
for (const parameters of [true, false]) {
const signature = keyPair.privateKey.sign(digestInfo({ parameters, extra: true }), "NONE");
Expand Down Expand Up @@ -112,3 +147,123 @@ test("braces preserves normal glob compilation, expansion and literal escaping",
const boundary = "{".repeat(100) + "x" + "}".repeat(100);
assert.equal(braces.stringify(braces.parse(boundary)), boundary);
});

test("Expo's Zod rejects oversized bounded arrays before visiting elements", () => {
let elementVisits = 0;
const schema = z.array(z.string().transform((value) => {
elementVisits++;
return value.toUpperCase();
})).max(2);
const oversized = schema.safeParse(["a", "b", "c"]);
assert.equal(oversized.success, false);
assert.equal(oversized.error.issues[0].code, "too_big");
assert.equal(elementVisits, 0);
assert.deepEqual(schema.parse(["a", "b"]), ["A", "B"]);
assert.equal(elementVisits, 2);
});

test("Expo's DevTools plugin schema preserves command and parameter validation", () => {
const plugin = devToolsPlugin();
assert.deepEqual(PluginSchema.parse(plugin), plugin);
for (const invalid of [
{ ...plugin, packageName: "" },
{ ...plugin, packageRoot: "" },
{ ...plugin, cliExtensions: { ...plugin.cliExtensions, commands: [] } },
{
...plugin,
cliExtensions: {
...plugin.cliExtensions,
commands: [{
...plugin.cliExtensions.commands[0],
parameters: [{ name: "query", type: "object" }],
}],
},
},
]) {
assert.equal(PluginSchema.safeParse(invalid).success, false);
}
});

test("Expo's DevTools output schema preserves text, audio and image validation", () => {
const output = [
{ type: "text", text: "Ready", level: "info" },
{ type: "audio", url: "https://example.com/audio.mp3" },
{ type: "image", url: "https://example.com/image.png", text: "Preview" },
];
assert.deepEqual(DevToolsPluginOutputSchema.parse(output), output);
for (const invalid of [
{ type: "video", url: "https://example.com/video.mp4" },
{ type: "text", text: "Missing level" },
{ type: "text", text: "Invalid level", level: "debug" },
{ type: "audio", url: "invalid-url" },
{ type: "image" },
]) {
assert.equal(DevToolsPluginOutputSchema.safeParse([invalid]).success, false);
}
});

test("Expo's generated MCP schema preserves strict commands and optional typed parameters", () => {
const plugin = devToolsPlugin();
const schema = createMCPDevToolsExtensionSchema(plugin);
const parameters = { command: "inspect", query: "status", count: 2, confirm: true };
assert.deepEqual(schema.parse(parameters), parameters);
assert.deepEqual(schema.parse({ command: "ping" }), { command: "ping" });
for (const invalid of [
{},
{ command: "unknown" },
{ ...parameters, query: 1 },
{ ...parameters, count: "2" },
{ ...parameters, confirm: "true" },
{ ...parameters, unexpected: true },
]) {
assert.equal(schema.safeParse(invalid).success, false);
}
assert.throws(() => createMCPDevToolsExtensionSchema({ ...plugin, cliExtensions: undefined }), /no commands defined/);
assert.throws(() => createMCPDevToolsExtensionSchema({
...plugin,
cliExtensions: { ...plugin.cliExtensions, commands: [] },
}), /no commands defined/);
});

test("Expo's MCP registration retains constraints through the legacy JSON schema converter", async () => {
const plugin = devToolsPlugin();
plugin.cliExtensions.commands.push({
name: "local",
title: "Local only",
environments: ["cli"],
parameters: [{ name: "localPath", type: "text" }],
});
const registrations = [];
await addMcpCapabilities({
registerTool(...args) {
registrations.push(args);
},
}, {
devtoolsPluginManager: {
async queryPluginsAsync() {
return [plugin];
},
},
});
assert.equal(registrations.length, 1);
const [name, config, handler] = registrations[0];
assert.equal(name, plugin.packageName);
assert.equal(config.title, plugin.packageName);
assert.equal(typeof handler, "function");
assert.equal(config.inputSchema.parameters.safeParse({ command: "local" }).success, false);

// Expo's optional MCP tunnel wraps inputSchema with Zod 3 before this conversion.
const converted = zodToJsonSchema(legacyZod.object(config.inputSchema));
assert.equal(converted.type, "object");
assert.deepEqual(converted.required, ["parameters"]);
const parameters = converted.properties.parameters;
assert.equal(parameters.type, "object");
assert.deepEqual(parameters.properties.command.enum, ["inspect", "ping"]);
assert.equal(parameters.properties.command.type, "string");
assert.equal(parameters.properties.query.type, "string");
assert.equal(parameters.properties.count.type, "number");
assert.equal(parameters.properties.confirm.type, "boolean");
assert.equal(Object.hasOwn(parameters.properties, "localPath"), false);
assert.deepEqual(parameters.required, ["command"]);
assert.equal(parameters.additionalProperties, false);
});
17 changes: 11 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,26 @@
],
"overrides": {
"@radix-ui/react-dismissable-layer@1.1.19": "1.1.20",
"@expo/cli>zod": "4.6.6",
"brace-expansion@>=1.0.0 <1.1.21": "1.1.21",
"brace-expansion@>=2.0.0 <2.1.7": "2.1.7",
"brace-expansion@>=5.0.0 <5.0.12": "5.0.12",
"fast-uri@>=3.0.0 <3.1.8": "3.1.8",
"hono@>=4.0.0 <4.13.7": "4.13.7",
"body-parser@>=2.0.0 <2.4.0": "2.4.0",
"dompurify@>=3.0.0 <3.4.17": "3.4.17",
"fast-uri@>=3.0.0 <3.1.9": "3.1.9",
"hono@>=4.0.0 <4.13.11": "4.13.11",
"http-cache-semantics@>=4.0.0 <4.3.0": "4.3.0",
"ip-address@>=10.0.0 <10.7.1": "10.7.1",
"ip-address@>=10.0.0 <10.7.4": "10.7.4",
"js-yaml@>=4.0.0 <4.3.2": "4.3.2",
"postcss@>=8.0.0 <=8.5.17": "8.5.25",
"protobufjs@>=7.5.0 <7.6.5": "7.6.5",
"tar@>=7.0.0 <=7.5.20": "7.5.22",
"undici@>=7.0.0 <7.29.1": "7.30.0",
"node-gyp>undici@>=6.0.0 <6.29.1": "6.29.1",
"@electron/get>undici@>=7.0.0 <7.30.1": "7.30.1",
"uuid@7.0.3": "11.1.1",
"image-size@1.2.1": "2.0.4",
"officeparser>pdfjs-dist": "6.2.108",
"undici@>=8.0.0 <8.10.2": "8.11.2",
"@earendil-works/pi-coding-agent>undici@>=8.0.0 <8.12.2": "8.12.2",
"katex@>=0.11.0 <0.18.2": "0.19.0"
},
"patchedDependencies": {
Expand All @@ -80,7 +84,8 @@
"braces@3.0.3": "patches/braces@3.0.3.patch",
"sprintf-js@1.0.3": "patches/sprintf-js@1.0.3.patch",
"sprintf-js@1.1.3": "patches/sprintf-js@1.1.3.patch",
"image-size@2.0.4": "patches/image-size@2.0.4.patch"
"image-size@2.0.4": "patches/image-size@2.0.4.patch",
"@expo/cli@58.1.13": "patches/@expo__cli@58.1.13.patch"
}
}
}
3 changes: 2 additions & 1 deletion packages/daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"pdf-parse": "2.4.5",
"pptxgenjs": "4.0.1",
"shell-quote": "1.12.0",
"undici": "7.30.0",
"undici": "7.30.1",
"zod": "4.6.6",
"ws": "^8.21.0",
"xlsx-populate": "1.21.0",
"@agentclientprotocol/sdk": "1.9.0",
Expand Down
19 changes: 16 additions & 3 deletions packages/daemon/test/packaged-gstack-startup.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,26 @@ test("packaged startup admits and retains a descriptor without downloading the b
t.mock.method(globalThis, "fetch", () => assert.fail("startup must not acquire browser bytes"));
const module = code => `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
const installedModule = module("export let installed; export function installPackagedSkillRuntime(current,retained){installed={current,retained};}");
const fixtureModule = module(`
let fixture;
export function configure(value) { fixture = value; }
export function resolveUserConfigPaths() { return { sourcesDir: fixture.sourcesDir }; }
export function readPackagedGstackIdentity() { return fixture.identity; }
export async function prepareBundledSkillStartup(selection, callback) {
return callback({
manifestChecksum: selection.expectedManifestChecksum,
readResource: async () => fixture.receipt,
});
}
`);
(await import(fixtureModule)).configure({ sourcesDir: join(root, "sources"), identity, receipt });
// Only desktop discovery and the unrelated loader/install singleton are test
// substitutes. Descriptor admission and generation publication remain real.
const replacements = new Map([
["@ling-stack/core/bundled-skill-browser", new URL("../../core/dist/bundled-skill-browser.js", import.meta.url).href],
["@ling-stack/core", module(`export function resolveUserConfigPaths(){return {sourcesDir:${JSON.stringify(join(root, "sources"))}};}`)],
["./packaged-identity.js", module(`export function readPackagedGstackIdentity(){return ${JSON.stringify(identity)};}`)],
["./bundled-startup.js", module(`export async function prepareBundledSkillStartup(selection, callback){return callback({manifestChecksum:selection.expectedManifestChecksum,readResource:async()=>${JSON.stringify(receipt)}});}`)],
["@ling-stack/core", fixtureModule],
["./packaged-identity.js", fixtureModule],
["./bundled-startup.js", fixtureModule],
["./packaged-runtime.js", installedModule],
["./packaged-history.js", new URL("../dist/features/skills/packaged-history.js", import.meta.url).href],
]);
Expand Down
3 changes: 2 additions & 1 deletion packages/runner-pi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@earendil-works/pi-ai": "1.1.0",
"@earendil-works/pi-coding-agent": "1.1.0",
"@ling-stack/plugin-sdk": "workspace:*",
"undici": "7.30.0"
"undici": "7.30.1",
"zod": "4.6.6"
}
}
16 changes: 16 additions & 0 deletions patches/@expo__cli@58.1.13.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
diff --git a/build/src/start/server/createMCPDevToolsExtensionSchema.js b/build/src/start/server/createMCPDevToolsExtensionSchema.js
index 3b6f73d81bda27c90ec68aec19eb2568758ddd66..80b7c245cf5f00ee1f35811b6a2fae5fa34a4fd1 100644
--- a/build/src/start/server/createMCPDevToolsExtensionSchema.js
+++ b/build/src/start/server/createMCPDevToolsExtensionSchema.js
@@ -9,7 +9,10 @@ Object.defineProperty(exports, "createMCPDevToolsExtensionSchema", {
}
});
function _zod() {
- const data = require("zod");
+ // The optional MCP tunnel still converts these primitive-only schemas with
+ // zod-to-json-schema v3. Keep its schema protocol while other CLI validators
+ // use the patched Zod 4 default export.
+ const data = require("zod/v3");
_zod = function() {
return data;
};
8 changes: 8 additions & 0 deletions patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Reviewed baseline: `fd39ec302cbae759e4d4a866172a804e1bc5abfd`, 2026-10-12.

| Package | Advisory / purpose | Actual consumer | Removal gate |
| --- | --- | --- | --- |
| `@expo/cli@58.1.13` | Preserve the optional MCP tunnel's Zod 3 schema protocol while the CLI uses fixed Zod 4 for [GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5) | Expo dev-tools MCP command schemas | Upstream tunnel supports Zod 4 and command enum, parameter types, and strict JSON Schema conversion regressions pass without this bridge |
| `node-forge@1.4.0` | [GHSA-86w9-cpqp-85rv](https://github.com/advisories/GHSA-86w9-cpqp-85rv): reject unconsumed nested DigestAlgorithm elements | Expo code-signing certificates | Released upstream fix plus malformed-signature and valid-certificate regressions |
| `braces@3.0.3` | [GHSA-vfj7-8cjw-p6xm](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm): cap brace/parenthesis nesting and validate direct AST inputs | Metro file matching via micromatch | Upstream depth protection covering parse, compile, expand and stringify |
| `sprintf-js@1.0.3`, `sprintf-js@1.1.3` | [GHSA-hp3w-g68c-fv3c](https://github.com/advisories/GHSA-hp3w-g68c-fv3c): bound floating-point precision | Mammoth CLI via argparse; Desktop downloader logger via Roarr | Upstream protection plus formatter, CLI help and logger regressions |
Expand All @@ -17,6 +18,13 @@ Reviewed baseline: `fd39ec302cbae759e4d4a866172a804e1bc5abfd`, 2026-10-12.

## Patch Behavior And Provenance

- `@expo/cli`: only the MCP command-schema factory imports the fixed Zod
package's public `zod/v3` compatibility entry. Its command enum and optional
string, number, and boolean parameters retain the schema protocol consumed by
`@expo/mcp-tunnel` and `zod-to-json-schema` 3. The plugin and output collection
validators keep the patched Zod 4 default export. This local compatibility
bridge contains no array, map, or set schemas and does not backport Zod 4's
collection fix to the compatibility entry.
- `node-forge`: backport the `lib/rsa.js` change from
[official PR 1152](https://github.com/digitalbazaar/forge/pull/1152), reviewed at
`ceba34402e329f0365134f23fe19898756527d65`. No cryptographic primitive is replaced.
Expand Down
Loading
Loading