From 966d8800ccfd30e3c9faf37ec88f0b952dae8cc6 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Mon, 22 Jun 2026 14:10:55 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20(ai-sdk):=20Patch=20Vercel?= =?UTF-8?q?=20adapter=20via=20shim=20instead=20of=20mutating=20frozen=20ES?= =?UTF-8?q?M?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vercel adapter assigned `module.tool = governed` onto the loaded `ai` module. When a consumer has the real `ai` 5.x/6.x installed, its namespace is a frozen ES-module exotic object whose named exports are non-writable, so the assignment throws `Cannot assign to read only property 'tool'` and crashes `initAssembly()`. Install the governed factory through `applyGovernedToolFactory`: assign in place only for writable plain objects (the unit suite's loadModule fakes), and otherwise fall back to a mutable shim copy `{ ...module, tool: governed }` — the approach the AAASM-3525 driver proved works — never touching the frozen namespace. `unpatch` restores the original only when it was mutated in place. Refs AAASM-3532 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/hooks/ai-sdk.ts | 70 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/src/hooks/ai-sdk.ts b/src/hooks/ai-sdk.ts index 358e7b7a..f1f4ada9 100644 --- a/src/hooks/ai-sdk.ts +++ b/src/hooks/ai-sdk.ts @@ -14,13 +14,27 @@ export interface VercelAiSdkModule { export interface VercelAiSdkPatchState { isPatched: boolean; originalToolFactory: VercelAiToolFactory | undefined; + /** + * The module object whose `tool` factory is governed. When the loaded `ai` + * package is a real ES module its namespace is frozen (assignment to a named + * export throws), so this is a mutable **shim copy** (`{ ...module, tool: + * governed }`) rather than the frozen original — see `applyGovernedToolFactory`. + */ patchedModule: VercelAiSdkModule | undefined; + /** + * True only when `tool` was assigned back onto the loaded module object (a + * writable plain object); false when a frozen ESM namespace forced a shim copy. + * Governs whether `unpatchVercelAiSdk` writes the original factory back — + * there is nothing to restore on the frozen original in the shim case. + */ + mutatedOriginal: boolean; } export const vercelAiSdkPatchState: VercelAiSdkPatchState = { isPatched: false, originalToolFactory: undefined, - patchedModule: undefined + patchedModule: undefined, + mutatedOriginal: false }; export function captureOriginalToolFactory( @@ -154,6 +168,39 @@ export interface PatchVercelAiSdkOptions { loadModule?: () => Promise; } +/** + * Install `governed` as the module's `tool` factory without ever assigning to a + * frozen ESM namespace. + * + * A real `ai` package loaded via `import()` is an ES module: its namespace is an + * exotic object whose named exports are non-writable, so `module.tool = …` throws + * `Cannot assign to read only property 'tool'` (AAASM-3532). We therefore attempt + * the in-place assignment only as a fast path for writable plain objects (the + * shape used by the unit suite's `loadModule` fakes) and fall back to a mutable + * **shim copy** for the frozen-namespace case — the same `{ tool: aiModule.tool }` + * shim the AAASM-3525 integration driver proved works. The returned module is what + * downstream consumers read the governed factory from (`patchedModule.tool`). + */ +function applyGovernedToolFactory( + module: VercelAiSdkModule, + governed: VercelAiToolFactory +): { patchedModule: VercelAiSdkModule; mutatedOriginal: boolean } { + if (Object.isExtensible(module)) { + try { + module.tool = governed; + return { patchedModule: module, mutatedOriginal: true }; + } catch { + // Some non-extensible-but-reported-extensible exotic objects still reject + // the write; fall through to the shim copy below. + } + } + + return { + patchedModule: { ...module, tool: governed }, + mutatedOriginal: false + }; +} + async function loadVercelAiSdkModule(): Promise { try { const moduleName = "ai"; @@ -182,7 +229,7 @@ export async function patchVercelAiSdk( return false; } - module.tool = createPatchedToolFactory( + const governed = createPatchedToolFactory( originalToolFactory, options.gatewayClient, { @@ -191,8 +238,15 @@ export async function patchVercelAiSdk( ...(options.agentId === undefined ? {} : { agentId: options.agentId }) } ); + + const { patchedModule, mutatedOriginal } = applyGovernedToolFactory( + module, + governed + ); + vercelAiSdkPatchState.isPatched = true; - vercelAiSdkPatchState.patchedModule = module; + vercelAiSdkPatchState.patchedModule = patchedModule; + vercelAiSdkPatchState.mutatedOriginal = mutatedOriginal; return true; } @@ -207,9 +261,15 @@ export function unpatchVercelAiSdk(): boolean { return false; } - vercelAiSdkPatchState.patchedModule.tool = - vercelAiSdkPatchState.originalToolFactory; + // Only restore when we mutated a writable module in place. For the frozen-ESM + // shim path the original `ai` namespace was never touched, so there is nothing + // to write back — and attempting it would re-throw the AAASM-3532 crash. + if (vercelAiSdkPatchState.mutatedOriginal) { + vercelAiSdkPatchState.patchedModule.tool = + vercelAiSdkPatchState.originalToolFactory; + } vercelAiSdkPatchState.isPatched = false; vercelAiSdkPatchState.patchedModule = undefined; + vercelAiSdkPatchState.mutatedOriginal = false; return true; } From f6b3ba1f7f21bcbb6b9320a8582a310f769355eb Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Mon, 22 Jun 2026 14:11:05 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=A7=20(deps):=20Install=20real=20`?= =?UTF-8?q?ai`=20so=20the=20Vercel=20adapter=20is=20tested=20against=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `.pnpmfile.cjs` from PR #174 stripped `ai` from this repo's install to avoid the frozen-ESM crash in the unit suite. With AAASM-3532 fixed, that workaround is no longer needed for `ai`: add `ai` as a devDependency and drop it from the strip list so the real-module regression test exercises the genuine frozen namespace. `@langchain/langgraph` / `@mastra/core` stay stripped — their adapters mutate a class prototype (not the namespace) and never crashed. Refs AAASM-3532 Co-Authored-By: Claude Opus 4.8 (1M context) --- .pnpmfile.cjs | 22 ++++++---- package.json | 1 + pnpm-lock.yaml | 110 +++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 103 insertions(+), 30 deletions(-) diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs index de386342..d34c0c53 100644 --- a/.pnpmfile.cjs +++ b/.pnpmfile.cjs @@ -8,15 +8,19 @@ // gets a correct version range — that is a contract, not an install. // // pnpm 10 materializes *declared* peerDependencies (even optional ones) into the -// importer and `node_modules`. If the real `ai` / `@langchain/langgraph` / -// `@mastra/core` packages land in this repo's `node_modules`, the SDK's own unit -// suite trips: `initAssembly` detects the framework present and the Vercel adapter -// then assigns to the frozen ESM `tool` binding of the real `ai` module -// (`Cannot assign to read only property 'tool'`). Those frameworks must therefore -// stay declared-but-uninstalled here. `@langchain/core` / `@openai/agents` are left -// alone — `@langchain/core` is already a transitive dep of the `langchain` devDep -// and the existing suite relies on them being resolvable. -const STRIP_FROM_INSTALL = ["@langchain/langgraph", "ai", "@mastra/core"]; +// importer and `node_modules`. We keep the heavyweight `@langchain/langgraph` and +// `@mastra/core` declared-but-uninstalled here purely to avoid pulling their large +// dependency trees into this repo's own install — their adapters mutate a class +// *prototype*, not the module namespace, so they never crashed on a frozen ESM. +// +// `ai` is now installed as a `devDependency` (not stripped): AAASM-3532 fixed the +// Vercel adapter so it no longer assigns to the frozen ESM `tool` binding of the +// real `ai` module (it falls back to a shim copy), and the real-`ai` regression +// test in `tests/vercel-ai-real-module.test.ts` needs the genuine frozen namespace +// to prove the fix. `@langchain/core` / `@openai/agents` are likewise left alone — +// `@langchain/core` is already a transitive dep of the `langchain` devDep and the +// existing suite relies on them being resolvable. +const STRIP_FROM_INSTALL = ["@langchain/langgraph", "@mastra/core"]; function readPackage(pkg) { if (pkg.name === "@agent-assembly/sdk") { diff --git a/package.json b/package.json index 9a798954..cd3415ad 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^25.9.1", "@vitest/coverage-v8": "^3.2.6", + "ai": "^6.0.0", "blocked-at": "^1.2.0", "eslint": "^10.4.1", "grpc-tools": "^1.13.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba469520..11a97c99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ overrides: tar: ^7.5.16 uuid: '>=11.1.1' -pnpmfileChecksum: sha256-G0rPZ7h/JMrFXtzbD/VloEPh3PHNVVjaBe3B0LqF8r0= +pnpmfileChecksum: sha256-Ba1Bu9ZW48eqIymOVoJKwAIn7SvtKEytaNxSbWYgaI0= importers: @@ -30,7 +30,7 @@ importers: version: 1.14.4 '@langchain/core': specifier: '>=0.3.0' - version: 1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + version: 1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) '@openai/agents': specifier: '>=0.1.0' version: 0.8.5(@cfworker/json-schema@4.1.1)(ws@8.21.0)(zod@4.4.3) @@ -50,6 +50,9 @@ importers: '@vitest/coverage-v8': specifier: ^3.2.6 version: 3.2.6(vitest@3.2.6(@types/node@25.9.3)(lightningcss@1.32.0)) + ai: + specifier: ^6.0.0 + version: 6.0.208(zod@4.4.3) blocked-at: specifier: ^1.2.0 version: 1.2.0 @@ -64,7 +67,7 @@ importers: version: 4.2.0 langchain: specifier: ^1.4.0 - version: 1.4.0(@langchain/core@1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)) + version: 1.4.0(@langchain/core@1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)) prettier: specifier: ^3.5.3 version: 3.8.4 @@ -104,6 +107,22 @@ importers: packages: + '@ai-sdk/gateway@3.0.133': + resolution: {integrity: sha512-Ebs+7iS9zUgJu5B0RlxM2JmDWzq79Cpd6YdiqcCzB5qFdpfQJPUDiXutqlQP89F2XGjOdDeidulBTXUdXWzOxw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.30': + resolution: {integrity: sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.10': + resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} + engines: {node: '>=18'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -1020,6 +1039,10 @@ packages: peerDependencies: zod: ^4.0.0 + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1281,6 +1304,10 @@ packages: resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + '@vitest/coverage-v8@3.2.6': resolution: {integrity: sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==} peerDependencies: @@ -1341,6 +1368,12 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ai@6.0.208: + resolution: {integrity: sha512-STz+AaZqJ4ZjH7UkpXkbHx+bjgIDOsE8fIUoZjkZ2whoZcfVmG9K/TqEKouJZ03SuZuD7lagntlU3zBhAEkRpQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -1902,6 +1935,9 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2608,6 +2644,24 @@ packages: snapshots: + '@ai-sdk/gateway@3.0.133(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/provider-utils@4.0.30(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.8 + zod: 4.4.3 + + '@ai-sdk/provider@3.0.10': + dependencies: + json-schema: 0.4.0 + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -2943,7 +2997,7 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@langchain/core@1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)': + '@langchain/core@1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 @@ -2951,7 +3005,7 @@ snapshots: camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.6.3(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + langsmith: 0.6.3(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) mustache: 4.2.0 p-queue: 6.6.2 zod: 4.4.3 @@ -2962,14 +3016,14 @@ snapshots: - openai - ws - '@langchain/langgraph-checkpoint@1.0.2(@langchain/core@1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))': + '@langchain/langgraph-checkpoint@1.0.2(@langchain/core@1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))': dependencies: - '@langchain/core': 1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/core': 1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) uuid: 13.0.2 - '@langchain/langgraph-sdk@1.9.1(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)': + '@langchain/langgraph-sdk@1.9.1(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)': dependencies: - '@langchain/core': 1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/core': 1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) '@langchain/protocol': 0.0.15 '@types/json-schema': 7.0.15 p-queue: 9.2.0 @@ -2982,11 +3036,11 @@ snapshots: - openai - ws - '@langchain/langgraph@1.3.0(@langchain/core@1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': + '@langchain/langgraph@1.3.0(@langchain/core@1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': dependencies: - '@langchain/core': 1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) - '@langchain/langgraph-checkpoint': 1.0.2(@langchain/core@1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) - '@langchain/langgraph-sdk': 1.9.1(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/core': 1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/langgraph-checkpoint': 1.0.2(@langchain/core@1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) + '@langchain/langgraph-sdk': 1.9.1(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) '@langchain/protocol': 0.0.15 '@standard-schema/spec': 1.1.0 uuid: 13.0.2 @@ -3424,6 +3478,8 @@ snapshots: - utf-8-validate - ws + '@opentelemetry/api@1.9.1': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -3647,6 +3703,8 @@ snapshots: '@typescript-eslint/types': 8.61.0 eslint-visitor-keys: 5.0.1 + '@vercel/oidc@3.2.0': {} + '@vitest/coverage-v8@3.2.6(vitest@3.2.6(@types/node@25.9.3)(lightningcss@1.32.0))': dependencies: '@ampproject/remapping': 2.3.0 @@ -3724,6 +3782,14 @@ snapshots: agent-base@7.1.4: {} + ai@6.0.208(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 3.0.133(zod@4.4.3) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@4.4.3) + '@opentelemetry/api': 1.9.1 + zod: 4.4.3 + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -4050,8 +4116,7 @@ snapshots: eventemitter3@5.0.4: {} - eventsource-parser@3.0.8: - optional: true + eventsource-parser@3.0.8: {} eventsource@3.0.7: dependencies: @@ -4330,6 +4395,8 @@ snapshots: json-schema-typed@8.0.2: optional: true + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-with-bigint@3.5.8: {} @@ -4338,12 +4405,12 @@ snapshots: dependencies: json-buffer: 3.0.1 - langchain@1.4.0(@langchain/core@1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)): + langchain@1.4.0(@langchain/core@1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)): dependencies: - '@langchain/core': 1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) - '@langchain/langgraph': 1.3.0(@langchain/core@1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) - '@langchain/langgraph-checkpoint': 1.0.2(@langchain/core@1.1.45(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) - langsmith: 0.6.3(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/core': 1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/langgraph': 1.3.0(@langchain/core@1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) + '@langchain/langgraph-checkpoint': 1.0.2(@langchain/core@1.1.45(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) + langsmith: 0.6.3(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) zod: 4.4.3 transitivePeerDependencies: - '@opentelemetry/api' @@ -4357,10 +4424,11 @@ snapshots: - ws - zod-to-json-schema - langsmith@0.6.3(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0): + langsmith@0.6.3(@opentelemetry/api@1.9.1)(openai@6.35.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0): dependencies: p-queue: 6.6.2 optionalDependencies: + '@opentelemetry/api': 1.9.1 openai: 6.35.0(ws@8.21.0)(zod@4.4.3) ws: 8.21.0 From 879e5f664063a82684aefc98b64f54d353bc2457 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Mon, 22 Jun 2026 14:11:17 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=85=20(ai-sdk):=20Cover=20the=20real?= =?UTF-8?q?=20frozen-`ai`=20patch=20path=20for=20AAASM-3532?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drive `patchVercelAiSdk` against the real, frozen `ai` namespace via the default loader (no loadModule fake) and assert: patching no longer crashes, the original `ai.tool` export is left untouched, and governance (deny/allow) flows through the governed factory on the shim copy. This test crashes with `Cannot assign to read only property 'tool'` under the pre-fix code. Reset the new `mutatedOriginal` patch-state field in the existing suite's teardown. Refs AAASM-3532 Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/vercel-ai-hook.test.ts | 1 + tests/vercel-ai-real-module.test.ts | 119 ++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/vercel-ai-real-module.test.ts diff --git a/tests/vercel-ai-hook.test.ts b/tests/vercel-ai-hook.test.ts index dc65eeb5..8cd9961b 100644 --- a/tests/vercel-ai-hook.test.ts +++ b/tests/vercel-ai-hook.test.ts @@ -402,4 +402,5 @@ async function resetPatchState(): Promise { hooks.vercelAiSdkPatchState.originalToolFactory = undefined; hooks.vercelAiSdkPatchState.patchedModule = undefined; hooks.vercelAiSdkPatchState.isPatched = false; + hooks.vercelAiSdkPatchState.mutatedOriginal = false; } diff --git a/tests/vercel-ai-real-module.test.ts b/tests/vercel-ai-real-module.test.ts new file mode 100644 index 00000000..49a50760 --- /dev/null +++ b/tests/vercel-ai-real-module.test.ts @@ -0,0 +1,119 @@ +// AAASM-3532 regression: drive the Vercel adapter against the *real* `ai` package +// (installed as a devDependency), not a `loadModule` fake. The real `ai` namespace +// is a frozen ES module exotic object, so the old `module.tool = …` patch threw +// `Cannot assign to read only property 'tool'`. This suite proves the shim-copy +// fix patches without crashing AND that governance flows through the governed +// factory the patch produces. +import { afterEach, describe, expect, it, vi } from "vitest"; +import * as realAi from "ai"; +import type { GatewayClient } from "../src/gateway/client.js"; + +function createGatewayClientMock(): GatewayClient { + return { + mode: "sdk-only", + start: vi.fn(async () => undefined), + close: vi.fn(async () => undefined), + check: vi.fn(async () => ({ denied: false, pending: false })), + waitForApproval: vi.fn(async () => ({ denied: false })), + record: vi.fn(async () => undefined), + recordResult: vi.fn(async () => undefined), + scanPrompts: vi.fn(async () => undefined) + }; +} + +afterEach(async () => { + const hooks = await import("../src/hooks/ai-sdk.js"); + hooks.unpatchVercelAiSdk(); + hooks.vercelAiSdkPatchState.originalToolFactory = undefined; + hooks.vercelAiSdkPatchState.patchedModule = undefined; + hooks.vercelAiSdkPatchState.isPatched = false; + hooks.vercelAiSdkPatchState.mutatedOriginal = false; + vi.resetModules(); +}); + +describe("vercel ai sdk adapter — real `ai` module", () => { + it("confirms the real `ai` namespace is a frozen exotic object", () => { + // Guards the premise of this suite: if `ai` ever ships a writable namespace, + // the shim path stops being exercised and this test should be revisited. + expect(typeof realAi.tool).toBe("function"); + expect(Object.isExtensible(realAi)).toBe(false); + expect(() => { + (realAi as unknown as { tool: unknown }).tool = undefined; + }).toThrow(/read only property 'tool'/); + }); + + it("patches the real frozen `ai` module without crashing", async () => { + const gateway = createGatewayClientMock(); + const hooks = await import("../src/hooks/ai-sdk.js"); + + // Default loader (no `loadModule` fake) → imports the real, frozen `ai`. + // With the pre-fix `module.tool = …` code this rejects with + // `Cannot assign to read only property 'tool'`. + const patched = await hooks.patchVercelAiSdk({ gatewayClient: gateway }); + + expect(patched).toBe(true); + expect(hooks.vercelAiSdkPatchState.isPatched).toBe(true); + // The frozen namespace forced a shim copy, so we never mutated the original. + expect(hooks.vercelAiSdkPatchState.mutatedOriginal).toBe(false); + // The real `ai.tool` export is untouched (governance lives on the shim copy). + expect(realAi.tool).toBe(hooks.vercelAiSdkPatchState.originalToolFactory); + }); + + it("applies governance through the governed factory from the real module", async () => { + const gateway = createGatewayClientMock(); + gateway.check = vi.fn(async () => ({ denied: true, reason: "blocked-by-policy" })); + const hooks = await import("../src/hooks/ai-sdk.js"); + + await hooks.patchVercelAiSdk({ gatewayClient: gateway }); + + const governedTool = hooks.vercelAiSdkPatchState.patchedModule?.tool; + expect(typeof governedTool).toBe("function"); + + const executed = vi.fn(async () => ({ ok: true })); + // Build a tool through the real `ai` factory shape, then wrap via the patch. + const governed = governedTool!({ + description: "delete database", + // `inputSchema` is the real `ai` 5.x/6.x field; `parameters` kept for older. + parameters: {}, + execute: executed + } as never) as { execute?: (a: unknown, o: unknown) => Promise }; + + const error = await governed + .execute!({ table: "users" }, { toolCallId: "real-call-1" }) + .catch((e: Error) => e); + + // Assert by `name`, not `instanceof`: `vi.resetModules()` between tests makes + // the adapter construct PolicyViolationError from a fresh module graph whose + // class identity differs from a statically-imported one. + expect(error).toBeInstanceOf(Error); + expect((error as Error).name).toBe("PolicyViolationError"); + expect((error as Error).message).toBe( + "Tool blocked by governance policy: blocked-by-policy" + ); + expect(executed).not.toHaveBeenCalled(); + expect(gateway.check).toHaveBeenCalledWith( + expect.objectContaining({ toolName: "delete database", action: "tool_call" }) + ); + }); + + it("allows governed execution through the real module on ALLOW", async () => { + const gateway = createGatewayClientMock(); + gateway.check = vi.fn(async () => ({ denied: false, pending: false })); + const hooks = await import("../src/hooks/ai-sdk.js"); + + await hooks.patchVercelAiSdk({ gatewayClient: gateway }); + + const governedTool = hooks.vercelAiSdkPatchState.patchedModule!.tool; + const executed = vi.fn(async () => ({ ok: "allowed" })); + const governed = governedTool({ + description: "read weather", + parameters: {}, + execute: executed + } as never) as { execute?: (a: unknown, o: unknown) => Promise }; + + const result = await governed.execute!({ city: "Tokyo" }, { toolCallId: "real-call-2" }); + + expect(result).toEqual({ ok: "allowed" }); + expect(executed).toHaveBeenCalledTimes(1); + }); +});