Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
0161d33
๐Ÿ› (readme): Make both quickstart entrypoints run as written
Chisanan232 Aug 14, 2026
b826878
๐Ÿ“ (readme): Bind the policy-check claims to what the config decides
Chisanan232 Aug 14, 2026
c6376fa
๐Ÿ› (docs): Make the 04-guides LangChain snippets run, and say what decโ€ฆ
Chisanan232 Aug 14, 2026
115e892
๐Ÿ“ (docs): Qualify the introduction's before-it-runs claim
Chisanan232 Aug 14, 2026
eed03d3
๐Ÿšจ (docs): Drop banned absolutes and a duplicated note from the new prose
Chisanan232 Aug 14, 2026
4dbfc3d
โœ… (tests): Correct the claim binding justification this ticket invaliโ€ฆ
Chisanan232 Aug 14, 2026
808be85
๐Ÿ“ (docs): Attribute the deny/pending outcome to the client that decidโ€ฆ
Chisanan232 Aug 14, 2026
f4f06d1
๐Ÿ“ (docs): Say which component decides the call in the introduction waโ€ฆ
Chisanan232 Aug 14, 2026
05e4ac3
โœ… (tests): Repoint the gateway claim's unproven referent to AAASM-5758
Chisanan232 Aug 14, 2026
6bcb886
๐Ÿ› (docs): Scope the fail-closed refusal to langchain.tools in the intโ€ฆ
Chisanan232 Aug 14, 2026
be84d23
๐Ÿ“ (docs): Say which component decides the call in core concepts
Chisanan232 Aug 14, 2026
d36e4f0
๐Ÿ“ (docs): Drop "governs" from the authoritative compatibility page
Chisanan232 Aug 14, 2026
ed813a2
๐Ÿ“ (docs): Attribute the PolicyViolationError to the client that denieโ€ฆ
Chisanan232 Aug 14, 2026
c4476ea
๐Ÿ“ (docs): Stop crediting the core runtime for decisions this SDK may โ€ฆ
Chisanan232 Aug 14, 2026
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
95 changes: 75 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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.

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
Expand All @@ -87,7 +95,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";

// 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,
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",
Expand All @@ -97,10 +122,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();
```

Expand All @@ -109,27 +135,54 @@ await ctx.shutdown();
```js
const { initAssembly } = require("@agent-assembly/sdk");

// 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,
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
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 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()`
wrapper, see the **Examples** guide on the
[documentation site](https://docs.agent-assembly.com/node-sdk/).

## Supported Node.js versions
Expand All @@ -153,8 +206,8 @@ binding requires Node 18.18 or newer.

## Framework compatibility

`initAssembly()` auto-detects and governs 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:
Expand All @@ -174,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

Expand Down Expand Up @@ -346,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 |
Expand Down
30 changes: 19 additions & 11 deletions docs/01-introduction/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@ 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
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. 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
Expand All @@ -40,10 +48,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

Expand Down
4 changes: 3 additions & 1 deletion docs/03-core-concepts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 35 additions & 10 deletions docs/04-guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,30 @@ 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; 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 such a client is refused at startup โ€”
see the note below the snippet.

```ts
import { initAssembly } from "@agent-assembly/sdk";
import { initAssembly, type GatewayClient } from "@agent-assembly/sdk";

// 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,
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 = {
Expand All @@ -47,20 +66,23 @@ 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();
```

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.

Expand Down Expand Up @@ -179,14 +201,14 @@ 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.
- **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).

Expand All @@ -196,8 +218,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
Expand Down
3 changes: 2 additions & 1 deletion docs/07-compatibility-versioning/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/08-troubleshooting/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 8 additions & 3 deletions tests/quickstart-claim-bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +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: 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-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 do โ€” so they prove a wrapped " +
"call is decided before its body runs, not that the deciding party is 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",
Expand Down
Loading