From 0161d3303b4cd2af5f0262f4edc03e01eec76250 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 10:10:34 +0800 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=90=9B=20(readme):=20Make=20both=20?= =?UTF-8?q?quickstart=20entrypoints=20run=20as=20written?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both snippets failed on execution. The ESM one wrapped LangChain tools with no gatewayClient, no mode and a default (fail-closed) enforcementMode, which trips the AAASM-4735 guard in init-assembly.ts and throws ConfigurationError. The CJS twin has the same config, and additionally used top-level await in a CommonJS file — a SyntaxError that fails before initAssembly is ever called, so fixing the config alone would have left it broken. Both now supply the gatewayClient that decides the wrapped tools, and the CJS sequence runs inside an async function. Verified by extracting each snippet verbatim from this file and executing it: both exit 0. Refs AAASM-5663 --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 11b29771c..d920b5a43 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,24 @@ npm install @agent-assembly/sdk @langchain/core ### ESM (`import`) ```ts -import { initAssembly } from "@agent-assembly/sdk"; +import { initAssembly, type GatewayClient } from "@agent-assembly/sdk"; + +// Decides every wrapped invoke(). This one is a local allow-list so the +// snippet runs offline; point `check` at a gateway you run to source +// decisions from there instead. +const policyClient: GatewayClient = { + mode: "sdk-only", + start: async () => undefined, + close: async () => undefined, + check: async (request) => + request.toolName === "search_web" + ? { denied: false } + : { denied: true, reason: "not on the allow-list" }, + waitForApproval: async () => ({ denied: false }), + record: async () => undefined, + recordResult: async () => undefined, + scanPrompts: async () => undefined +}; const searchWeb = { name: "search_web", @@ -97,10 +114,11 @@ const searchWeb = { const ctx = await initAssembly({ gatewayUrl: "http://localhost:7391", agentId: "demo", + gatewayClient: policyClient, langchain: { tools: { searchWeb } } }); -await searchWeb.invoke({ q: "agent assembly" }); // governed; throws on policy deny +console.log(await searchWeb.invoke({ q: "agent assembly" })); await ctx.shutdown(); ``` @@ -109,19 +127,43 @@ await ctx.shutdown(); ```js const { initAssembly } = require("@agent-assembly/sdk"); +// Decides every wrapped invoke(). This one is a local allow-list so the +// snippet runs offline; point `check` at a gateway you run to source +// decisions from there instead. +const policyClient = { + mode: "sdk-only", + start: async () => undefined, + close: async () => undefined, + check: async (request) => + request.toolName === "search_web" + ? { denied: false } + : { denied: true, reason: "not on the allow-list" }, + waitForApproval: async () => ({ denied: false }), + record: async () => undefined, + recordResult: async () => undefined, + scanPrompts: async () => undefined +}; + const searchWeb = { name: "search_web", invoke: async (input) => `results for ${input.q}` }; -const ctx = await initAssembly({ - gatewayUrl: "http://localhost:7391", - agentId: "demo", - langchain: { tools: { searchWeb } } -}); - -await searchWeb.invoke({ q: "agent assembly" }); -await ctx.shutdown(); +// CommonJS has no top-level await, so the init/shutdown sequence runs +// inside an async function. +async function main() { + const ctx = await initAssembly({ + gatewayUrl: "http://localhost:7391", + agentId: "demo", + gatewayClient: policyClient, + langchain: { tools: { searchWeb } } + }); + + console.log(await searchWeb.invoke({ q: "agent assembly" })); + await ctx.shutdown(); +} + +main(); ``` Both entrypoints resolve to the same governance pipeline; the package's `exports` field From b82687891b388534f978d1ed042b11d68810af18 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 10:10:53 +0800 Subject: [PATCH 02/14] =?UTF-8?q?=F0=9F=93=9D=20(readme):=20Bind=20the=20p?= =?UTF-8?q?olicy-check=20claims=20to=20what=20the=20config=20decides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "checked against gateway policy before it runs/before invocation" was false in the configuration this README documents: with no check-capable client the wrapper routes through the allow-all no-op client, so nothing is decided. The sweep found the same claim at three more sites the report did not name — the Quickstart lead-in, "auto-detects and governs", and "so every tool call is checked against policy before it runs" under How it works. All four now name the ADR 0033 §6 term the shown configuration earns, "denied before execution", and attribute the decision to the gatewayClient rather than to a gateway that is not in the picture. The fail-closed ConfigurationError is called a startup configuration check so a reader cannot read the refusal as a policy decision about a tool. Drops the banned absolutes "every"/"each". Refs AAASM-5663 --- README.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d920b5a43..aec4b0a74 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,16 @@ during `postinstall`. No additional build step is required for typical consumers ## Quickstart Pass your LangChain-style tools (`{ name, invoke }`) to `initAssembly` under -`langchain.tools`. Each tool is wrapped **in place** so every `invoke()` is checked -against gateway policy before it runs. +`langchain.tools`, along with the `gatewayClient` that decides them. Each tool is +wrapped **in place**: the wrapper asks that client for a decision before running the +tool body, so a call the client denies is **denied before execution** — `invoke()` +throws `PolicyViolationError` and the body does not run. + +The `gatewayClient` is what makes that decision real. Wrap tools without one and +`initAssembly` throws a `ConfigurationError` under its default fail-closed posture, +rather than route tool checks through the allow-all no-op client it would otherwise +use. That refusal is a startup configuration check — it is not a policy decision about +any tool, and nothing is inspected when it fires. The snippets below take the LangChain adapter path, which needs `@langchain/core`. It is an **optional** peer dependency, so `pnpm add @agent-assembly/sdk` does not @@ -170,8 +178,11 @@ Both entrypoints resolve to the same governance pipeline; the package's `exports selects ESM or CJS automatically based on how the consumer imports it. `initAssembly()` registers the LangChain callback handler and auto-wraps the configured -tools, so each is checked against gateway policy before invocation. For more frameworks -and the lower-level `withAssembly()` wrapper, see the **Examples** guide on the +tools, so each wrapped `invoke()` reaches your `gatewayClient` for a decision before the +tool body runs. What that decision is worth is whatever the client backs it with: the +allow-list above answers locally, while a client that consults a gateway you run carries +that gateway's verdict. For more frameworks and the lower-level `withAssembly()` +wrapper, see the **Examples** guide on the [documentation site](https://docs.agent-assembly.com/node-sdk/). ## Supported Node.js versions @@ -195,7 +206,7 @@ binding requires Node 18.18 or newer. ## Framework compatibility -`initAssembly()` auto-detects and governs five optional framework integrations +`initAssembly()` auto-detects and installs governance hooks for five optional framework integrations (LangChain.js, LangGraph.js, Vercel AI SDK, Mastra, OpenAI Agents). The full table — each framework's optional peer dependency, supported version range, and current status (including the [known Vercel AI SDK caveat](https://lightning-dust-mite.atlassian.net/browse/AAASM-3532)) — @@ -216,8 +227,10 @@ the runtime over one of two transports: - a **native in-process** binding built with napi-rs. `initAssembly()` is the primary entrypoint. It resolves the gateway, registers the agent, -and installs governance hooks for whichever supported framework it detects, so every tool -call is checked against policy before it runs. +and installs governance hooks for whichever supported framework it detects. What a hook +can do to a call depends on the gateway client deciding it — see +[How LangChain tools are blocked](#how-langchain-tools-are-blocked) for which layer +blocks and which only observes. ## What the package exports From c6376fa071b7264ae9724e25d2b98b995841777a Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 10:11:50 +0800 Subject: [PATCH 03/14] =?UTF-8?q?=F0=9F=90=9B=20(docs):=20Make=20the=2004-?= =?UTF-8?q?guides=20LangChain=20snippets=20run,=20and=20say=20what=20decid?= =?UTF-8?q?es=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit init-assembly.ts names this page as the concrete documented vector for the AAASM-4735 fail-open, and it carried the README's defect verbatim: the same false "every invoke() is checked against gateway policy before it runs" sentence, plus two snippets that wrap LangChain tools with no gatewayClient. Executed as written, the first throws ConfigurationError before reaching the wrap guard because it also omits gatewayUrl. Both snippets now supply the deciding client and a gateway URL, and the prose names the ADR 0033 §6 term the configuration earns. "the gateway is consulted on every call" becomes what the wrapper actually does — ask the configured client — since no gateway is consulted on this path. Refs AAASM-5663 --- docs/04-guides/index.md | 43 ++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/docs/04-guides/index.md b/docs/04-guides/index.md index db8eb1695..15e5af60d 100644 --- a/docs/04-guides/index.md +++ b/docs/04-guides/index.md @@ -32,11 +32,34 @@ SDK's own test suite. ## LangChain (validated) Install `@langchain/core` (a peer dependency). Pass your tools to `initAssembly` under -`langchain.tools`; each tool is wrapped **in place** so every `invoke()` is checked -against gateway policy before it runs. The callback handler is registered automatically. +`langchain.tools`, along with the `gatewayClient` that decides them; each tool is wrapped +**in place**, so a call that client denies is **denied before execution** — the wrapper +throws `PolicyViolationError` and the tool body does not run. The callback handler is +registered automatically. + +Wrapping tools without a client that can decide them is refused: under the default +fail-closed posture `initAssembly` throws a `ConfigurationError` rather than route tool +checks through the allow-all no-op client. That refusal is a startup configuration +check, not a policy decision about a tool. ```ts -import { initAssembly } from "@agent-assembly/sdk"; +import { initAssembly, type GatewayClient } from "@agent-assembly/sdk"; + +// Decides every wrapped invoke(). This one is a local allow-list so the snippet +// runs offline; point `check` at a gateway you run to source decisions from there. +const policyClient: GatewayClient = { + mode: "sdk-only", + start: async () => undefined, + close: async () => undefined, + check: async (request) => + request.toolName === "search_web" + ? { denied: false } + : { denied: true, reason: "not on the allow-list" }, + waitForApproval: async () => ({ denied: false }), + record: async () => undefined, + recordResult: async () => undefined, + scanPrompts: async () => undefined +}; // A LangChain-style tool is any object with { name, invoke }. const searchWeb = { @@ -47,15 +70,18 @@ const searchWeb = { }; const ctx = await initAssembly({ + gatewayUrl: "http://localhost:7391", agentId: "demo", + gatewayClient: policyClient, langchain: { tools: { searchWeb }, approvalTimeoutMs: 30_000 // optional; how long to wait on a "pending" decision } }); -// Governed: if policy denies the call, invoke() rejects with a PolicyViolationError. -await searchWeb.invoke({ q: "agent assembly" }); +// policyClient allows search_web, so this runs and returns. A tool it denies +// would reject with PolicyViolationError instead. +console.log(await searchWeb.invoke({ q: "agent assembly" })); await ctx.shutdown(); ``` @@ -179,8 +205,8 @@ For the full list of configuration fields used above, see ## Handling allow / deny decisions and errors When you wrap a tool — whether through `initAssembly`'s `langchain.tools` or directly -with `withAssembly` — the gateway is consulted on every call. The outcome shows up as -ordinary async control flow: +with `withAssembly` — the wrapper asks your `gatewayClient` for a decision before it +runs the tool body. The outcome shows up as ordinary async control flow: - **Allow.** The wrapped call runs the real tool and returns its result. Nothing extra to handle. @@ -196,8 +222,11 @@ Because these surface as rejected promises, you handle them with a normal ```ts import { initAssembly } from "@agent-assembly/sdk"; +// policyClient and searchWeb are the ones defined in the LangChain section above. const ctx = await initAssembly({ + gatewayUrl: "http://localhost:7391", agentId: "demo", + gatewayClient: policyClient, langchain: { tools: { searchWeb }, approvalTimeoutMs: 30_000 // how long to wait on a "pending" decision From 115e892504384b9cf6ff5eaf3a96ae0ca0cc1548 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 10:11:50 +0800 Subject: [PATCH 04/14] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Qualify=20the=20?= =?UTF-8?q?introduction's=20before-it-runs=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "every tool an agent calls is checked against policy before it runs" is the same claim the README made and is false on the default path. The paragraph after it already qualified the audit half honestly (AAASM-5681); this gives the enforcement half the same treatment and drops the banned absolute. Refs AAASM-5663 --- docs/01-introduction/index.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/01-introduction/index.md b/docs/01-introduction/index.md index 1ff5636a0..70733b14f 100644 --- a/docs/01-introduction/index.md +++ b/docs/01-introduction/index.md @@ -13,9 +13,14 @@ agent. **`@agent-assembly/sdk`** is the TypeScript and Node.js SDK for [Agent Assembly](https://github.com/ai-agent-assembly). It lets you put a -governance layer in front of the AI agents you build in Node — so every tool an -agent calls is checked against policy *before* it runs, and every governance-relevant -action is emitted as an audit event. +governance layer in front of the AI agents you build in Node — so a tool you wrap +reaches the gateway client you configure for a decision *before* its body runs, and +governance-relevant actions are emitted as audit events. + +What that buys depends on the client. Wrapped tools decided by a client that can +answer authoritatively are **denied before execution** when it denies them; wrap +tools without such a client and `initAssembly` refuses to start rather than route +checks through the allow-all no-op client. Whether those events are *retained* depends on which gateway client you use. Both clients this SDK ships discard hook-layer audit events, so on the default path From eed03d33cd1646bc8cb4463316a6dddd85478ba8 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 10:39:57 +0800 Subject: [PATCH 05/14] =?UTF-8?q?=F0=9F=9A=A8=20(docs):=20Drop=20banned=20?= =?UTF-8?q?absolutes=20and=20a=20duplicated=20note=20from=20the=20new=20pr?= =?UTF-8?q?ose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prose added for this ticket reintroduced the fd-7 absolutes it was meant to remove ("Decides every wrapped invoke()", "each tool is wrapped"). Reworded to state what the wrapper does without quantifying over calls. The 04-guides paragraph on the fail-closed refusal also restated the ":::note[In-process tool enforcement needs a check-capable mode]" admonition sitting directly under the same snippet; replaced with a pointer to it. Refs AAASM-5663 --- README.md | 16 ++++++++-------- docs/04-guides/index.md | 14 +++++--------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index aec4b0a74..b2af16d43 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ during `postinstall`. No additional build step is required for typical consumers ## Quickstart Pass your LangChain-style tools (`{ name, invoke }`) to `initAssembly` under -`langchain.tools`, along with the `gatewayClient` that decides them. Each tool is +`langchain.tools`, along with the `gatewayClient` that decides them. Tools are wrapped **in place**: the wrapper asks that client for a decision before running the tool body, so a call the client denies is **denied before execution** — `invoke()` throws `PolicyViolationError` and the body does not run. @@ -97,9 +97,9 @@ npm install @agent-assembly/sdk @langchain/core ```ts import { initAssembly, type GatewayClient } from "@agent-assembly/sdk"; -// Decides every wrapped invoke(). This one is a local allow-list so the -// snippet runs offline; point `check` at a gateway you run to source -// decisions from there instead. +// The wrapper calls this before it runs a wrapped tool body. This one is a +// local allow-list so the snippet runs offline; point `check` at a gateway +// you run to source decisions from there instead. const policyClient: GatewayClient = { mode: "sdk-only", start: async () => undefined, @@ -135,9 +135,9 @@ await ctx.shutdown(); ```js const { initAssembly } = require("@agent-assembly/sdk"); -// Decides every wrapped invoke(). This one is a local allow-list so the -// snippet runs offline; point `check` at a gateway you run to source -// decisions from there instead. +// The wrapper calls this before it runs a wrapped tool body. This one is a +// local allow-list so the snippet runs offline; point `check` at a gateway +// you run to source decisions from there instead. const policyClient = { mode: "sdk-only", start: async () => undefined, @@ -178,7 +178,7 @@ Both entrypoints resolve to the same governance pipeline; the package's `exports selects ESM or CJS automatically based on how the consumer imports it. `initAssembly()` registers the LangChain callback handler and auto-wraps the configured -tools, so each wrapped `invoke()` reaches your `gatewayClient` for a decision before the +tools, so a wrapped `invoke()` reaches your `gatewayClient` for a decision before the tool body runs. What that decision is worth is whatever the client backs it with: the allow-list above answers locally, while a client that consults a gateway you run carries that gateway's verdict. For more frameworks and the lower-level `withAssembly()` diff --git a/docs/04-guides/index.md b/docs/04-guides/index.md index 15e5af60d..fbb23c1c4 100644 --- a/docs/04-guides/index.md +++ b/docs/04-guides/index.md @@ -32,21 +32,17 @@ SDK's own test suite. ## LangChain (validated) Install `@langchain/core` (a peer dependency). Pass your tools to `initAssembly` under -`langchain.tools`, along with the `gatewayClient` that decides them; each tool is wrapped +`langchain.tools`, along with the `gatewayClient` that decides them; tools are wrapped **in place**, so a call that client denies is **denied before execution** — the wrapper throws `PolicyViolationError` and the tool body does not run. The callback handler is -registered automatically. - -Wrapping tools without a client that can decide them is refused: under the default -fail-closed posture `initAssembly` throws a `ConfigurationError` rather than route tool -checks through the allow-all no-op client. That refusal is a startup configuration -check, not a policy decision about a tool. +registered automatically. Wrapping tools without such a client is refused at startup — +see the note below the snippet. ```ts import { initAssembly, type GatewayClient } from "@agent-assembly/sdk"; -// Decides every wrapped invoke(). This one is a local allow-list so the snippet -// runs offline; point `check` at a gateway you run to source decisions from there. +// The wrapper calls this before it runs a wrapped tool body. This one is a local +// allow-list so the snippet runs offline; point `check` at a gateway you run instead. const policyClient: GatewayClient = { mode: "sdk-only", start: async () => undefined, From 4dbfc3de4b40409077670ce0808ce671a728ead2 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 10:39:57 +0800 Subject: [PATCH 06/14] =?UTF-8?q?=E2=9C=85=20(tests):=20Correct=20the=20cl?= =?UTF-8?q?aim=20binding=20justification=20this=20ticket=20invalidated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AAASM-5529 gate registered "The SDK enforces policy by talking to an Agent Assembly gateway" as unproven, justified by the README entrypoints not running as written. That premise no longer holds once this ticket fixes them, so the justification is restated on the ground that still stands: the controls decide tool calls through a caller-supplied gatewayClient answering in-process, which proves a wrapped call is decided before its body runs, not that a gateway is the deciding party. The quote is unchanged, so the gate's whole-sentence equality match is unaffected. Refs AAASM-5663 --- tests/quickstart-claim-bindings.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/quickstart-claim-bindings.test.ts b/tests/quickstart-claim-bindings.test.ts index 88d42fa7e..b06e49e8f 100644 --- a/tests/quickstart-claim-bindings.test.ts +++ b/tests/quickstart-claim-bindings.test.ts @@ -116,9 +116,13 @@ const BINDINGS: readonly ClaimBinding[] = [ id: "sdk-enforces-by-talking-to-a-gateway", quote: "The SDK enforces policy by talking to an Agent Assembly **gateway**.", unprovenReason: - "AAASM-5663: both README entrypoints were executed and neither runs as written, so no " + - "control covers a reader actually reaching a gateway. The control that exists asserts " + - "the documented config REFUSES to init; it does not prove this sentence." + "AAASM-5663: no control covers a reader actually reaching a gateway. Every control " + + "that exists decides tool calls through a caller-supplied gatewayClient answering " + + "in-process, which is what the documented snippets now do — so they prove a wrapped " + + "call is decided before its body runs, not that the deciding party is a gateway. " + + "This reason previously said the README entrypoints do not run as written; they were " + + "fixed under this ticket and now execute, which changes nothing about this sentence, " + + "because running is not talking to a gateway." }, { id: "auto-start-is-opt-in", From 808be859286f30e321ad3cdd9f0b8634d54c1033 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 10:52:37 +0800 Subject: [PATCH 07/14] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Attribute=20the?= =?UTF-8?q?=20deny/pending=20outcome=20to=20the=20client=20that=20decides?= =?UTF-8?q?=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The allow/deny/pending walkthrough still credited "the gateway" for the verdict and the reason string, one paragraph below the sentence corrected earlier in this ticket. On this path the decision comes from the configured gatewayClient, so the surrounding text now says so consistently. Refs AAASM-5663 --- docs/04-guides/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/04-guides/index.md b/docs/04-guides/index.md index fbb23c1c4..323503496 100644 --- a/docs/04-guides/index.md +++ b/docs/04-guides/index.md @@ -82,7 +82,7 @@ console.log(await searchWeb.invoke({ q: "agent assembly" })); await ctx.shutdown(); ``` -When the gateway returns a **deny**, the wrapped call throws `PolicyViolationError`. When +When the client returns a **deny**, the wrapped call throws `PolicyViolationError`. When it returns **pending**, the call waits up to `approvalTimeoutMs` for a decision and then either proceeds or throws. @@ -207,8 +207,8 @@ runs the tool body. The outcome shows up as ordinary async control flow: - **Allow.** The wrapped call runs the real tool and returns its result. Nothing extra to handle. - **Deny.** The wrapped call **rejects** with a `PolicyViolationError`. The tool body - never runs. The error message carries the tool name and the gateway's stated reason. -- **Pending → resolved.** If the gateway needs a human, the call waits up to + never runs. The error message carries the tool name and the reason the client gave. +- **Pending → resolved.** If the decision needs a human, the call waits up to `approvalTimeoutMs` for a decision and then either proceeds (approved) or rejects (denied / timed out). From f4f06d12a9732e64ca28f624fcdaf73262921bdc Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 11:08:30 +0800 Subject: [PATCH 08/14] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Say=20which=20co?= =?UTF-8?q?mponent=20decides=20the=20call=20in=20the=20introduction=20walk?= =?UTF-8?q?through?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "The SDK wraps each tool so the gateway sees the call first" is the same misattribution corrected elsewhere in this ticket, sitting three paragraphs below one of those corrections. On this path the deciding party is the configured gateway client, not a gateway. Also rewraps the Framework-compatibility line this ticket had pushed past the file's wrap width. Refs AAASM-5663 --- README.md | 4 ++-- docs/01-introduction/index.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b2af16d43..adcf70b66 100644 --- a/README.md +++ b/README.md @@ -206,8 +206,8 @@ binding requires Node 18.18 or newer. ## Framework compatibility -`initAssembly()` auto-detects and installs governance hooks for five optional framework integrations -(LangChain.js, LangGraph.js, Vercel AI SDK, Mastra, OpenAI Agents). The full table — +`initAssembly()` auto-detects and installs governance hooks for five optional framework +integrations (LangChain.js, LangGraph.js, Vercel AI SDK, Mastra, OpenAI Agents). The full table — each framework's optional peer dependency, supported version range, and current status (including the [known Vercel AI SDK caveat](https://lightning-dust-mite.atlassian.net/browse/AAASM-3532)) — is the **authoritative** reference and lives on the docs site: diff --git a/docs/01-introduction/index.md b/docs/01-introduction/index.md index 70733b14f..70b44c5b6 100644 --- a/docs/01-introduction/index.md +++ b/docs/01-introduction/index.md @@ -45,10 +45,10 @@ In practice the SDK is two things working together: your policies and renders allow / deny / approval decisions. The SDK can even auto-start a local gateway for you so there is nothing to stand up by hand. -You write your agent the way you normally would. The SDK wraps each tool so the -gateway sees the call first: if policy **allows** it, the tool runs; if it **denies** -it, the call throws instead of executing; if it needs a human, the call waits for an -approval decision. +You write your agent the way you normally would. The SDK wraps the tools you hand it +so the gateway client you configured decides the call first: if it **allows**, the +tool runs; if it **denies**, the call throws instead of executing; if it needs a +human, the call waits for an approval decision. ## Who this is for From 05e4ac31a1e2bacd8a3deeda7f43b8a6cbcfbc6b Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 11:13:26 +0800 Subject: [PATCH 09/14] =?UTF-8?q?=E2=9C=85=20(tests):=20Repoint=20the=20ga?= =?UTF-8?q?teway=20claim's=20unproven=20referent=20to=20AAASM-5758?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reason on "The SDK enforces policy by talking to an Agent Assembly gateway" is a FORWARD pointer — "no control covers a reader actually reaching a gateway" — not a citation for where the claim was last examined. Naming AAASM-5663 sent a reader to a ticket that fixes README snippets and never intended to deliver gateway coverage, and that pointer goes stale the moment this work merges. AAASM-5758 ("CI job running each documented SDK quick-start from a clean environment, using published artifacts only") owns the gap, is still open, and carries the node-sdk, python-sdk and go-sdk components — so the three SDKs name one referent rather than three. It lists AAASM-5663 among its blockers, which is the right direction: this ticket unblocks that one. The reason now says outright that it is a forward pointer, so the next reader does not have to infer which kind of reference it is. The bound quote is unchanged, so the gate's whole-sentence equality match is unaffected. Refs AAASM-5663, AAASM-5758 --- tests/quickstart-claim-bindings.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/quickstart-claim-bindings.test.ts b/tests/quickstart-claim-bindings.test.ts index b06e49e8f..f123664ff 100644 --- a/tests/quickstart-claim-bindings.test.ts +++ b/tests/quickstart-claim-bindings.test.ts @@ -116,13 +116,14 @@ const BINDINGS: readonly ClaimBinding[] = [ id: "sdk-enforces-by-talking-to-a-gateway", quote: "The SDK enforces policy by talking to an Agent Assembly **gateway**.", unprovenReason: - "AAASM-5663: no control covers a reader actually reaching a gateway. Every control " + + "AAASM-5758: no control covers a reader actually reaching a gateway. Every control " + "that exists decides tool calls through a caller-supplied gatewayClient answering " + - "in-process, which is what the documented snippets now do — so they prove a wrapped " + + "in-process, which is what the documented snippets do — so they prove a wrapped " + "call is decided before its body runs, not that the deciding party is a gateway. " + - "This reason previously said the README entrypoints do not run as written; they were " + - "fixed under this ticket and now execute, which changes nothing about this sentence, " + - "because running is not talking to a gateway." + "Closing this needs a CI job that runs each documented quick-start from a clean " + + "environment against published artifacts only, which AAASM-5758 owns. This is a " + + "forward pointer to the work that would prove the sentence, not a citation for " + + "where it was last examined." }, { id: "auto-start-is-opt-in", From 6bcb886d13e7eb422737e8fa578df6cdf3e47783 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 11:34:56 +0800 Subject: [PATCH 10/14] =?UTF-8?q?=F0=9F=90=9B=20(docs):=20Scope=20the=20fa?= =?UTF-8?q?il-closed=20refusal=20to=20langchain.tools=20in=20the=20introdu?= =?UTF-8?q?ction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sentence THIS BRANCH added was false. "wrap tools without such a client and initAssembly refuses to start" is unscoped, but the refusal fires only for explicit langchain.tools: wrapsToolsThroughGatewayClient (init-assembly.ts :217-220) reads config.langchain?.tools and nothing else, and the guard at :755-762 is &&-gated on it. init-assembly.ts:538-540 states the opposite for everything else — auto-detected frameworks have no init-time throw. An in-repo control falsifies the sentence directly: tests/auto-detected-noop- enforcement-warning.test.ts:59-66 calls initAssembly with no gatewayClient under default fail-closed enforcement and asserts ctx IS defined. The README paragraph keeps the langchain.tools conjunct because it opens on it; dropping it here made a true sentence false. Now scoped, and says what the other path does instead: it warns and proceeds. Also corrects the plain-terms opener, which promised "an agent can only do what your rules allow" and "a record of everything it did" — both false on the default path, and both already contradicted by this file's own lines 34-38 and 65-67. The opener now defers to them instead of contradicting them. Refs AAASM-5663 --- docs/01-introduction/index.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/01-introduction/index.md b/docs/01-introduction/index.md index 70b44c5b6..a2ff8599a 100644 --- a/docs/01-introduction/index.md +++ b/docs/01-introduction/index.md @@ -6,10 +6,11 @@ sidebar_position: 1 # Introduction **In plain terms:** AI agents take actions on their own — searching the web, calling -APIs, reading files. This SDK puts a checkpoint in front of those actions so an agent -can only do what your rules allow, and so there's a record of everything it did. You add -it to an agent built in Node.js with a few lines of code; you don't have to rewrite the -agent. +APIs, reading files. This SDK puts a checkpoint in front of those actions, so a tool you +wrap is decided by a policy client before its body runs. How much that checkpoint is +worth depends on how you configure it: the client the SDK falls back to allows +everything and keeps no record, which the two sections below spell out. You add it to an +agent built in Node.js with a few lines of code; you don't have to rewrite the agent. **`@agent-assembly/sdk`** is the TypeScript and Node.js SDK for [Agent Assembly](https://github.com/ai-agent-assembly). It lets you put a @@ -17,10 +18,12 @@ governance layer in front of the AI agents you build in Node — so a tool you w reaches the gateway client you configure for a decision *before* its body runs, and governance-relevant actions are emitted as audit events. -What that buys depends on the client. Wrapped tools decided by a client that can -answer authoritatively are **denied before execution** when it denies them; wrap -tools without such a client and `initAssembly` refuses to start rather than route -checks through the allow-all no-op client. +What that buys depends on the client. Tools decided by a client that can answer +authoritatively are **denied before execution** when it denies them. Pass explicit +`langchain.tools` without such a client and `initAssembly` refuses to start, rather +than route their checks through the allow-all no-op client. For a framework it +auto-detects instead, there is no such refusal — it warns and proceeds, so that +installing a dependency does not break a zero-config startup. Whether those events are *retained* depends on which gateway client you use. Both clients this SDK ships discard hook-layer audit events, so on the default path From be84d23521388d747638b47a815a198a4ea717e7 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 11:34:56 +0800 Subject: [PATCH 11/14] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Say=20which=20co?= =?UTF-8?q?mponent=20decides=20the=20call=20in=20core=20concepts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "the tools you handed it are wrapped so the gateway evaluates each call before it executes" is the untouched twin of the introduction sentence already fixed under this ticket. No gateway evaluates anything on the default path — the fallback client answers in-process and allows everything. The mermaid flowchart directly above draws SDK to aa-ffi-node to gateway, so the sentence reads as a product claim rather than a description of one configuration; the replacement says which path the diagram depicts and what the fallback client does instead. Refs AAASM-5663 --- docs/03-core-concepts/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/03-core-concepts/index.md b/docs/03-core-concepts/index.md index 8f035fad9..0edacc33e 100644 --- a/docs/03-core-concepts/index.md +++ b/docs/03-core-concepts/index.md @@ -20,7 +20,9 @@ flowchart LR ``` You call `initAssembly(...)` once. From then on, the tools you handed it are wrapped -so the gateway evaluates each call before it executes. +so the gateway client you configured decides a call before it executes. The path drawn +above is the one a client that reaches the gateway takes; the client this SDK falls back +to when you supply none answers in-process and allows everything. The gateway, the policy engine, and the audit trail live in the core runtime. For the platform-level picture — how the gateway renders decisions and how this SDK relates to From d36e4f0ce094db393097755a62695f81c0ddd908 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 11:34:56 +0800 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Drop=20"governs"?= =?UTF-8?q?=20from=20the=20authoritative=20compatibility=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Byte-for-byte the sentence already corrected in the README, left behind on the page README.md:213-214 calls "the authoritative reference" — so the derived copy was fixed and the authoritative one was not. "Governs" is exactly the undifferentiated verb ADR 0033 §6 tells downstream material not to use. Refs AAASM-5663 --- docs/07-compatibility-versioning/compatibility.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/07-compatibility-versioning/compatibility.md b/docs/07-compatibility-versioning/compatibility.md index 272fb0ab8..5aa8ec7ae 100644 --- a/docs/07-compatibility-versioning/compatibility.md +++ b/docs/07-compatibility-versioning/compatibility.md @@ -29,7 +29,8 @@ requires a Rust toolchain. See [Troubleshooting](../08-troubleshooting/index.md) ## Frameworks -`initAssembly()` auto-detects and governs the agent frameworks below. Each is an +`initAssembly()` auto-detects and installs governance hooks for the agent frameworks +below. Each is an **optional** peer dependency — the SDK works without any of them installed, and only hooks into the ones it finds at runtime. The version floors are the major lines the governance hooks are built against and verified in the cross-repo live smokes; newer From ed813a2509fd74d9cc26629db6e06da27c6b789a Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 11:34:56 +0800 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Attribute=20the?= =?UTF-8?q?=20PolicyViolationError=20to=20the=20client=20that=20denied=20i?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "the gateway denied the tool call ... the gateway's reason" is the same misattribution corrected in 04-guides, phrased differently enough that a phrase-pattern sweep did not reach it. Found by reading the file. Refs AAASM-5663 --- docs/08-troubleshooting/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/08-troubleshooting/index.md b/docs/08-troubleshooting/index.md index 2ed174df2..1b47fabd3 100644 --- a/docs/08-troubleshooting/index.md +++ b/docs/08-troubleshooting/index.md @@ -63,8 +63,9 @@ agent under the wrong governance posture. See [Configuration](../05-configuratio ## `PolicyViolationError` at tool call time -This is expected behavior, not a bug: the gateway **denied** the tool call (or an approval -request timed out). The message includes the tool name and the gateway's reason. To run an +This is expected behavior, not a bug: the gateway client deciding the call **denied** it +(or an approval request timed out). The message includes the tool name and the reason that +client gave. To run an agent without blocking while you tune policy, register it with `enforcementMode: "observe"` — actions proceed and would-be violations are recorded as shadow audit events. From c4476eaea649ca4b8b39e97dc354e6db12a4b939 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 14 Aug 2026 11:34:57 +0800 Subject: [PATCH 14/14] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Stop=20crediting?= =?UTF-8?q?=20the=20core=20runtime=20for=20decisions=20this=20SDK=20may=20?= =?UTF-8?q?not=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "The governance decisions it enforces are made by the core Rust runtime" is false in the configuration this README now documents, where the decision comes from a caller-supplied client answering in-process. Narrowed to the authoritative decisions, which is the claim that holds. Refs AAASM-5663 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index adcf70b66..df540a681 100644 --- a/README.md +++ b/README.md @@ -401,8 +401,8 @@ and is re-published on every push to `main` via the `publish-docs.yml` workflow. ## Related projects -`@agent-assembly/sdk` is one client of the Agent Assembly platform. The governance -decisions it enforces are made by the core Rust runtime; the protocol it speaks is shared +`@agent-assembly/sdk` is one client of the Agent Assembly platform. The authoritative +governance decisions are made by the core Rust runtime; the protocol it speaks is shared across all SDKs. | Project | What it is |