@atbash/atbash-langgraph 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # `@atbash/atbash-langgraph`
2
+
3
+ Add Atbash as a safety guard inside a LangGraph workflow.
4
+
5
+ This package adds a guard node before tool execution and an audit node after, using native LangGraph interrupt semantics for `HOLD` verdicts.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @atbash/atbash-langgraph
11
+ ```
12
+
13
+ Peer dependencies:
14
+
15
+ ```bash
16
+ npm install @langchain/core @langchain/langgraph
17
+ ```
18
+
19
+ ## When To Use It
20
+
21
+ Use this package when:
22
+
23
+ - your app already uses LangGraph
24
+ - your graph has a distinct `agent` phase and `tools` phase
25
+ - you want `HOLD` to pause execution using LangGraph interrupt semantics
26
+ - you want audit logging after the tool phase
27
+
28
+ ## Quick Start
29
+
30
+ ```ts
31
+ import { loadAgent } from "@atbash/sdk";
32
+ import { StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
33
+ import { AtbashStateAnnotation, addAtbashSafety } from "@atbash/atbash-langgraph";
34
+
35
+ const builder = new StateGraph(AtbashStateAnnotation)
36
+ .addNode("agent", agentNode)
37
+ .addNode("tools", toolsNode)
38
+ .addEdge(START, "agent")
39
+ .addConditionalEdges("agent", routeAfterAgent);
40
+
41
+ addAtbashSafety(builder, {
42
+ privkey: process.env.ATBASH_AGENT_PRIVKEY,
43
+ });
44
+
45
+ const app = builder.compile({ checkpointer: new MemorySaver() });
46
+ ```
47
+
48
+ ## API
49
+
50
+ ### `addAtbashSafety(builder, opts)`
51
+
52
+ Convenience helper that wires `atbash_guard` and `atbash_audit` into your graph.
53
+
54
+ Assumes your graph has nodes named `agent` and `tools`. If your layout differs, use `createGuardNode()` and `createAuditNode()` directly.
55
+
56
+ | Option | Type | Description |
57
+ |---|---|---|
58
+ | `privkey` | `string` | Agent private key (falls back to `ATBASH_AGENT_PRIVKEY` env var) |
59
+ | `agent` | `AgentAuth` | Pre-loaded agent (alternative to `privkey`) |
60
+ | `endpoint` | `string` | Override Atbash endpoint |
61
+
62
+ ### `createGuardNode(opts)`
63
+
64
+ Creates the pre-tool safety node. Calls `client.auditToolCall()` for every tool call in the last AI message.
65
+
66
+ Returns a node function that writes `atbashVerdict`, `atbashReason`, `atbashToolCallId` to state.
67
+
68
+ ### `createAuditNode(opts)`
69
+
70
+ Creates the post-tool audit node. Fire-and-forget call to `logToolCall()` — errors are suppressed so they never break the graph.
71
+
72
+ ### `AtbashStateAnnotation`
73
+
74
+ Extends `MessagesAnnotation` with Atbash fields:
75
+
76
+ | Field | Type | Description |
77
+ |---|---|---|
78
+ | `atbashVerdict` | `string \| null` | Last verdict from the guard node |
79
+ | `atbashReason` | `string \| null` | Reason from the judge |
80
+ | `atbashToolCallId` | `string \| null` | Tool call ID for polling or HOLD resume |
81
+ | `atbashConfidence` | `number \| null` | Confidence score |
82
+
83
+ ### `createJudgeTool(agent, endpoint?)`
84
+
85
+ Optional: creates an `atbash_safety_check` LangChain tool for direct LLM access to the judge.
86
+
87
+ ## Verdict Handling
88
+
89
+ | Verdict | Meaning | Graph Behavior |
90
+ |---|---|---|
91
+ | `ALLOW` | Safe to proceed | Routes to `tools` node |
92
+ | `HOLD` | Needs human review | Graph interrupts; resume with `Command({ resume: "approve" })` |
93
+ | `BLOCK` | Policy violation | Injects blocked `ToolMessage`; routes back to `agent` |
94
+ | `ERROR` | Judge unreachable | Treated as `BLOCK` — fail closed |
95
+
96
+ ## HOLD / Resume Pattern
97
+
98
+ ```ts
99
+ import { Command, isInterrupted } from "@langchain/langgraph";
100
+
101
+ const result = await app.invoke(input, config);
102
+
103
+ if (isInterrupted(result)) {
104
+ const payload = result.__interrupt__[0]?.value;
105
+ // show payload to operator ...
106
+
107
+ const approved = await app.invoke(
108
+ new Command({ resume: "approve" }),
109
+ config,
110
+ );
111
+ }
112
+ ```
113
+
114
+ ## Environment Variables
115
+
116
+ | Variable | Required | Description |
117
+ |---|---|---|
118
+ | `ATBASH_AGENT_PRIVKEY` | Yes (if not passing `agent`) | Your Atbash agent private key |
119
+ | `ATBASH_ENDPOINT` | No | Override the default Atbash endpoint (`https://atbash.ai`) |
120
+
121
+ ## What This Package Does Not Do
122
+
123
+ - It does not invent your graph structure.
124
+ - It does not automatically find your tool node if you use a custom layout.
125
+ - It does not execute operator review — it only exposes pause/resume mechanics.
126
+
127
+ ## Example
128
+
129
+ A runnable example is in [`examples/langgraph-runtime-agent/`](./examples/langgraph-runtime-agent/).
130
+
131
+ ```bash
132
+ npm install && npm run build
133
+ ATBASH_AGENT_PRIVKEY=your_key_here node examples/langgraph-runtime-agent/run.mjs
134
+ ```
@@ -0,0 +1,72 @@
1
+ import * as _langchain_core_messages from '@langchain/core/messages';
2
+ import * as _langchain_langgraph from '@langchain/langgraph';
3
+ import { StateGraph } from '@langchain/langgraph';
4
+ import { AtbashClient, AgentAuth, ClientOpts } from '@atbash/sdk';
5
+ import * as _langchain_core_tools from '@langchain/core/tools';
6
+ import { z } from 'zod';
7
+
8
+ declare const AtbashStateAnnotation: _langchain_langgraph.AnnotationRoot<{
9
+ atbashVerdict: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
10
+ atbashReason: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
11
+ atbashToolCallId: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
12
+ atbashConfidence: _langchain_langgraph.BaseChannel<number | null, number | _langchain_langgraph.OverwriteValue<number | null> | null, unknown>;
13
+ messages: _langchain_langgraph.BaseChannel<_langchain_core_messages.BaseMessage<_langchain_core_messages.MessageStructure<_langchain_core_messages.MessageToolSet>, _langchain_core_messages.MessageType>[], _langchain_langgraph.OverwriteValue<_langchain_core_messages.BaseMessage<_langchain_core_messages.MessageStructure<_langchain_core_messages.MessageToolSet>, _langchain_core_messages.MessageType>[]> | _langchain_langgraph.Messages, unknown>;
14
+ }>;
15
+ type AtbashState = typeof AtbashStateAnnotation.State;
16
+
17
+ interface GuardNodeOptions {
18
+ client: AtbashClient;
19
+ }
20
+ declare function createGuardNode(opts: GuardNodeOptions): (state: AtbashState) => Promise<Partial<AtbashState>>;
21
+
22
+ interface AuditNodeOptions {
23
+ agent: AgentAuth;
24
+ clientOpts?: ClientOpts;
25
+ }
26
+ declare function createAuditNode(opts: AuditNodeOptions): (state: AtbashState) => Promise<Partial<AtbashState>>;
27
+
28
+ interface AtbashSafetyOptions {
29
+ agent?: AgentAuth;
30
+ privkey?: string;
31
+ endpoint?: string;
32
+ toolsNode?: string;
33
+ agentNode?: string;
34
+ }
35
+ declare function addAtbashSafety(builder: StateGraph<AtbashState>, opts: AtbashSafetyOptions): StateGraph<_langchain_langgraph.StateType<{
36
+ atbashVerdict: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
37
+ atbashReason: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
38
+ atbashToolCallId: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
39
+ atbashConfidence: _langchain_langgraph.BaseChannel<number | null, number | _langchain_langgraph.OverwriteValue<number | null> | null, unknown>;
40
+ messages: _langchain_langgraph.BaseChannel<_langchain_core_messages.BaseMessage<_langchain_core_messages.MessageStructure<_langchain_core_messages.MessageToolSet>, _langchain_core_messages.MessageType>[], _langchain_langgraph.OverwriteValue<_langchain_core_messages.BaseMessage<_langchain_core_messages.MessageStructure<_langchain_core_messages.MessageToolSet>, _langchain_core_messages.MessageType>[]> | _langchain_langgraph.Messages, unknown>;
41
+ }>, _langchain_langgraph.StateType<{
42
+ atbashVerdict: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
43
+ atbashReason: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
44
+ atbashToolCallId: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
45
+ atbashConfidence: _langchain_langgraph.BaseChannel<number | null, number | _langchain_langgraph.OverwriteValue<number | null> | null, unknown>;
46
+ messages: _langchain_langgraph.BaseChannel<_langchain_core_messages.BaseMessage<_langchain_core_messages.MessageStructure<_langchain_core_messages.MessageToolSet>, _langchain_core_messages.MessageType>[], _langchain_langgraph.OverwriteValue<_langchain_core_messages.BaseMessage<_langchain_core_messages.MessageStructure<_langchain_core_messages.MessageToolSet>, _langchain_core_messages.MessageType>[]> | _langchain_langgraph.Messages, unknown>;
47
+ }>, Partial<_langchain_langgraph.StateType<{
48
+ atbashVerdict: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
49
+ atbashReason: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
50
+ atbashToolCallId: _langchain_langgraph.BaseChannel<string | null, string | _langchain_langgraph.OverwriteValue<string | null> | null, unknown>;
51
+ atbashConfidence: _langchain_langgraph.BaseChannel<number | null, number | _langchain_langgraph.OverwriteValue<number | null> | null, unknown>;
52
+ messages: _langchain_langgraph.BaseChannel<_langchain_core_messages.BaseMessage<_langchain_core_messages.MessageStructure<_langchain_core_messages.MessageToolSet>, _langchain_core_messages.MessageType>[], _langchain_langgraph.OverwriteValue<_langchain_core_messages.BaseMessage<_langchain_core_messages.MessageStructure<_langchain_core_messages.MessageToolSet>, _langchain_core_messages.MessageType>[]> | _langchain_langgraph.Messages, unknown>;
53
+ }>>, "__start__", _langchain_langgraph.StateDefinition, _langchain_langgraph.StateDefinition, _langchain_langgraph.StateDefinition, unknown, unknown, unknown>;
54
+
55
+ declare function createJudgeTool(agent: AgentAuth, endpoint?: string): _langchain_core_tools.DynamicStructuredTool<z.ZodObject<{
56
+ action: z.ZodString;
57
+ context: z.ZodString;
58
+ }, "strip", z.ZodTypeAny, {
59
+ action: string;
60
+ context: string;
61
+ }, {
62
+ action: string;
63
+ context: string;
64
+ }>, {
65
+ action: string;
66
+ context: string;
67
+ }, {
68
+ action: string;
69
+ context: string;
70
+ }, string, unknown, "atbash_safety_check">;
71
+
72
+ export { type AtbashSafetyOptions, type AtbashState, AtbashStateAnnotation, type AuditNodeOptions, type GuardNodeOptions, addAtbashSafety, createAuditNode, createGuardNode, createJudgeTool };
package/dist/index.js ADDED
@@ -0,0 +1,174 @@
1
+ // src/state.ts
2
+ import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
3
+ var AtbashStateAnnotation = Annotation.Root({
4
+ ...MessagesAnnotation.spec,
5
+ atbashVerdict: Annotation({
6
+ reducer: (_current, update) => update,
7
+ default: () => null
8
+ }),
9
+ atbashReason: Annotation({
10
+ reducer: (_current, update) => update,
11
+ default: () => null
12
+ }),
13
+ atbashToolCallId: Annotation({
14
+ reducer: (_current, update) => update,
15
+ default: () => null
16
+ }),
17
+ atbashConfidence: Annotation({
18
+ reducer: (_current, update) => update,
19
+ default: () => null
20
+ })
21
+ });
22
+
23
+ // src/nodes/guardNode.ts
24
+ import { ToolMessage } from "@langchain/core/messages";
25
+ import { isGraphBubbleUp } from "@langchain/langgraph";
26
+ function createGuardNode(opts) {
27
+ return async (state) => {
28
+ const lastMessage = state.messages[state.messages.length - 1];
29
+ const toolCalls = lastMessage?.tool_calls ?? [];
30
+ if (toolCalls.length === 0) {
31
+ return {
32
+ atbashVerdict: "ALLOW",
33
+ atbashReason: "No tool calls detected"
34
+ };
35
+ }
36
+ const actionText = toolCalls.map((toolCall) => `${toolCall.name}(${JSON.stringify(toolCall.args)})`).join("; ");
37
+ try {
38
+ const decision = await opts.client.auditToolCall({
39
+ toolName: toolCalls.map((t) => t.name).join(",") || "langgraph_batch",
40
+ args: toolCalls,
41
+ context: `LangGraph agent attempting: ${actionText}`
42
+ });
43
+ if (decision.verdict === "BLOCK" || decision.verdict === "ERROR") {
44
+ return {
45
+ messages: toolCalls.map(
46
+ (toolCall) => new ToolMessage({
47
+ tool_call_id: toolCall.id,
48
+ content: `BLOCKED by Atbash safety policy: ${decision.reason ?? "no reason"}`
49
+ })
50
+ ),
51
+ atbashVerdict: "BLOCK",
52
+ atbashReason: decision.reason ?? "blocked by Atbash",
53
+ atbashToolCallId: decision.toolCallId,
54
+ atbashConfidence: null
55
+ };
56
+ }
57
+ return {
58
+ atbashVerdict: "ALLOW",
59
+ atbashReason: decision.reason,
60
+ atbashToolCallId: decision.toolCallId,
61
+ atbashConfidence: null
62
+ };
63
+ } catch (error) {
64
+ if (isGraphBubbleUp(error)) {
65
+ throw error;
66
+ }
67
+ const reason = error instanceof Error ? error.message : "Safety check failed";
68
+ return {
69
+ messages: toolCalls.map(
70
+ (toolCall) => new ToolMessage({
71
+ tool_call_id: toolCall.id,
72
+ content: `Atbash safety check failed: ${reason}`
73
+ })
74
+ ),
75
+ atbashVerdict: "BLOCK",
76
+ atbashReason: reason,
77
+ atbashToolCallId: null,
78
+ atbashConfidence: null
79
+ };
80
+ }
81
+ };
82
+ }
83
+
84
+ // src/nodes/auditNode.ts
85
+ import { logToolCall } from "@atbash/sdk";
86
+
87
+ // src/utils.ts
88
+ function truncateText(value, max = 500) {
89
+ return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
90
+ }
91
+
92
+ // src/nodes/auditNode.ts
93
+ function createAuditNode(opts) {
94
+ return async (state) => {
95
+ const lastMessage = state.messages[state.messages.length - 1];
96
+ const content = typeof lastMessage?.content === "string" ? lastMessage.content : JSON.stringify(lastMessage?.content ?? "");
97
+ try {
98
+ await logToolCall(
99
+ truncateText(content, 500),
100
+ "LangGraph tool execution completed",
101
+ opts.agent,
102
+ void 0,
103
+ void 0,
104
+ opts.clientOpts
105
+ );
106
+ } catch {
107
+ }
108
+ return {};
109
+ };
110
+ }
111
+
112
+ // src/builder.ts
113
+ import {
114
+ createAtbashClient,
115
+ loadAgent
116
+ } from "@atbash/sdk";
117
+ function addAtbashSafety(builder, opts) {
118
+ const privkey = opts.privkey ?? process.env.ATBASH_AGENT_PRIVKEY;
119
+ const agent = opts.agent ?? loadAgent(privkey ?? "");
120
+ const clientOpts = opts.endpoint ? { endpoint: opts.endpoint } : void 0;
121
+ const client = createAtbashClient({
122
+ keyPair: { privKey: agent.privkey, pubKey: agent.pubkey },
123
+ judge: opts.endpoint ? { endpoint: opts.endpoint } : void 0
124
+ });
125
+ const graph = builder;
126
+ const toolsNode = opts.toolsNode ?? "tools";
127
+ const agentNode = opts.agentNode ?? "agent";
128
+ graph.addNode("atbash_guard", createGuardNode({ client }));
129
+ graph.addNode("atbash_audit", createAuditNode({ agent, clientOpts }));
130
+ graph.addConditionalEdges("atbash_guard", (state) => {
131
+ return state.atbashVerdict === "ALLOW" ? toolsNode : agentNode;
132
+ });
133
+ graph.addEdge(toolsNode, "atbash_audit");
134
+ graph.addEdge("atbash_audit", agentNode);
135
+ return builder;
136
+ }
137
+
138
+ // src/tools/judgeTool.ts
139
+ import { judgeAction } from "@atbash/sdk";
140
+ import { tool } from "@langchain/core/tools";
141
+ import { z } from "zod";
142
+ function createJudgeTool(agent, endpoint) {
143
+ return tool(
144
+ async ({ action, context }) => {
145
+ const result = await judgeAction(
146
+ action,
147
+ context,
148
+ agent,
149
+ endpoint ? { endpoint } : void 0
150
+ );
151
+ return JSON.stringify({
152
+ verdict: result.verdict,
153
+ reason: result.reason,
154
+ confidence: result.confidence,
155
+ tool_call_id: result.tool_call_id
156
+ });
157
+ },
158
+ {
159
+ name: "atbash_safety_check",
160
+ description: "Check whether an action is safe before executing it.",
161
+ schema: z.object({
162
+ action: z.string().describe("Action to evaluate"),
163
+ context: z.string().describe("Context for the action")
164
+ })
165
+ }
166
+ );
167
+ }
168
+ export {
169
+ AtbashStateAnnotation,
170
+ addAtbashSafety,
171
+ createAuditNode,
172
+ createGuardNode,
173
+ createJudgeTool
174
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@atbash/atbash-langgraph",
3
+ "version": "0.0.1",
4
+ "description": "Atbash safety guard and audit nodes for LangGraph workflows",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "keywords": [
19
+ "atbash",
20
+ "langgraph",
21
+ "langchain",
22
+ "ai-safety",
23
+ "agent-safety",
24
+ "guard",
25
+ "audit",
26
+ "policy"
27
+ ],
28
+ "license": "MIT",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup src/index.ts --format esm --dts --clean",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "dependencies": {
40
+ "@atbash/sdk": "^0.3.20",
41
+ "zod": "^3.25.76"
42
+ },
43
+ "peerDependencies": {
44
+ "@langchain/core": ">=0.3.0",
45
+ "@langchain/langgraph": ">=0.2.0"
46
+ },
47
+ "devDependencies": {
48
+ "@langchain/core": "^1.1.45",
49
+ "@langchain/langgraph": "^1.3.0",
50
+ "@types/node": "^25.7.0",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.0.0"
53
+ }
54
+ }