@orangecheck/agent-langgraph 0.2.0 → 1.0.0

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # @orangecheck/agent-langgraph
2
2
 
3
+ > **Full reference:** [docs.ochk.io/sdk/agent-langgraph](https://docs.ochk.io/sdk/agent-langgraph) — auto-generated from the TypeScript source on every release.
4
+ > Hand-written prose below is the high-level overview; the docs site is the source of truth for every export, type, and signature.
5
+
6
+
3
7
  Wrap [LangGraph](https://langchain-ai.github.io/langgraph/) tool-node executions in [OC Agent](https://github.com/orangecheck/oc-agent-protocol) action envelopes. Binds each action to the **graph-state hash** at execution time — so verifiers replaying the audit against a persisted graph snapshot can confirm the action fired against the expected state, not a divergent fork.
4
8
 
5
9
  ## Why
@@ -90,4 +94,3 @@ MIT.
90
94
  - [oc-agent-protocol](https://github.com/orangecheck/oc-agent-protocol) — normative wire format.
91
95
  - [@orangecheck/agent-core](https://www.npmjs.com/package/@orangecheck/agent-core) — verifier library.
92
96
  - [@orangecheck/agent-mcp](https://www.npmjs.com/package/@orangecheck/agent-mcp) / [-anthropic](https://www.npmjs.com/package/@orangecheck/agent-anthropic) / [-openai](https://www.npmjs.com/package/@orangecheck/agent-openai) / [-vercel](https://www.npmjs.com/package/@orangecheck/agent-vercel) — sibling adapters, all stateless.
93
- - [fleet.ochk.io/integrations/langgraph](https://fleet.ochk.io/integrations/langgraph) — managed-tier story.
package/dist/index.d.mts CHANGED
@@ -30,25 +30,11 @@ interface OcToolNodeWrapped<TArgs extends Record<string, unknown>, TResult> {
30
30
  execute: (args: TArgs, ctx: AgentContext & {
31
31
  callId: string;
32
32
  graphState: Record<string, unknown>;
33
- fleet?: FleetClient;
34
33
  }) => Promise<{
35
34
  result: TResult;
36
35
  action: ActionEnvelope;
37
- posted: PostActionResult | null;
38
36
  }>;
39
37
  }
40
38
  declare function ocToolNode<TArgs extends Record<string, unknown>, TResult>(input: OcToolNodeInput<TArgs, TResult>): OcToolNodeWrapped<TArgs, TResult>;
41
- interface FleetClient {
42
- baseUrl?: string;
43
- apiToken: string;
44
- projectId: string;
45
- fetch?: typeof fetch;
46
- }
47
- interface PostActionResult {
48
- id: string;
49
- project_id: string;
50
- delegation_id: string;
51
- }
52
- declare function postActionToFleet(action: ActionEnvelope, client: FleetClient): Promise<PostActionResult>;
53
39
 
54
- export { type AgentContext, type FleetClient, type LangGraphToolCall, type OcToolNodeInput, type OcToolNodeWrapped, type PostActionResult, type StampToolCallInput, canonicalizeToolCall, graphStateHash, ocToolNode, postActionToFleet, stampToolCall, toolCallActionId, toolCallHash };
40
+ export { type AgentContext, type LangGraphToolCall, type OcToolNodeInput, type OcToolNodeWrapped, type StampToolCallInput, canonicalizeToolCall, graphStateHash, ocToolNode, stampToolCall, toolCallActionId, toolCallHash };
package/dist/index.d.ts CHANGED
@@ -30,25 +30,11 @@ interface OcToolNodeWrapped<TArgs extends Record<string, unknown>, TResult> {
30
30
  execute: (args: TArgs, ctx: AgentContext & {
31
31
  callId: string;
32
32
  graphState: Record<string, unknown>;
33
- fleet?: FleetClient;
34
33
  }) => Promise<{
35
34
  result: TResult;
36
35
  action: ActionEnvelope;
37
- posted: PostActionResult | null;
38
36
  }>;
39
37
  }
40
38
  declare function ocToolNode<TArgs extends Record<string, unknown>, TResult>(input: OcToolNodeInput<TArgs, TResult>): OcToolNodeWrapped<TArgs, TResult>;
41
- interface FleetClient {
42
- baseUrl?: string;
43
- apiToken: string;
44
- projectId: string;
45
- fetch?: typeof fetch;
46
- }
47
- interface PostActionResult {
48
- id: string;
49
- project_id: string;
50
- delegation_id: string;
51
- }
52
- declare function postActionToFleet(action: ActionEnvelope, client: FleetClient): Promise<PostActionResult>;
53
39
 
54
- export { type AgentContext, type FleetClient, type LangGraphToolCall, type OcToolNodeInput, type OcToolNodeWrapped, type PostActionResult, type StampToolCallInput, canonicalizeToolCall, graphStateHash, ocToolNode, postActionToFleet, stampToolCall, toolCallActionId, toolCallHash };
40
+ export { type AgentContext, type LangGraphToolCall, type OcToolNodeInput, type OcToolNodeWrapped, type StampToolCallInput, canonicalizeToolCall, graphStateHash, ocToolNode, stampToolCall, toolCallActionId, toolCallHash };
package/dist/index.js CHANGED
@@ -35,13 +35,7 @@ function toolCallActionId(agentAddress, signedAt, delegationId, scopeExercised,
35
35
  }
36
36
  async function stampToolCall(input) {
37
37
  const scopeExercised = input.scopeExercised ?? `langgraph:tool(verb=${input.call.verb})`;
38
- const granted = (input.delegation.scopes ?? []).map(agentCore.parseScope);
39
- const exercisedParsed = agentCore.parseScope(scopeExercised);
40
- if (!granted.some((g) => agentCore.isSubScope(exercisedParsed, g))) {
41
- throw new Error(
42
- `stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
43
- );
44
- }
38
+ agentCore.assertScopeGranted(input.delegation, scopeExercised, "stampToolCall");
45
39
  const bytes = canonicalizeToolCall(input.call);
46
40
  return agentSigner.signAsAgent({
47
41
  agent: input.agent,
@@ -68,58 +62,14 @@ function ocToolNode(input) {
68
62
  call
69
63
  });
70
64
  const result = await input.execute(args);
71
- let posted = null;
72
- if (ctx.fleet) {
73
- try {
74
- posted = await postActionToFleet(action, ctx.fleet);
75
- } catch (err) {
76
- console.error("[oc-agent-langgraph] postActionToFleet failed:", err);
77
- }
78
- }
79
- return { result, action, posted };
65
+ return { result, action };
80
66
  }
81
67
  };
82
68
  }
83
- async function postActionToFleet(action, client) {
84
- const baseUrl = client.baseUrl ?? "https://fleet.ochk.io";
85
- const f = client.fetch ?? fetch;
86
- const body = {
87
- project_id: client.projectId,
88
- delegation_id: action.delegation_id,
89
- agent_address: action.signer.address,
90
- scope_exercised: action.scope_exercised,
91
- content_hash: action.content.hash,
92
- content_length: action.content.length,
93
- content_mime: action.content.mime,
94
- signed_at: action.signed_at,
95
- signature: action.sig.value,
96
- id: action.id
97
- };
98
- const r = await f(`${baseUrl}/api/actions`, {
99
- method: "POST",
100
- headers: {
101
- "Content-Type": "application/json",
102
- Authorization: `Bearer ${client.apiToken}`
103
- },
104
- body: JSON.stringify(body)
105
- });
106
- if (!r.ok) {
107
- let reason = `http_${r.status}`;
108
- try {
109
- const j2 = await r.json();
110
- if (j2.reason) reason = j2.reason;
111
- } catch {
112
- }
113
- throw new Error(`postActionToFleet failed: ${reason}`);
114
- }
115
- const j = await r.json();
116
- return j.action;
117
- }
118
69
 
119
70
  exports.canonicalizeToolCall = canonicalizeToolCall;
120
71
  exports.graphStateHash = graphStateHash;
121
72
  exports.ocToolNode = ocToolNode;
122
- exports.postActionToFleet = postActionToFleet;
123
73
  exports.stampToolCall = stampToolCall;
124
74
  exports.toolCallActionId = toolCallActionId;
125
75
  exports.toolCallHash = toolCallHash;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","parseScope","isSubScope","signAsAgent","j"],"mappings":"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","file":"index.js","sourcesContent":["// @orangecheck/agent-langgraph — wrap LangGraph tool-node executions in\n// OC Agent action envelopes.\n//\n// LangGraph's distinguishing feature is graph state: stateful agents\n// composed of typed nodes that share a persisted state object. The\n// compliance-relevant question becomes \"at what graph state did this\n// agent take this action?\" — and the answer must be cryptographically\n// bound to the action receipt.\n//\n// This adapter therefore canonicalizes (verb, args, callId, graph_state_hash)\n// — one extra field over the agent-mcp / agent-anthropic / agent-openai /\n// agent-vercel canonicalization. Two verifiers re-deriving the action from\n// a persisted graph snapshot compute the same envelope id only when the\n// snapshot's state-hash matches; tampered or out-of-order replay fails.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n canonicalize,\n computeActionId,\n hexEncode,\n isSubScope,\n parseScope,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Graph state hashing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Compute a stable hash of a LangGraph state object. Use canonical JSON\n * (RFC 8785) so reordered keys / repeated serializations produce the same\n * hash. Returns `sha256:<64-hex>`.\n *\n * Most LangGraph deployments persist the state object via a checkpointer\n * (in-memory, postgres, redis, etc.). The state-hash here is over the\n * checkpointer's serialized state at the moment the tool node fires.\n */\nexport function graphStateHash(state: Record<string, unknown>): string {\n const str = canonicalize(state as unknown as Parameters<typeof canonicalize>[0]);\n return 'sha256:' + hexEncode(sha256(new TextEncoder().encode(str + '\\n')));\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Tool-call canonicalization\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface LangGraphToolCall {\n /** Stable id assigned by your graph runtime for this tool invocation. */\n callId: string;\n /** Tool verb — typically the node name. */\n verb: string;\n /** JSON-serializable arguments to the tool node. */\n args: Record<string, unknown>;\n /** sha256:<64-hex> of the graph state at the moment of execution. */\n graphStateHash: string;\n}\n\n/**\n * Produce the canonical bytes for a LangGraph tool call.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"args\": <canonical JSON of args>,\n * \"call_id\": <callId>,\n * \"graph_state_hash\": <state hash>,\n * \"verb\": <verb>\n * }\n */\nexport function canonicalizeToolCall(c: LangGraphToolCall): Uint8Array {\n const canon = {\n args: c.args,\n call_id: c.callId,\n graph_state_hash: c.graphStateHash,\n verb: c.verb,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function toolCallHash(c: LangGraphToolCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeToolCall(c)));\n}\n\nexport function toolCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: LangGraphToolCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: toolCallHash(c),\n content_length: canonicalizeToolCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.langgraph-tool-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping primitive\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AgentContext {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n}\n\nexport interface StampToolCallInput extends AgentContext {\n call: LangGraphToolCall;\n /** Defaults to `langgraph:tool(verb=<verb>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ?? `langgraph:tool(verb=${input.call.verb})`;\n\n const granted = (input.delegation.scopes ?? []).map(parseScope);\n const exercisedParsed = parseScope(scopeExercised);\n if (!granted.some((g) => isSubScope(exercisedParsed, g))) {\n throw new Error(\n `stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeToolCall(input.call);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolCallHash(input.call), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.langgraph-tool-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ocToolNode — convenience wrapper\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OcToolNodeInput<TArgs extends Record<string, unknown>, TResult> {\n /** OC Agent verb under which this tool is exercised. */\n verb: string;\n /** Your real handler. Receives the same args your LangGraph tool node receives. */\n execute: (args: TArgs) => Promise<TResult>;\n}\n\nexport interface OcToolNodeWrapped<TArgs extends Record<string, unknown>, TResult> {\n verb: string;\n /**\n * The execute fn your LangGraph node calls. Receives args + a context\n * object with the agent ref, delegation, callId, current graph state,\n * and an optional `fleet` field that triggers fire-and-forget POST\n * to fleet.ochk.io/api/actions after the underlying execute returns.\n */\n execute: (\n args: TArgs,\n ctx: AgentContext & {\n callId: string;\n graphState: Record<string, unknown>;\n fleet?: FleetClient;\n }\n ) => Promise<{\n result: TResult;\n action: ActionEnvelope;\n posted: PostActionResult | null;\n }>;\n}\n\n/**\n * Wrap a LangGraph tool-node execute path so it produces an OC Agent\n * action envelope per call. The wrapper computes the graph_state_hash\n * inline from the supplied state object, so the verifier replaying the\n * graph against a persisted snapshot gets the same hash deterministically.\n *\n * Typical shape:\n *\n * const createInvoice = ocToolNode({\n * verb: 'invoice.create',\n * execute: async (args) => myInvoiceCreateImpl(args),\n * });\n *\n * const graph = new StateGraph(MyState)\n * .addNode('createInvoice', async (state) => {\n * const args = pickToolArgs(state);\n * const callId = state.lastToolCallId;\n * const { result, action } = await createInvoice.execute(args, {\n * agent, delegation, callId, graphState: state,\n * });\n * await audit.append(action);\n * return mergeResultIntoState(state, result);\n * });\n */\nexport function ocToolNode<TArgs extends Record<string, unknown>, TResult>(\n input: OcToolNodeInput<TArgs, TResult>\n): OcToolNodeWrapped<TArgs, TResult> {\n return {\n verb: input.verb,\n execute: async (args, ctx) => {\n const call: LangGraphToolCall = {\n callId: ctx.callId,\n verb: input.verb,\n args,\n graphStateHash: graphStateHash(ctx.graphState),\n };\n const action = await stampToolCall({\n agent: ctx.agent,\n delegation: ctx.delegation,\n call,\n });\n const result = await input.execute(args);\n let posted: PostActionResult | null = null;\n if (ctx.fleet) {\n try {\n posted = await postActionToFleet(action, ctx.fleet);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-langgraph] postActionToFleet failed:', err);\n }\n }\n return { result, action, posted };\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Fleet integration: POST stamped actions to fleet.ochk.io/api/actions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface FleetClient {\n /** Defaults to https://fleet.ochk.io. */\n baseUrl?: string;\n /** Bearer token from /settings § 03 (`ock_<hex>`). */\n apiToken: string;\n /** Project the action belongs to (proj_*). */\n projectId: string;\n /** Optional fetch override for runtimes that need it. */\n fetch?: typeof fetch;\n}\n\nexport interface PostActionResult {\n id: string;\n project_id: string;\n delegation_id: string;\n}\n\nexport async function postActionToFleet(\n action: ActionEnvelope,\n client: FleetClient\n): Promise<PostActionResult> {\n const baseUrl = client.baseUrl ?? 'https://fleet.ochk.io';\n const f = client.fetch ?? fetch;\n const body = {\n project_id: client.projectId,\n delegation_id: action.delegation_id,\n agent_address: action.signer.address,\n scope_exercised: action.scope_exercised,\n content_hash: action.content.hash,\n content_length: action.content.length,\n content_mime: action.content.mime,\n signed_at: action.signed_at,\n signature: action.sig.value,\n id: action.id,\n };\n const r = await f(`${baseUrl}/api/actions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${client.apiToken}`,\n },\n body: JSON.stringify(body),\n });\n if (!r.ok) {\n let reason = `http_${r.status}`;\n try {\n const j = (await r.json()) as { reason?: string };\n if (j.reason) reason = j.reason;\n } catch {\n // body wasn't json\n }\n throw new Error(`postActionToFleet failed: ${reason}`);\n }\n const j = (await r.json()) as { ok: true; action: PostActionResult };\n return j.action;\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","assertScopeGranted","signAsAgent"],"mappings":";;;;;;;AA0CO,SAAS,eAAe,KAAA,EAAwC;AACnE,EAAA,MAAM,GAAA,GAAMA,uBAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,SAAA,GAAYC,mBAAA,CAAUC,aAAA,CAAO,IAAI,WAAA,GAAc,MAAA,CAAO,GAAA,GAAM,IAAI,CAAC,CAAC,CAAA;AAC7E;AA4BO,SAAS,qBAAqB,CAAA,EAAkC;AACnE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,SAAS,CAAA,CAAE,MAAA;AAAA,IACX,kBAAkB,CAAA,CAAE,cAAA;AAAA,IACpB,MAAM,CAAA,CAAE;AAAA,GACZ;AACA,EAAA,MAAM,GAAA,GAAMF,uBAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,aAAa,CAAA,EAA8B;AACvD,EAAA,OAAO,YAAYC,mBAAA,CAAUC,aAAA,CAAO,oBAAA,CAAqB,CAAC,CAAC,CAAC,CAAA;AAChE;AAEO,SAAS,gBAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,CAAA,EACM;AACN,EAAA,OAAOC,yBAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,aAAa,CAAC,CAAA;AAAA,IAC5B,cAAA,EAAgB,oBAAA,CAAqB,CAAC,CAAA,CAAE,UAAA;AAAA,IACxC,YAAA,EAAc,mDAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAkBA,eAAsB,cAAc,KAAA,EAAoD;AACpF,EAAA,MAAM,iBACF,KAAA,CAAM,cAAA,IAAkB,CAAA,oBAAA,EAAuB,KAAA,CAAM,KAAK,IAAI,CAAA,CAAA,CAAA;AAElE,EAAAC,4BAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,eAAe,CAAA;AAEpE,EAAA,MAAM,KAAA,GAAQ,oBAAA,CAAqB,KAAA,CAAM,IAAI,CAAA;AAC7C,EAAA,OAAOC,uBAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,YAAA,CAAa,MAAM,IAAI,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IACpE,IAAA,EAAM,mDAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAwDO,SAAS,WACZ,KAAA,EACiC;AACjC,EAAA,OAAO;AAAA,IACH,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,OAAA,EAAS,OAAO,IAAA,EAAM,GAAA,KAAQ;AAC1B,MAAA,MAAM,IAAA,GAA0B;AAAA,QAC5B,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,IAAA;AAAA,QACA,cAAA,EAAgB,cAAA,CAAe,GAAA,CAAI,UAAU;AAAA,OACjD;AACA,MAAA,MAAM,MAAA,GAAS,MAAM,aAAA,CAAc;AAAA,QAC/B,OAAO,GAAA,CAAI,KAAA;AAAA,QACX,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB;AAAA,OACH,CAAA;AACD,MAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA;AACvC,MAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAAA,IAC5B;AAAA,GACJ;AACJ","file":"index.js","sourcesContent":["// @orangecheck/agent-langgraph — wrap LangGraph tool-node executions in\n// OC Agent action envelopes.\n//\n// LangGraph's distinguishing feature is graph state: stateful agents\n// composed of typed nodes that share a persisted state object. The\n// compliance-relevant question becomes \"at what graph state did this\n// agent take this action?\" — and the answer must be cryptographically\n// bound to the action receipt.\n//\n// This adapter therefore canonicalizes (verb, args, callId, graph_state_hash)\n// — one extra field over the agent-mcp / agent-anthropic / agent-openai /\n// agent-vercel canonicalization. Two verifiers re-deriving the action from\n// a persisted graph snapshot compute the same envelope id only when the\n// snapshot's state-hash matches; tampered or out-of-order replay fails.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n assertScopeGranted,\n canonicalize,\n computeActionId,\n hexEncode,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Graph state hashing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Compute a stable hash of a LangGraph state object. Use canonical JSON\n * (RFC 8785) so reordered keys / repeated serializations produce the same\n * hash. Returns `sha256:<64-hex>`.\n *\n * Most LangGraph deployments persist the state object via a checkpointer\n * (in-memory, postgres, redis, etc.). The state-hash here is over the\n * checkpointer's serialized state at the moment the tool node fires.\n */\nexport function graphStateHash(state: Record<string, unknown>): string {\n const str = canonicalize(state as unknown as Parameters<typeof canonicalize>[0]);\n return 'sha256:' + hexEncode(sha256(new TextEncoder().encode(str + '\\n')));\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Tool-call canonicalization\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface LangGraphToolCall {\n /** Stable id assigned by your graph runtime for this tool invocation. */\n callId: string;\n /** Tool verb — typically the node name. */\n verb: string;\n /** JSON-serializable arguments to the tool node. */\n args: Record<string, unknown>;\n /** sha256:<64-hex> of the graph state at the moment of execution. */\n graphStateHash: string;\n}\n\n/**\n * Produce the canonical bytes for a LangGraph tool call.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"args\": <canonical JSON of args>,\n * \"call_id\": <callId>,\n * \"graph_state_hash\": <state hash>,\n * \"verb\": <verb>\n * }\n */\nexport function canonicalizeToolCall(c: LangGraphToolCall): Uint8Array {\n const canon = {\n args: c.args,\n call_id: c.callId,\n graph_state_hash: c.graphStateHash,\n verb: c.verb,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function toolCallHash(c: LangGraphToolCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeToolCall(c)));\n}\n\nexport function toolCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: LangGraphToolCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: toolCallHash(c),\n content_length: canonicalizeToolCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.langgraph-tool-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping primitive\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AgentContext {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n}\n\nexport interface StampToolCallInput extends AgentContext {\n call: LangGraphToolCall;\n /** Defaults to `langgraph:tool(verb=<verb>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ?? `langgraph:tool(verb=${input.call.verb})`;\n\n assertScopeGranted(input.delegation, scopeExercised, 'stampToolCall');\n\n const bytes = canonicalizeToolCall(input.call);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolCallHash(input.call), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.langgraph-tool-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ocToolNode — convenience wrapper\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OcToolNodeInput<TArgs extends Record<string, unknown>, TResult> {\n /** OC Agent verb under which this tool is exercised. */\n verb: string;\n /** Your real handler. Receives the same args your LangGraph tool node receives. */\n execute: (args: TArgs) => Promise<TResult>;\n}\n\nexport interface OcToolNodeWrapped<TArgs extends Record<string, unknown>, TResult> {\n verb: string;\n /**\n * The execute fn your LangGraph node calls. Receives args + a context\n * object with the agent ref, delegation, callId, current graph state,\n * and the current graph state.\n */\n execute: (\n args: TArgs,\n ctx: AgentContext & {\n callId: string;\n graphState: Record<string, unknown>;\n }\n ) => Promise<{\n result: TResult;\n action: ActionEnvelope;\n }>;\n}\n\n/**\n * Wrap a LangGraph tool-node execute path so it produces an OC Agent\n * action envelope per call. The wrapper computes the graph_state_hash\n * inline from the supplied state object, so the verifier replaying the\n * graph against a persisted snapshot gets the same hash deterministically.\n *\n * Typical shape:\n *\n * const createInvoice = ocToolNode({\n * verb: 'invoice.create',\n * execute: async (args) => myInvoiceCreateImpl(args),\n * });\n *\n * const graph = new StateGraph(MyState)\n * .addNode('createInvoice', async (state) => {\n * const args = pickToolArgs(state);\n * const callId = state.lastToolCallId;\n * const { result, action } = await createInvoice.execute(args, {\n * agent, delegation, callId, graphState: state,\n * });\n * await audit.append(action);\n * return mergeResultIntoState(state, result);\n * });\n */\nexport function ocToolNode<TArgs extends Record<string, unknown>, TResult>(\n input: OcToolNodeInput<TArgs, TResult>\n): OcToolNodeWrapped<TArgs, TResult> {\n return {\n verb: input.verb,\n execute: async (args, ctx) => {\n const call: LangGraphToolCall = {\n callId: ctx.callId,\n verb: input.verb,\n args,\n graphStateHash: graphStateHash(ctx.graphState),\n };\n const action = await stampToolCall({\n agent: ctx.agent,\n delegation: ctx.delegation,\n call,\n });\n const result = await input.execute(args);\n return { result, action };\n },\n };\n}\n"]}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
- import { canonicalize, hexEncode, computeActionId, parseScope, isSubScope } from '@orangecheck/agent-core';
2
+ import { canonicalize, hexEncode, computeActionId, assertScopeGranted } from '@orangecheck/agent-core';
3
3
  import { signAsAgent } from '@orangecheck/agent-signer';
4
4
 
5
5
  // src/index.ts
@@ -33,13 +33,7 @@ function toolCallActionId(agentAddress, signedAt, delegationId, scopeExercised,
33
33
  }
34
34
  async function stampToolCall(input) {
35
35
  const scopeExercised = input.scopeExercised ?? `langgraph:tool(verb=${input.call.verb})`;
36
- const granted = (input.delegation.scopes ?? []).map(parseScope);
37
- const exercisedParsed = parseScope(scopeExercised);
38
- if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
39
- throw new Error(
40
- `stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
41
- );
42
- }
36
+ assertScopeGranted(input.delegation, scopeExercised, "stampToolCall");
43
37
  const bytes = canonicalizeToolCall(input.call);
44
38
  return signAsAgent({
45
39
  agent: input.agent,
@@ -66,54 +60,11 @@ function ocToolNode(input) {
66
60
  call
67
61
  });
68
62
  const result = await input.execute(args);
69
- let posted = null;
70
- if (ctx.fleet) {
71
- try {
72
- posted = await postActionToFleet(action, ctx.fleet);
73
- } catch (err) {
74
- console.error("[oc-agent-langgraph] postActionToFleet failed:", err);
75
- }
76
- }
77
- return { result, action, posted };
63
+ return { result, action };
78
64
  }
79
65
  };
80
66
  }
81
- async function postActionToFleet(action, client) {
82
- const baseUrl = client.baseUrl ?? "https://fleet.ochk.io";
83
- const f = client.fetch ?? fetch;
84
- const body = {
85
- project_id: client.projectId,
86
- delegation_id: action.delegation_id,
87
- agent_address: action.signer.address,
88
- scope_exercised: action.scope_exercised,
89
- content_hash: action.content.hash,
90
- content_length: action.content.length,
91
- content_mime: action.content.mime,
92
- signed_at: action.signed_at,
93
- signature: action.sig.value,
94
- id: action.id
95
- };
96
- const r = await f(`${baseUrl}/api/actions`, {
97
- method: "POST",
98
- headers: {
99
- "Content-Type": "application/json",
100
- Authorization: `Bearer ${client.apiToken}`
101
- },
102
- body: JSON.stringify(body)
103
- });
104
- if (!r.ok) {
105
- let reason = `http_${r.status}`;
106
- try {
107
- const j2 = await r.json();
108
- if (j2.reason) reason = j2.reason;
109
- } catch {
110
- }
111
- throw new Error(`postActionToFleet failed: ${reason}`);
112
- }
113
- const j = await r.json();
114
- return j.action;
115
- }
116
67
 
117
- export { canonicalizeToolCall, graphStateHash, ocToolNode, postActionToFleet, stampToolCall, toolCallActionId, toolCallHash };
68
+ export { canonicalizeToolCall, graphStateHash, ocToolNode, stampToolCall, toolCallActionId, toolCallHash };
118
69
  //# sourceMappingURL=index.mjs.map
119
70
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["j"],"mappings":"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","file":"index.mjs","sourcesContent":["// @orangecheck/agent-langgraph — wrap LangGraph tool-node executions in\n// OC Agent action envelopes.\n//\n// LangGraph's distinguishing feature is graph state: stateful agents\n// composed of typed nodes that share a persisted state object. The\n// compliance-relevant question becomes \"at what graph state did this\n// agent take this action?\" — and the answer must be cryptographically\n// bound to the action receipt.\n//\n// This adapter therefore canonicalizes (verb, args, callId, graph_state_hash)\n// — one extra field over the agent-mcp / agent-anthropic / agent-openai /\n// agent-vercel canonicalization. Two verifiers re-deriving the action from\n// a persisted graph snapshot compute the same envelope id only when the\n// snapshot's state-hash matches; tampered or out-of-order replay fails.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n canonicalize,\n computeActionId,\n hexEncode,\n isSubScope,\n parseScope,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Graph state hashing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Compute a stable hash of a LangGraph state object. Use canonical JSON\n * (RFC 8785) so reordered keys / repeated serializations produce the same\n * hash. Returns `sha256:<64-hex>`.\n *\n * Most LangGraph deployments persist the state object via a checkpointer\n * (in-memory, postgres, redis, etc.). The state-hash here is over the\n * checkpointer's serialized state at the moment the tool node fires.\n */\nexport function graphStateHash(state: Record<string, unknown>): string {\n const str = canonicalize(state as unknown as Parameters<typeof canonicalize>[0]);\n return 'sha256:' + hexEncode(sha256(new TextEncoder().encode(str + '\\n')));\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Tool-call canonicalization\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface LangGraphToolCall {\n /** Stable id assigned by your graph runtime for this tool invocation. */\n callId: string;\n /** Tool verb — typically the node name. */\n verb: string;\n /** JSON-serializable arguments to the tool node. */\n args: Record<string, unknown>;\n /** sha256:<64-hex> of the graph state at the moment of execution. */\n graphStateHash: string;\n}\n\n/**\n * Produce the canonical bytes for a LangGraph tool call.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"args\": <canonical JSON of args>,\n * \"call_id\": <callId>,\n * \"graph_state_hash\": <state hash>,\n * \"verb\": <verb>\n * }\n */\nexport function canonicalizeToolCall(c: LangGraphToolCall): Uint8Array {\n const canon = {\n args: c.args,\n call_id: c.callId,\n graph_state_hash: c.graphStateHash,\n verb: c.verb,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function toolCallHash(c: LangGraphToolCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeToolCall(c)));\n}\n\nexport function toolCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: LangGraphToolCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: toolCallHash(c),\n content_length: canonicalizeToolCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.langgraph-tool-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping primitive\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AgentContext {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n}\n\nexport interface StampToolCallInput extends AgentContext {\n call: LangGraphToolCall;\n /** Defaults to `langgraph:tool(verb=<verb>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ?? `langgraph:tool(verb=${input.call.verb})`;\n\n const granted = (input.delegation.scopes ?? []).map(parseScope);\n const exercisedParsed = parseScope(scopeExercised);\n if (!granted.some((g) => isSubScope(exercisedParsed, g))) {\n throw new Error(\n `stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeToolCall(input.call);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolCallHash(input.call), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.langgraph-tool-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ocToolNode — convenience wrapper\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OcToolNodeInput<TArgs extends Record<string, unknown>, TResult> {\n /** OC Agent verb under which this tool is exercised. */\n verb: string;\n /** Your real handler. Receives the same args your LangGraph tool node receives. */\n execute: (args: TArgs) => Promise<TResult>;\n}\n\nexport interface OcToolNodeWrapped<TArgs extends Record<string, unknown>, TResult> {\n verb: string;\n /**\n * The execute fn your LangGraph node calls. Receives args + a context\n * object with the agent ref, delegation, callId, current graph state,\n * and an optional `fleet` field that triggers fire-and-forget POST\n * to fleet.ochk.io/api/actions after the underlying execute returns.\n */\n execute: (\n args: TArgs,\n ctx: AgentContext & {\n callId: string;\n graphState: Record<string, unknown>;\n fleet?: FleetClient;\n }\n ) => Promise<{\n result: TResult;\n action: ActionEnvelope;\n posted: PostActionResult | null;\n }>;\n}\n\n/**\n * Wrap a LangGraph tool-node execute path so it produces an OC Agent\n * action envelope per call. The wrapper computes the graph_state_hash\n * inline from the supplied state object, so the verifier replaying the\n * graph against a persisted snapshot gets the same hash deterministically.\n *\n * Typical shape:\n *\n * const createInvoice = ocToolNode({\n * verb: 'invoice.create',\n * execute: async (args) => myInvoiceCreateImpl(args),\n * });\n *\n * const graph = new StateGraph(MyState)\n * .addNode('createInvoice', async (state) => {\n * const args = pickToolArgs(state);\n * const callId = state.lastToolCallId;\n * const { result, action } = await createInvoice.execute(args, {\n * agent, delegation, callId, graphState: state,\n * });\n * await audit.append(action);\n * return mergeResultIntoState(state, result);\n * });\n */\nexport function ocToolNode<TArgs extends Record<string, unknown>, TResult>(\n input: OcToolNodeInput<TArgs, TResult>\n): OcToolNodeWrapped<TArgs, TResult> {\n return {\n verb: input.verb,\n execute: async (args, ctx) => {\n const call: LangGraphToolCall = {\n callId: ctx.callId,\n verb: input.verb,\n args,\n graphStateHash: graphStateHash(ctx.graphState),\n };\n const action = await stampToolCall({\n agent: ctx.agent,\n delegation: ctx.delegation,\n call,\n });\n const result = await input.execute(args);\n let posted: PostActionResult | null = null;\n if (ctx.fleet) {\n try {\n posted = await postActionToFleet(action, ctx.fleet);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-langgraph] postActionToFleet failed:', err);\n }\n }\n return { result, action, posted };\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Fleet integration: POST stamped actions to fleet.ochk.io/api/actions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface FleetClient {\n /** Defaults to https://fleet.ochk.io. */\n baseUrl?: string;\n /** Bearer token from /settings § 03 (`ock_<hex>`). */\n apiToken: string;\n /** Project the action belongs to (proj_*). */\n projectId: string;\n /** Optional fetch override for runtimes that need it. */\n fetch?: typeof fetch;\n}\n\nexport interface PostActionResult {\n id: string;\n project_id: string;\n delegation_id: string;\n}\n\nexport async function postActionToFleet(\n action: ActionEnvelope,\n client: FleetClient\n): Promise<PostActionResult> {\n const baseUrl = client.baseUrl ?? 'https://fleet.ochk.io';\n const f = client.fetch ?? fetch;\n const body = {\n project_id: client.projectId,\n delegation_id: action.delegation_id,\n agent_address: action.signer.address,\n scope_exercised: action.scope_exercised,\n content_hash: action.content.hash,\n content_length: action.content.length,\n content_mime: action.content.mime,\n signed_at: action.signed_at,\n signature: action.sig.value,\n id: action.id,\n };\n const r = await f(`${baseUrl}/api/actions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${client.apiToken}`,\n },\n body: JSON.stringify(body),\n });\n if (!r.ok) {\n let reason = `http_${r.status}`;\n try {\n const j = (await r.json()) as { reason?: string };\n if (j.reason) reason = j.reason;\n } catch {\n // body wasn't json\n }\n throw new Error(`postActionToFleet failed: ${reason}`);\n }\n const j = (await r.json()) as { ok: true; action: PostActionResult };\n return j.action;\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;AA0CO,SAAS,eAAe,KAAA,EAAwC;AACnE,EAAA,MAAM,GAAA,GAAM,aAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,SAAA,GAAY,SAAA,CAAU,MAAA,CAAO,IAAI,WAAA,GAAc,MAAA,CAAO,GAAA,GAAM,IAAI,CAAC,CAAC,CAAA;AAC7E;AA4BO,SAAS,qBAAqB,CAAA,EAAkC;AACnE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,SAAS,CAAA,CAAE,MAAA;AAAA,IACX,kBAAkB,CAAA,CAAE,cAAA;AAAA,IACpB,MAAM,CAAA,CAAE;AAAA,GACZ;AACA,EAAA,MAAM,GAAA,GAAM,aAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,aAAa,CAAA,EAA8B;AACvD,EAAA,OAAO,YAAY,SAAA,CAAU,MAAA,CAAO,oBAAA,CAAqB,CAAC,CAAC,CAAC,CAAA;AAChE;AAEO,SAAS,gBAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,CAAA,EACM;AACN,EAAA,OAAO,eAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,aAAa,CAAC,CAAA;AAAA,IAC5B,cAAA,EAAgB,oBAAA,CAAqB,CAAC,CAAA,CAAE,UAAA;AAAA,IACxC,YAAA,EAAc,mDAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAkBA,eAAsB,cAAc,KAAA,EAAoD;AACpF,EAAA,MAAM,iBACF,KAAA,CAAM,cAAA,IAAkB,CAAA,oBAAA,EAAuB,KAAA,CAAM,KAAK,IAAI,CAAA,CAAA,CAAA;AAElE,EAAA,kBAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,eAAe,CAAA;AAEpE,EAAA,MAAM,KAAA,GAAQ,oBAAA,CAAqB,KAAA,CAAM,IAAI,CAAA;AAC7C,EAAA,OAAO,WAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,YAAA,CAAa,MAAM,IAAI,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IACpE,IAAA,EAAM,mDAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAwDO,SAAS,WACZ,KAAA,EACiC;AACjC,EAAA,OAAO;AAAA,IACH,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,OAAA,EAAS,OAAO,IAAA,EAAM,GAAA,KAAQ;AAC1B,MAAA,MAAM,IAAA,GAA0B;AAAA,QAC5B,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,IAAA;AAAA,QACA,cAAA,EAAgB,cAAA,CAAe,GAAA,CAAI,UAAU;AAAA,OACjD;AACA,MAAA,MAAM,MAAA,GAAS,MAAM,aAAA,CAAc;AAAA,QAC/B,OAAO,GAAA,CAAI,KAAA;AAAA,QACX,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB;AAAA,OACH,CAAA;AACD,MAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA;AACvC,MAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAAA,IAC5B;AAAA,GACJ;AACJ","file":"index.mjs","sourcesContent":["// @orangecheck/agent-langgraph — wrap LangGraph tool-node executions in\n// OC Agent action envelopes.\n//\n// LangGraph's distinguishing feature is graph state: stateful agents\n// composed of typed nodes that share a persisted state object. The\n// compliance-relevant question becomes \"at what graph state did this\n// agent take this action?\" — and the answer must be cryptographically\n// bound to the action receipt.\n//\n// This adapter therefore canonicalizes (verb, args, callId, graph_state_hash)\n// — one extra field over the agent-mcp / agent-anthropic / agent-openai /\n// agent-vercel canonicalization. Two verifiers re-deriving the action from\n// a persisted graph snapshot compute the same envelope id only when the\n// snapshot's state-hash matches; tampered or out-of-order replay fails.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n assertScopeGranted,\n canonicalize,\n computeActionId,\n hexEncode,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Graph state hashing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Compute a stable hash of a LangGraph state object. Use canonical JSON\n * (RFC 8785) so reordered keys / repeated serializations produce the same\n * hash. Returns `sha256:<64-hex>`.\n *\n * Most LangGraph deployments persist the state object via a checkpointer\n * (in-memory, postgres, redis, etc.). The state-hash here is over the\n * checkpointer's serialized state at the moment the tool node fires.\n */\nexport function graphStateHash(state: Record<string, unknown>): string {\n const str = canonicalize(state as unknown as Parameters<typeof canonicalize>[0]);\n return 'sha256:' + hexEncode(sha256(new TextEncoder().encode(str + '\\n')));\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Tool-call canonicalization\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface LangGraphToolCall {\n /** Stable id assigned by your graph runtime for this tool invocation. */\n callId: string;\n /** Tool verb — typically the node name. */\n verb: string;\n /** JSON-serializable arguments to the tool node. */\n args: Record<string, unknown>;\n /** sha256:<64-hex> of the graph state at the moment of execution. */\n graphStateHash: string;\n}\n\n/**\n * Produce the canonical bytes for a LangGraph tool call.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"args\": <canonical JSON of args>,\n * \"call_id\": <callId>,\n * \"graph_state_hash\": <state hash>,\n * \"verb\": <verb>\n * }\n */\nexport function canonicalizeToolCall(c: LangGraphToolCall): Uint8Array {\n const canon = {\n args: c.args,\n call_id: c.callId,\n graph_state_hash: c.graphStateHash,\n verb: c.verb,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function toolCallHash(c: LangGraphToolCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeToolCall(c)));\n}\n\nexport function toolCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: LangGraphToolCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: toolCallHash(c),\n content_length: canonicalizeToolCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.langgraph-tool-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping primitive\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AgentContext {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n}\n\nexport interface StampToolCallInput extends AgentContext {\n call: LangGraphToolCall;\n /** Defaults to `langgraph:tool(verb=<verb>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ?? `langgraph:tool(verb=${input.call.verb})`;\n\n assertScopeGranted(input.delegation, scopeExercised, 'stampToolCall');\n\n const bytes = canonicalizeToolCall(input.call);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolCallHash(input.call), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.langgraph-tool-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ocToolNode — convenience wrapper\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OcToolNodeInput<TArgs extends Record<string, unknown>, TResult> {\n /** OC Agent verb under which this tool is exercised. */\n verb: string;\n /** Your real handler. Receives the same args your LangGraph tool node receives. */\n execute: (args: TArgs) => Promise<TResult>;\n}\n\nexport interface OcToolNodeWrapped<TArgs extends Record<string, unknown>, TResult> {\n verb: string;\n /**\n * The execute fn your LangGraph node calls. Receives args + a context\n * object with the agent ref, delegation, callId, current graph state,\n * and the current graph state.\n */\n execute: (\n args: TArgs,\n ctx: AgentContext & {\n callId: string;\n graphState: Record<string, unknown>;\n }\n ) => Promise<{\n result: TResult;\n action: ActionEnvelope;\n }>;\n}\n\n/**\n * Wrap a LangGraph tool-node execute path so it produces an OC Agent\n * action envelope per call. The wrapper computes the graph_state_hash\n * inline from the supplied state object, so the verifier replaying the\n * graph against a persisted snapshot gets the same hash deterministically.\n *\n * Typical shape:\n *\n * const createInvoice = ocToolNode({\n * verb: 'invoice.create',\n * execute: async (args) => myInvoiceCreateImpl(args),\n * });\n *\n * const graph = new StateGraph(MyState)\n * .addNode('createInvoice', async (state) => {\n * const args = pickToolArgs(state);\n * const callId = state.lastToolCallId;\n * const { result, action } = await createInvoice.execute(args, {\n * agent, delegation, callId, graphState: state,\n * });\n * await audit.append(action);\n * return mergeResultIntoState(state, result);\n * });\n */\nexport function ocToolNode<TArgs extends Record<string, unknown>, TResult>(\n input: OcToolNodeInput<TArgs, TResult>\n): OcToolNodeWrapped<TArgs, TResult> {\n return {\n verb: input.verb,\n execute: async (args, ctx) => {\n const call: LangGraphToolCall = {\n callId: ctx.callId,\n verb: input.verb,\n args,\n graphStateHash: graphStateHash(ctx.graphState),\n };\n const action = await stampToolCall({\n agent: ctx.agent,\n delegation: ctx.delegation,\n call,\n });\n const result = await input.execute(args);\n return { result, action };\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/agent-langgraph",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "Wrap LangGraph tool nodes with OC Agent scope enforcement and emit a signed agent-action envelope per execution. Binds the action to the graph-state hash so verifiers replay the audit against the persisted graph snapshot.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -52,8 +52,8 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@noble/hashes": "^1.5.0",
55
- "@orangecheck/agent-core": "^0.3.0",
56
- "@orangecheck/agent-signer": "^0.1.0"
55
+ "@orangecheck/agent-core": "^1.2.0",
56
+ "@orangecheck/agent-signer": "^0.3.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^22.10.2",
package/src/index.ts CHANGED
@@ -15,11 +15,10 @@
15
15
 
16
16
  import { sha256 } from '@noble/hashes/sha256';
17
17
  import {
18
+ assertScopeGranted,
18
19
  canonicalize,
19
20
  computeActionId,
20
21
  hexEncode,
21
- isSubScope,
22
- parseScope,
23
22
  type DelegationEnvelope,
24
23
  } from '@orangecheck/agent-core';
25
24
  import {
@@ -125,13 +124,7 @@ export async function stampToolCall(input: StampToolCallInput): Promise<ActionEn
125
124
  const scopeExercised =
126
125
  input.scopeExercised ?? `langgraph:tool(verb=${input.call.verb})`;
127
126
 
128
- const granted = (input.delegation.scopes ?? []).map(parseScope);
129
- const exercisedParsed = parseScope(scopeExercised);
130
- if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
131
- throw new Error(
132
- `stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
133
- );
134
- }
127
+ assertScopeGranted(input.delegation, scopeExercised, 'stampToolCall');
135
128
 
136
129
  const bytes = canonicalizeToolCall(input.call);
137
130
  return signAsAgent({
@@ -160,20 +153,17 @@ export interface OcToolNodeWrapped<TArgs extends Record<string, unknown>, TResul
160
153
  /**
161
154
  * The execute fn your LangGraph node calls. Receives args + a context
162
155
  * object with the agent ref, delegation, callId, current graph state,
163
- * and an optional `fleet` field that triggers fire-and-forget POST
164
- * to fleet.ochk.io/api/actions after the underlying execute returns.
156
+ * and the current graph state.
165
157
  */
166
158
  execute: (
167
159
  args: TArgs,
168
160
  ctx: AgentContext & {
169
161
  callId: string;
170
162
  graphState: Record<string, unknown>;
171
- fleet?: FleetClient;
172
163
  }
173
164
  ) => Promise<{
174
165
  result: TResult;
175
166
  action: ActionEnvelope;
176
- posted: PostActionResult | null;
177
167
  }>;
178
168
  }
179
169
 
@@ -219,77 +209,7 @@ export function ocToolNode<TArgs extends Record<string, unknown>, TResult>(
219
209
  call,
220
210
  });
221
211
  const result = await input.execute(args);
222
- let posted: PostActionResult | null = null;
223
- if (ctx.fleet) {
224
- try {
225
- posted = await postActionToFleet(action, ctx.fleet);
226
- } catch (err) {
227
- // eslint-disable-next-line no-console
228
- console.error('[oc-agent-langgraph] postActionToFleet failed:', err);
229
- }
230
- }
231
- return { result, action, posted };
212
+ return { result, action };
232
213
  },
233
214
  };
234
215
  }
235
-
236
- // ─────────────────────────────────────────────────────────────────────────────
237
- // Fleet integration: POST stamped actions to fleet.ochk.io/api/actions
238
- // ─────────────────────────────────────────────────────────────────────────────
239
-
240
- export interface FleetClient {
241
- /** Defaults to https://fleet.ochk.io. */
242
- baseUrl?: string;
243
- /** Bearer token from /settings § 03 (`ock_<hex>`). */
244
- apiToken: string;
245
- /** Project the action belongs to (proj_*). */
246
- projectId: string;
247
- /** Optional fetch override for runtimes that need it. */
248
- fetch?: typeof fetch;
249
- }
250
-
251
- export interface PostActionResult {
252
- id: string;
253
- project_id: string;
254
- delegation_id: string;
255
- }
256
-
257
- export async function postActionToFleet(
258
- action: ActionEnvelope,
259
- client: FleetClient
260
- ): Promise<PostActionResult> {
261
- const baseUrl = client.baseUrl ?? 'https://fleet.ochk.io';
262
- const f = client.fetch ?? fetch;
263
- const body = {
264
- project_id: client.projectId,
265
- delegation_id: action.delegation_id,
266
- agent_address: action.signer.address,
267
- scope_exercised: action.scope_exercised,
268
- content_hash: action.content.hash,
269
- content_length: action.content.length,
270
- content_mime: action.content.mime,
271
- signed_at: action.signed_at,
272
- signature: action.sig.value,
273
- id: action.id,
274
- };
275
- const r = await f(`${baseUrl}/api/actions`, {
276
- method: 'POST',
277
- headers: {
278
- 'Content-Type': 'application/json',
279
- Authorization: `Bearer ${client.apiToken}`,
280
- },
281
- body: JSON.stringify(body),
282
- });
283
- if (!r.ok) {
284
- let reason = `http_${r.status}`;
285
- try {
286
- const j = (await r.json()) as { reason?: string };
287
- if (j.reason) reason = j.reason;
288
- } catch {
289
- // body wasn't json
290
- }
291
- throw new Error(`postActionToFleet failed: ${reason}`);
292
- }
293
- const j = (await r.json()) as { ok: true; action: PostActionResult };
294
- return j.action;
295
- }
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ScopeNotGrantedError } from '@orangecheck/agent-core';
4
+
5
+ import { stampToolCall } from './index.js';
6
+
7
+ const AGENT = 'bc1qagent0000000000000000000000000000000000';
8
+ const sign = () => async (msg: string) => Buffer.from(msg, 'utf8').toString('base64');
9
+
10
+ const SEALED = {
11
+ v: 1,
12
+ alg: 'x25519-xchacha20poly1305',
13
+ recipients: [{ device_id: 'dev1', ct: 'AA' }],
14
+ ct: 'BB',
15
+ nonce: 'CC',
16
+ } as never;
17
+
18
+ /**
19
+ * This adapter used to open-code the pre-flight scope check. It now routes to
20
+ * agent-core's `assertScopeGranted`, and these assert the routing rather than
21
+ * the check — agent-core owns testing the check itself.
22
+ *
23
+ * The case that matters is the encrypted one: a v1.2 private delegation has no
24
+ * readable `scopes`, and the old `?? []` reported that as "not a sub-scope of
25
+ * any granted scope" — pointing the integrator at a scope string that was
26
+ * probably fine, when the actual need was to decrypt the grant.
27
+ */
28
+ describe('stampToolCall scope pre-flight', () => {
29
+ const base = {
30
+ agent: { address: AGENT, sign: sign() },
31
+ toolCall: { name: 'search', args: { q: 'x' }, id: 't1' },
32
+ } as never;
33
+
34
+ it('names encrypted scopes rather than blaming the scope string', async () => {
35
+ const err = await stampToolCall({
36
+ ...(base as object),
37
+ delegation: { scopes_encrypted: SEALED },
38
+ scopeExercised: 'langgraph:tool(name=search)',
39
+ } as never).catch((e: unknown) => e);
40
+
41
+ expect(err).toBeInstanceOf(ScopeNotGrantedError);
42
+ expect((err as ScopeNotGrantedError).reason).toBe('scopes_encrypted');
43
+ expect((err as Error).message).toMatch(/decryptPrivateScopes/);
44
+ });
45
+
46
+ it('treats an absent grant as granting nothing', async () => {
47
+ const err = await stampToolCall({
48
+ ...(base as object),
49
+ delegation: {},
50
+ scopeExercised: 'langgraph:tool(name=search)',
51
+ } as never).catch((e: unknown) => e);
52
+
53
+ expect((err as ScopeNotGrantedError).reason).toBe('no_scopes');
54
+ });
55
+ });