diff --git a/evals/terminal-bench/pyproject.toml b/evals/terminal-bench/pyproject.toml index 353a216b9..ab21028a7 100644 --- a/evals/terminal-bench/pyproject.toml +++ b/evals/terminal-bench/pyproject.toml @@ -16,6 +16,9 @@ packages = ["src/diffex_terminal_bench"] [tool.uv] default-groups = [] prerelease = "if-necessary" +# LiteLLM 1.106.4 caps filelock below 4, but its FileLock calls remain compatible. +# 4.2.1 fixes GHSA-v4x8-3wr5-3wq8, GHSA-9vhv-jxww-xfm5, and GHSA-567q-5qvw-pf92. +override-dependencies = ["filelock==4.2.1"] [[tool.uv.index]] name = "pypi" diff --git a/evals/terminal-bench/tests/test_filelock_security.py b/evals/terminal-bench/tests/test_filelock_security.py new file mode 100644 index 000000000..d8c398f65 --- /dev/null +++ b/evals/terminal-bench/tests/test_filelock_security.py @@ -0,0 +1,60 @@ +"""Security and compatibility checks for the benchmark's filelock override.""" + +from contextlib import closing +from pathlib import Path +import os +import sqlite3 +import struct +import tempfile +import unittest + +from filelock import FileLock, ReadWriteLock, SoftFileLock, Timeout +from huggingface_hub.utils import WeakFileLock + + +class FilelockSecurityTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "POSIX shared-directory permissions") + def test_planted_sqlite_journal_cannot_delete_an_unrelated_file(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + root.chmod(0o777) + database = root / "lock.db" + target = root / "fixture.txt" + target.write_text("must survive lock acquisition\n") + with closing(sqlite3.connect(database)) as connection: + connection.execute("CREATE TABLE fixture (value INTEGER)") + connection.commit() + + # SQLite treats this tail as a super-journal filename and unlinks it + # during recovery if it opens the planted journal beside the database. + name = str(target).encode() + journal = Path(str(database) + "-journal") + journal.write_bytes(name + struct.pack(">II", len(name), sum(name)) + + bytes.fromhex("d9d505f920a163d7")) + lock = ReadWriteLock(str(database)) + try: + with lock.read_lock(timeout=1): + self.assertEqual(target.read_text(), "must survive lock acquisition\n") + with lock.write_lock(timeout=1): + self.assertEqual(target.read_text(), "must survive lock acquisition\n") + finally: + lock.close() + + def test_consumers_keep_exclusion_timeouts_and_release(self): + with tempfile.TemporaryDirectory() as temporary: + for lock_type in (FileLock, SoftFileLock): + with self.subTest(lock_type=lock_type.__name__): + path = str(Path(temporary) / f"{lock_type.__name__}.lock") + holder = lock_type(path, timeout=1, mode=0o600) + contender = lock_type(path, timeout=0) + with holder: + with self.assertRaises(Timeout): + contender.acquire() + with contender: + self.assertTrue(contender.is_locked) + with WeakFileLock(Path(temporary) / "huggingface.lock", timeout=1): + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/evals/terminal-bench/uv.lock b/evals/terminal-bench/uv.lock index 6e1dae2b0..980c803c3 100644 --- a/evals/terminal-bench/uv.lock +++ b/evals/terminal-bench/uv.lock @@ -5,6 +5,9 @@ requires-python = "==3.13.*" [options] prerelease-mode = "if-necessary" +[manifest] +overrides = [{ name = "filelock", specifier = "==4.2.1" }] + [[package]] name = "aiofiles" version = "25.1.0" @@ -608,11 +611,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.32.7" +version = "4.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/59/e19834834cb01a32febfbb0f8a23a9088088f5d45991824ff2bc3b5e8acb/filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7", size = 225154, upload-time = "2026-09-16T00:24:20.907Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/98/771d8ca889f220d7d7e352addf500666368f0fd081c6080a83767580c6e4/filelock-4.2.1.tar.gz", hash = "sha256:c4c937ccac2a813cee448e662a56366cfb374c0f8702e75f295c412cc1c5d5a3", size = 650997, upload-time = "2026-10-17T16:48:14.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/df/31098c5aeb4d966b553641472bd55fcf5fdfac953549894b8a765ba44e91/filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52", size = 100157, upload-time = "2026-09-16T00:24:19.543Z" }, + { url = "https://files.pythonhosted.org/packages/28/00/db89d339bc4ecdc8087fa7b8607ba22ffc27bf96286f340c9620bea428b3/filelock-4.2.1-py3-none-any.whl", hash = "sha256:7be2662e9e826b9bba28afc0f843a123280d799a2ea44bb5d35ccec67905ae44", size = 162455, upload-time = "2026-10-17T16:48:12.937Z" }, ] [[package]] diff --git a/package-lock.json b/package-lock.json index 09fb8677f..cbaeb1d6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "packages/coding-agent/examples/extensions/sandbox" ], "devDependencies": { - "@anthropic-ai/sandbox-runtime": "0.0.26", + "@anthropic-ai/sandbox-runtime": "0.0.78", "@biomejs/biome": "2.6.3", "@opentelemetry/api": "1.10.0", "@types/node": "26.6.5", @@ -33,23 +33,21 @@ } }, "node_modules/@anthropic-ai/sandbox-runtime": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.26.tgz", - "integrity": "sha512-DYV5LSsVMnzq0lbfaYMSpxZPUMAx4+hy343dRss+pVCLIfF62qOhxpYfZ5TmOk1GTDQm5f9wPprMNSStmnsV4w==", + "version": "0.0.78", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.78.tgz", + "integrity": "sha512-YAIcybXTp7MZkBjasnkR1E3yxnf7u6kUkyW0vZrtsAZ9tyJGMaugWETmquPZs0YvW0sR0VnZeMfS0gsN/wQiVQ==", "license": "Apache-2.0", "dependencies": { "@pondwader/socks5-server": "^1.0.10", - "@types/lodash-es": "^4.17.12", "commander": "^12.1.0", - "lodash-es": "^4.17.21", - "shell-quote": "^1.8.3", - "zod": "^3.24.1" + "node-forge": "^1.4.0", + "zod": "^3.25.0" }, "bin": { "srt": "dist/cli.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.11.0" } }, "node_modules/@anthropic-ai/sdk": { @@ -1723,21 +1721,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.27", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.27.tgz", - "integrity": "sha512-VenMRv/eKNrspX9v3H4dbmjsrnCOYdYBG3kxXKk3dG6X+urBGcj2Prww3xw1U5K4FV/mE4h5GbWd0szEKf+9ew==", - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -4232,12 +4215,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -4554,6 +4531,15 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -5241,18 +5227,6 @@ "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", - "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/shelljs": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", @@ -6059,9 +6033,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -6648,7 +6622,7 @@ "name": "diffex-extension-sandbox", "version": "0.3.2", "dependencies": { - "@anthropic-ai/sandbox-runtime": "0.0.26" + "@anthropic-ai/sandbox-runtime": "0.0.78" } }, "packages/coding-agent/examples/extensions/with-deps": { diff --git a/package.json b/package.json index ae7c22ef3..42bc8bf58 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "check:identities": "node scripts/check-runtime-identities.mjs" }, "devDependencies": { - "@anthropic-ai/sandbox-runtime": "0.0.26", + "@anthropic-ai/sandbox-runtime": "0.0.78", "@biomejs/biome": "2.6.3", "@opentelemetry/api": "1.10.0", "@types/node": "26.6.5", @@ -68,6 +68,7 @@ }, "version": "0.0.3", "overrides": { + "zod": "4.6.6", "nanoid": "3.3.18", "protobufjs": "7.6.5", "rimraf": "6.1.3", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index c87b5c7fc..b5a56e6ac 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -8,27 +8,25 @@ "name": "diffex-extension-sandbox", "version": "0.3.2", "dependencies": { - "@anthropic-ai/sandbox-runtime": "0.0.26" + "@anthropic-ai/sandbox-runtime": "0.0.78" } }, "node_modules/@anthropic-ai/sandbox-runtime": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.26.tgz", - "integrity": "sha512-DYV5LSsVMnzq0lbfaYMSpxZPUMAx4+hy343dRss+pVCLIfF62qOhxpYfZ5TmOk1GTDQm5f9wPprMNSStmnsV4w==", + "version": "0.0.78", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.78.tgz", + "integrity": "sha512-YAIcybXTp7MZkBjasnkR1E3yxnf7u6kUkyW0vZrtsAZ9tyJGMaugWETmquPZs0YvW0sR0VnZeMfS0gsN/wQiVQ==", "license": "Apache-2.0", "dependencies": { "@pondwader/socks5-server": "^1.0.10", - "@types/lodash-es": "^4.17.12", "commander": "^12.1.0", - "lodash-es": "^4.17.21", - "shell-quote": "^1.8.3", - "zod": "^3.24.1" + "node-forge": "^1.4.0", + "zod": "^3.25.0" }, "bin": { "srt": "dist/cli.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.11.0" } }, "node_modules/@pondwader/socks5-server": { @@ -37,21 +35,6 @@ "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.27", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.27.tgz", - "integrity": "sha512-VenMRv/eKNrspX9v3H4dbmjsrnCOYdYBG3kxXKk3dG6X+urBGcj2Prww3xw1U5K4FV/mE4h5GbWd0szEKf+9ew==", - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -61,28 +44,19 @@ "node": ">=18" } }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, - "node_modules/shell-quote": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", - "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", - "license": "MIT", + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6.13.0" } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 1e19e8fe0..8b9f2d045 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -14,6 +14,9 @@ ] }, "dependencies": { - "@anthropic-ai/sandbox-runtime": "0.0.26" + "@anthropic-ai/sandbox-runtime": "0.0.78" + }, + "overrides": { + "zod": "4.6.6" } } diff --git a/packages/coding-agent/install-lock/package-lock.json b/packages/coding-agent/install-lock/package-lock.json index f226cd3fb..404f29ffa 100644 --- a/packages/coding-agent/install-lock/package-lock.json +++ b/packages/coding-agent/install-lock/package-lock.json @@ -3044,9 +3044,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/packages/coding-agent/install-lock/package.json b/packages/coding-agent/install-lock/package.json index af1b10861..6ef360ee5 100644 --- a/packages/coding-agent/install-lock/package.json +++ b/packages/coding-agent/install-lock/package.json @@ -7,6 +7,7 @@ "@diffexai/diffex": "0.3.2" }, "overrides": { + "zod": "4.6.6", "protobufjs": "7.6.5", "rimraf": "6.1.3", "gaxios": { diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 9717d7096..393039824 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -3034,9 +3034,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 5d2ede083..8aff1fab3 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -75,6 +75,7 @@ "yaml": "2.9.1" }, "overrides": { + "zod": "4.6.6", "protobufjs": "7.6.5", "rimraf": "6.1.3", "gaxios": { diff --git a/scripts/dependency-security.test.mjs b/scripts/dependency-security.test.mjs new file mode 100644 index 000000000..6ba49d152 --- /dev/null +++ b/scripts/dependency-security.test.mjs @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { SandboxRuntimeConfigSchema } from "@anthropic-ai/sandbox-runtime"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { CompleteRequestSchema } from "@modelcontextprotocol/sdk/types.js"; + +test("MCP rejects oversized completions before validating every element", async () => { + const client = new Client({ name: "security-test", version: "1.0.0" }); + const server = new Server({ name: "fixture", version: "1.0.0" }, { capabilities: { completions: {} } }); + server.setRequestHandler(CompleteRequestSchema, async () => ({ + completion: { values: Array(10_000).fill(0) }, + })); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + await assert.rejects( + client.complete({ ref: { type: "ref/prompt", name: "fixture" }, argument: { name: "value", value: "" } }), + (error) => { + assert.equal(error.issues.length, 1); + assert.equal(error.issues[0].code, "too_big"); + assert.deepEqual(error.issues[0].path, ["completion", "values"]); + return true; + }, + ); + } finally { + await client.close(); + await server.close(); + } +}); + +test("sandbox configuration keeps its validation contract with patched Zod", () => { + const config = { + network: { allowedDomains: ["github.com", "*.npmjs.org"], deniedDomains: ["blocked.example"] }, + filesystem: { denyRead: ["~/.ssh"], allowWrite: [".", "/tmp"], denyWrite: [".env"] }, + ignoreViolations: { "*": ["/tmp/fixture"] }, + ripgrep: { command: "rg", args: ["--hidden"] }, + }; + assert.deepEqual(SandboxRuntimeConfigSchema.parse(config), config); + for (const domain of ["*", "*.com", "https://example.com", "example.com/path"]) { + assert.equal( + SandboxRuntimeConfigSchema.safeParse({ + ...config, + network: { ...config.network, allowedDomains: [domain] }, + }).success, + false, + ); + } + assert.equal( + SandboxRuntimeConfigSchema.safeParse({ ...config, ignoreViolations: { "*": [42] } }).success, + false, + ); +});