@orangecheck/agent-anthropic 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OrangeCheck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @orangecheck/agent-anthropic
2
+
3
+ Wrap [Anthropic Tool Use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) (Claude `tool_use` blocks) with [OC Agent](https://github.com/orangecheck/oc-agent-protocol) scope enforcement and emit a signed `agent-action` envelope per tool execution.
4
+
5
+ ## Why
6
+
7
+ Claude's tool-use API exposes a list of tools and produces structured `tool_use` content blocks. The MCP wire format has no native notion of "who authorized this invocation" beyond the transport layer's authentication. For anything consequential — sending email, moving sats, committing code — the handler running underneath should be able to verify:
8
+
9
+ 1. **Which principal stands behind this agent.** A Bitcoin address, not an opaque API key.
10
+ 2. **What scope authorizes this specific tool call.** A declarative string like `anthropic:tool(name=invoice.create)`.
11
+ 3. **That the agent hasn't exceeded its grant.** Automatic via OC Agent's sub-scope algorithm.
12
+ 4. **That the invocation happened before any revocation.** Optional OC Stamp anchoring.
13
+
14
+ `@orangecheck/agent-anthropic` canonicalizes the `(id, name, input)` tuple, hashes it, and stamps the hash as an `agent-action`. Your handler — or any downstream auditor — sees a verifiable authority artifact alongside the call itself.
15
+
16
+ ## Status
17
+
18
+ **v0.0.1 · in design.** API shape is stable; the package is published to give integrators something to build against. Production wiring (per-event streaming-API integration with `@anthropic-ai/sdk` v0.45+) lands when the first design partner names the integration. Until then this module is the canonical, transport-agnostic core; you call it from your existing Anthropic-SDK glue.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm i @orangecheck/agent-anthropic
24
+ # peer deps:
25
+ npm i @orangecheck/agent-core @orangecheck/agent-signer
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ ```ts
31
+ import { invokeWithStamp } from '@orangecheck/agent-anthropic';
32
+
33
+ // You already have a SignerRef and a DelegationEnvelope from
34
+ // @orangecheck/agent-signer. The delegation grants something like
35
+ // anthropic:tool(name=invoice.create)
36
+
37
+ const { result, action } = await invokeWithStamp({
38
+ agent,
39
+ delegation,
40
+ toolUse: {
41
+ id: 'toolu_01abc...',
42
+ name: 'invoice.create',
43
+ input: { customer: 'acme', amount: 14.20 },
44
+ },
45
+ call: async (t) => myInvoiceCreateImpl(t.input),
46
+ });
47
+
48
+ // `result` goes back to Claude as the tool_result content block.
49
+ // `action` is the signed envelope; ship it alongside the result, publish
50
+ // to Nostr kind 30084, anchor through the OC Stamp pipeline, etc.
51
+ ```
52
+
53
+ ## What gets stamped
54
+
55
+ The stamp's `content.hash` is a SHA-256 of the RFC-8785-canonicalized JSON:
56
+
57
+ ```json
58
+ {
59
+ "id": "<tool_use_id>",
60
+ "input": <canonical-json of toolUse.input>,
61
+ "name": "<tool name>"
62
+ }
63
+ ```
64
+
65
+ This shape is minimal by design — a verifier reconstructs it from the three fields without knowing anything Anthropic-SDK-specific.
66
+
67
+ The action's `scope_exercised` defaults to `anthropic:tool(name=<tool>)`, which is the tightest scope that exactly identifies the call. Callers who want a narrower scope can pass `scopeExercised` explicitly; `stampToolUse` pre-flights sub-scope containment against the delegation.
68
+
69
+ ## API
70
+
71
+ ### `canonicalizeToolUse(toolUse) -> Uint8Array`
72
+
73
+ RFC 8785 JSON canonicalization of `{id, name, input}`. Byte-identical across implementations.
74
+
75
+ ### `toolUseHash(toolUse) -> "sha256:<64-hex>"`
76
+
77
+ SHA-256 of the canonical bytes, prefixed for the envelope's `content.hash`.
78
+
79
+ ### `stampToolUse(input) -> Promise<ActionEnvelope>`
80
+
81
+ Produces a signed `agent-action` without executing the tool. Useful for pre-authorization flows or batch stamping.
82
+
83
+ ### `invokeWithStamp(input) -> Promise<{ result, action }>`
84
+
85
+ Stamp first, then execute the tool. Returns both.
86
+
87
+ ## License
88
+
89
+ MIT.
90
+
91
+ ## Related
92
+
93
+ - [oc-agent-protocol](https://github.com/orangecheck/oc-agent-protocol) — normative wire format.
94
+ - [@orangecheck/agent-core](https://www.npmjs.com/package/@orangecheck/agent-core) — verifier library.
95
+ - [@orangecheck/agent-mcp](https://www.npmjs.com/package/@orangecheck/agent-mcp) — same pattern, for MCP.
96
+ - [@orangecheck/agent-openai](https://www.npmjs.com/package/@orangecheck/agent-openai) — same pattern, for OpenAI Responses + Chat Completions.
97
+ - [console.ochk.io/integrations/anthropic](https://console.ochk.io/integrations/anthropic) — the managed-tier story this slots into.
@@ -0,0 +1,48 @@
1
+ import { DelegationEnvelope } from '@orangecheck/agent-core';
2
+ import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
3
+
4
+ interface AnthropicToolUse {
5
+ id: string;
6
+ name: string;
7
+ input: Record<string, unknown>;
8
+ }
9
+ declare function canonicalizeToolUse(t: AnthropicToolUse): Uint8Array;
10
+ declare function toolUseHash(t: AnthropicToolUse): string;
11
+ declare function toolUseId(agentAddress: string, signedAt: string, delegationId: string, scopeExercised: string, t: AnthropicToolUse): string;
12
+ interface StampToolUseInput {
13
+ agent: SignerRef;
14
+ delegation: DelegationEnvelope;
15
+ toolUse: AnthropicToolUse;
16
+ scopeExercised?: string;
17
+ signedAt?: Date;
18
+ }
19
+ declare function stampToolUse(input: StampToolUseInput): Promise<ActionEnvelope>;
20
+ interface InvokeWithStampInput<T> extends StampToolUseInput {
21
+ call: (toolUse: AnthropicToolUse) => Promise<T>;
22
+ }
23
+ interface InvokeWithStampResult<T> {
24
+ result: T;
25
+ action: ActionEnvelope;
26
+ }
27
+ declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
28
+ interface ConsoleClient {
29
+ baseUrl?: string;
30
+ apiToken: string;
31
+ projectId: string;
32
+ fetch?: typeof fetch;
33
+ }
34
+ interface PostActionResult {
35
+ id: string;
36
+ project_id: string;
37
+ delegation_id: string;
38
+ }
39
+ declare function postActionToConsole(action: ActionEnvelope, client: ConsoleClient): Promise<PostActionResult>;
40
+ interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
41
+ console?: ConsoleClient;
42
+ }
43
+ interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
44
+ posted: PostActionResult | null;
45
+ }
46
+ declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
47
+
48
+ export { type AnthropicToolUse, type ConsoleClient, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type PostActionResult, type StampToolUseInput, canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, postActionToConsole, stampToolUse, toolUseHash, toolUseId };
@@ -0,0 +1,48 @@
1
+ import { DelegationEnvelope } from '@orangecheck/agent-core';
2
+ import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
3
+
4
+ interface AnthropicToolUse {
5
+ id: string;
6
+ name: string;
7
+ input: Record<string, unknown>;
8
+ }
9
+ declare function canonicalizeToolUse(t: AnthropicToolUse): Uint8Array;
10
+ declare function toolUseHash(t: AnthropicToolUse): string;
11
+ declare function toolUseId(agentAddress: string, signedAt: string, delegationId: string, scopeExercised: string, t: AnthropicToolUse): string;
12
+ interface StampToolUseInput {
13
+ agent: SignerRef;
14
+ delegation: DelegationEnvelope;
15
+ toolUse: AnthropicToolUse;
16
+ scopeExercised?: string;
17
+ signedAt?: Date;
18
+ }
19
+ declare function stampToolUse(input: StampToolUseInput): Promise<ActionEnvelope>;
20
+ interface InvokeWithStampInput<T> extends StampToolUseInput {
21
+ call: (toolUse: AnthropicToolUse) => Promise<T>;
22
+ }
23
+ interface InvokeWithStampResult<T> {
24
+ result: T;
25
+ action: ActionEnvelope;
26
+ }
27
+ declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
28
+ interface ConsoleClient {
29
+ baseUrl?: string;
30
+ apiToken: string;
31
+ projectId: string;
32
+ fetch?: typeof fetch;
33
+ }
34
+ interface PostActionResult {
35
+ id: string;
36
+ project_id: string;
37
+ delegation_id: string;
38
+ }
39
+ declare function postActionToConsole(action: ActionEnvelope, client: ConsoleClient): Promise<PostActionResult>;
40
+ interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
41
+ console?: ConsoleClient;
42
+ }
43
+ interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
44
+ posted: PostActionResult | null;
45
+ }
46
+ declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
47
+
48
+ export { type AnthropicToolUse, type ConsoleClient, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type PostActionResult, type StampToolUseInput, canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, postActionToConsole, stampToolUse, toolUseHash, toolUseId };
package/dist/index.js ADDED
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ var sha256 = require('@noble/hashes/sha256');
4
+ var agentCore = require('@orangecheck/agent-core');
5
+ var agentSigner = require('@orangecheck/agent-signer');
6
+
7
+ // src/index.ts
8
+ function canonicalizeToolUse(t) {
9
+ const canon = {
10
+ id: t.id,
11
+ input: t.input,
12
+ name: t.name
13
+ };
14
+ const str = agentCore.canonicalize(canon);
15
+ return new TextEncoder().encode(str + "\n");
16
+ }
17
+ function toolUseHash(t) {
18
+ return "sha256:" + agentCore.hexEncode(sha256.sha256(canonicalizeToolUse(t)));
19
+ }
20
+ function toolUseId(agentAddress, signedAt, delegationId, scopeExercised, t) {
21
+ return agentCore.computeActionId({
22
+ address: agentAddress,
23
+ content_hash: toolUseHash(t),
24
+ content_length: canonicalizeToolUse(t).byteLength,
25
+ content_mime: "application/vnd.oc-agent.anthropic-tool-use+json",
26
+ signed_at: signedAt,
27
+ delegation_id: delegationId,
28
+ scope_exercised: scopeExercised
29
+ });
30
+ }
31
+ async function stampToolUse(input) {
32
+ const scopeExercised = input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;
33
+ const granted = (input.delegation.scopes ?? []).map(agentCore.parseScope);
34
+ const exercisedParsed = agentCore.parseScope(scopeExercised);
35
+ if (!granted.some((g) => agentCore.isSubScope(exercisedParsed, g))) {
36
+ throw new Error(
37
+ `stampToolUse: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
38
+ );
39
+ }
40
+ const bytes = canonicalizeToolUse(input.toolUse);
41
+ return agentSigner.signAsAgent({
42
+ agent: input.agent,
43
+ delegation: input.delegation,
44
+ content: { hash: toolUseHash(input.toolUse), length: bytes.byteLength },
45
+ mime: "application/vnd.oc-agent.anthropic-tool-use+json",
46
+ scopeExercised,
47
+ signedAt: input.signedAt
48
+ });
49
+ }
50
+ async function invokeWithStamp(input) {
51
+ const action = await stampToolUse(input);
52
+ const result = await input.call(input.toolUse);
53
+ return { result, action };
54
+ }
55
+ async function postActionToConsole(action, client) {
56
+ const baseUrl = client.baseUrl ?? "https://console.ochk.io";
57
+ const f = client.fetch ?? fetch;
58
+ const body = {
59
+ project_id: client.projectId,
60
+ delegation_id: action.delegation_id,
61
+ agent_address: action.signer.address,
62
+ scope_exercised: action.scope_exercised,
63
+ content_hash: action.content.hash,
64
+ content_length: action.content.length,
65
+ content_mime: action.content.mime,
66
+ signed_at: action.signed_at,
67
+ signature: action.sig.value,
68
+ id: action.id
69
+ };
70
+ const r = await f(`${baseUrl}/api/actions`, {
71
+ method: "POST",
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ Authorization: `Bearer ${client.apiToken}`
75
+ },
76
+ body: JSON.stringify(body)
77
+ });
78
+ if (!r.ok) {
79
+ let reason = `http_${r.status}`;
80
+ try {
81
+ const j2 = await r.json();
82
+ if (j2.reason) reason = j2.reason;
83
+ } catch {
84
+ }
85
+ throw new Error(`postActionToConsole failed: ${reason}`);
86
+ }
87
+ const j = await r.json();
88
+ return j.action;
89
+ }
90
+ async function invokeWithStampAndPost(input) {
91
+ const { result, action } = await invokeWithStamp(input);
92
+ let posted = null;
93
+ if (input.console) {
94
+ try {
95
+ posted = await postActionToConsole(action, input.console);
96
+ } catch (err) {
97
+ console.error("[oc-agent-anthropic] postActionToConsole failed:", err);
98
+ }
99
+ }
100
+ return { result, action, posted };
101
+ }
102
+
103
+ exports.canonicalizeToolUse = canonicalizeToolUse;
104
+ exports.invokeWithStamp = invokeWithStamp;
105
+ exports.invokeWithStampAndPost = invokeWithStampAndPost;
106
+ exports.postActionToConsole = postActionToConsole;
107
+ exports.stampToolUse = stampToolUse;
108
+ exports.toolUseHash = toolUseHash;
109
+ exports.toolUseId = toolUseId;
110
+ //# sourceMappingURL=index.js.map
111
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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-anthropic — wrap Anthropic Tool Use invocations in\n// OC Agent action envelopes.\n//\n// Anthropic's Messages API surfaces tool calls as `tool_use` content\n// blocks: { type: 'tool_use', id, name, input }. This module canonicalizes\n// each tool_use, hashes it, and produces an agent-action envelope binding\n// the BIP-322-authenticated agent to the (tool_name, input) tuple — at a\n// scope of `anthropic:tool(name=<tool>)`.\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// Canonicalization of an Anthropic tool_use\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AnthropicToolUse {\n /** The `id` Anthropic assigns to this tool_use block (passed back as tool_use_id on result). */\n id: string;\n /** The tool name registered in your `tools` array on messages.create. */\n name: string;\n /** The model's structured input for this tool — JSON-serializable. */\n input: Record<string, unknown>;\n}\n\n/**\n * Produce the canonical bytes for an Anthropic tool_use. The bytes are what\n * the agent-action envelope's `content.hash` is a SHA-256 of.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"id\": <tool_use_id>,\n * \"input\": <canonical JSON of input>,\n * \"name\": <tool name>\n * }\n */\nexport function canonicalizeToolUse(t: AnthropicToolUse): Uint8Array {\n const canon = {\n id: t.id,\n input: t.input,\n name: t.name,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function toolUseHash(t: AnthropicToolUse): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeToolUse(t)));\n}\n\nexport function toolUseId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n t: AnthropicToolUse\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: toolUseHash(t),\n content_length: canonicalizeToolUse(t).byteLength,\n content_mime: 'application/vnd.oc-agent.anthropic-tool-use+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamped tool_use\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampToolUseInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n toolUse: AnthropicToolUse;\n /** Defaults to `anthropic:tool(name=<tool>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\n/**\n * Stamp an Anthropic tool_use without executing it. Useful for pre-\n * authorization flows or batch stamping.\n */\nexport async function stampToolUse(input: StampToolUseInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;\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 `stampToolUse: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeToolUse(input.toolUse);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolUseHash(input.toolUse), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.anthropic-tool-use+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// invokeWithStamp — call the tool AND produce the stamp around it\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface InvokeWithStampInput<T> extends StampToolUseInput {\n /** Your handler that actually runs the tool. Returns whatever the tool returns. */\n call: (toolUse: AnthropicToolUse) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n}\n\n/**\n * Stamp the tool_use first, then execute the handler. The stamped envelope\n * is returned alongside the result so the caller can ship them together —\n * e.g. as a side-channel to an audit log, or as the `_oc_agent` metadata on\n * the tool_result content block back to Claude.\n */\nexport async function invokeWithStamp<T>(\n input: InvokeWithStampInput<T>\n): Promise<InvokeWithStampResult<T>> {\n const action = await stampToolUse(input);\n const result = await input.call(input.toolUse);\n return { result, action };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Console integration: POST stamped actions to console.ochk.io/api/actions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ConsoleClient {\n /** Defaults to https://console.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 (Node 18 LTS w/o global fetch, etc). */\n fetch?: typeof fetch;\n}\n\nexport interface PostActionResult {\n id: string;\n project_id: string;\n delegation_id: string;\n}\n\n/**\n * POST a stamped action envelope to console.ochk.io's /api/actions\n * registry. The console:\n * - re-derives the action id from canonical inputs and rejects on\n * mismatch (tamper defense)\n * - validates that the agent address matches the parent delegation's\n * agent (forgery defense)\n * - persists, fans out to Nostr (kind 30084), submits to OC Stamp,\n * and triggers any subscribed webhooks (action.registered)\n *\n * Returns the registered action's id + project_id + delegation_id.\n * Throws on non-2xx with the server's reason string.\n */\nexport async function postActionToConsole(\n action: ActionEnvelope,\n client: ConsoleClient\n): Promise<PostActionResult> {\n const baseUrl = client.baseUrl ?? 'https://console.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; keep the http_<n> reason\n }\n throw new Error(`postActionToConsole failed: ${reason}`);\n }\n const j = (await r.json()) as { ok: true; action: PostActionResult };\n return j.action;\n}\n\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp (no posting). */\n console?: ConsoleClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when console is configured AND the POST succeeded. null otherwise. */\n posted: PostActionResult | null;\n}\n\n/**\n * Convenience: stamp the tool_use, execute the handler, AND post the\n * stamped envelope to console. Three roundtrips → one call. If `console`\n * is omitted, this is identical to invokeWithStamp + posted=null.\n *\n * The post happens AFTER the call so a failing post doesn't prevent the\n * tool from running. If you need pre-authorization (the post must\n * succeed before the call runs), use stampToolUse + postActionToConsole\n * + your call() in that order.\n */\nexport async function invokeWithStampAndPost<T>(\n input: InvokeWithStampAndPostInput<T>\n): Promise<InvokeWithStampAndPostResult<T>> {\n const { result, action } = await invokeWithStamp(input);\n let posted: PostActionResult | null = null;\n if (input.console) {\n try {\n posted = await postActionToConsole(action, input.console);\n } catch (err) {\n // Best-effort post — surfacing the error here would force\n // every caller to handle \"tool ran fine but post failed\",\n // which is rarely what they want. Log and continue.\n // eslint-disable-next-line no-console\n console.error('[oc-agent-anthropic] postActionToConsole failed:', err);\n }\n }\n return { result, action, posted };\n}\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,103 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ import { canonicalize, hexEncode, computeActionId, parseScope, isSubScope } from '@orangecheck/agent-core';
3
+ import { signAsAgent } from '@orangecheck/agent-signer';
4
+
5
+ // src/index.ts
6
+ function canonicalizeToolUse(t) {
7
+ const canon = {
8
+ id: t.id,
9
+ input: t.input,
10
+ name: t.name
11
+ };
12
+ const str = canonicalize(canon);
13
+ return new TextEncoder().encode(str + "\n");
14
+ }
15
+ function toolUseHash(t) {
16
+ return "sha256:" + hexEncode(sha256(canonicalizeToolUse(t)));
17
+ }
18
+ function toolUseId(agentAddress, signedAt, delegationId, scopeExercised, t) {
19
+ return computeActionId({
20
+ address: agentAddress,
21
+ content_hash: toolUseHash(t),
22
+ content_length: canonicalizeToolUse(t).byteLength,
23
+ content_mime: "application/vnd.oc-agent.anthropic-tool-use+json",
24
+ signed_at: signedAt,
25
+ delegation_id: delegationId,
26
+ scope_exercised: scopeExercised
27
+ });
28
+ }
29
+ async function stampToolUse(input) {
30
+ const scopeExercised = input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;
31
+ const granted = (input.delegation.scopes ?? []).map(parseScope);
32
+ const exercisedParsed = parseScope(scopeExercised);
33
+ if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
34
+ throw new Error(
35
+ `stampToolUse: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
36
+ );
37
+ }
38
+ const bytes = canonicalizeToolUse(input.toolUse);
39
+ return signAsAgent({
40
+ agent: input.agent,
41
+ delegation: input.delegation,
42
+ content: { hash: toolUseHash(input.toolUse), length: bytes.byteLength },
43
+ mime: "application/vnd.oc-agent.anthropic-tool-use+json",
44
+ scopeExercised,
45
+ signedAt: input.signedAt
46
+ });
47
+ }
48
+ async function invokeWithStamp(input) {
49
+ const action = await stampToolUse(input);
50
+ const result = await input.call(input.toolUse);
51
+ return { result, action };
52
+ }
53
+ async function postActionToConsole(action, client) {
54
+ const baseUrl = client.baseUrl ?? "https://console.ochk.io";
55
+ const f = client.fetch ?? fetch;
56
+ const body = {
57
+ project_id: client.projectId,
58
+ delegation_id: action.delegation_id,
59
+ agent_address: action.signer.address,
60
+ scope_exercised: action.scope_exercised,
61
+ content_hash: action.content.hash,
62
+ content_length: action.content.length,
63
+ content_mime: action.content.mime,
64
+ signed_at: action.signed_at,
65
+ signature: action.sig.value,
66
+ id: action.id
67
+ };
68
+ const r = await f(`${baseUrl}/api/actions`, {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ Authorization: `Bearer ${client.apiToken}`
73
+ },
74
+ body: JSON.stringify(body)
75
+ });
76
+ if (!r.ok) {
77
+ let reason = `http_${r.status}`;
78
+ try {
79
+ const j2 = await r.json();
80
+ if (j2.reason) reason = j2.reason;
81
+ } catch {
82
+ }
83
+ throw new Error(`postActionToConsole failed: ${reason}`);
84
+ }
85
+ const j = await r.json();
86
+ return j.action;
87
+ }
88
+ async function invokeWithStampAndPost(input) {
89
+ const { result, action } = await invokeWithStamp(input);
90
+ let posted = null;
91
+ if (input.console) {
92
+ try {
93
+ posted = await postActionToConsole(action, input.console);
94
+ } catch (err) {
95
+ console.error("[oc-agent-anthropic] postActionToConsole failed:", err);
96
+ }
97
+ }
98
+ return { result, action, posted };
99
+ }
100
+
101
+ export { canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, postActionToConsole, stampToolUse, toolUseHash, toolUseId };
102
+ //# sourceMappingURL=index.mjs.map
103
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["j"],"mappings":"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","file":"index.mjs","sourcesContent":["// @orangecheck/agent-anthropic — wrap Anthropic Tool Use invocations in\n// OC Agent action envelopes.\n//\n// Anthropic's Messages API surfaces tool calls as `tool_use` content\n// blocks: { type: 'tool_use', id, name, input }. This module canonicalizes\n// each tool_use, hashes it, and produces an agent-action envelope binding\n// the BIP-322-authenticated agent to the (tool_name, input) tuple — at a\n// scope of `anthropic:tool(name=<tool>)`.\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// Canonicalization of an Anthropic tool_use\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AnthropicToolUse {\n /** The `id` Anthropic assigns to this tool_use block (passed back as tool_use_id on result). */\n id: string;\n /** The tool name registered in your `tools` array on messages.create. */\n name: string;\n /** The model's structured input for this tool — JSON-serializable. */\n input: Record<string, unknown>;\n}\n\n/**\n * Produce the canonical bytes for an Anthropic tool_use. The bytes are what\n * the agent-action envelope's `content.hash` is a SHA-256 of.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"id\": <tool_use_id>,\n * \"input\": <canonical JSON of input>,\n * \"name\": <tool name>\n * }\n */\nexport function canonicalizeToolUse(t: AnthropicToolUse): Uint8Array {\n const canon = {\n id: t.id,\n input: t.input,\n name: t.name,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function toolUseHash(t: AnthropicToolUse): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeToolUse(t)));\n}\n\nexport function toolUseId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n t: AnthropicToolUse\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: toolUseHash(t),\n content_length: canonicalizeToolUse(t).byteLength,\n content_mime: 'application/vnd.oc-agent.anthropic-tool-use+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamped tool_use\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampToolUseInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n toolUse: AnthropicToolUse;\n /** Defaults to `anthropic:tool(name=<tool>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\n/**\n * Stamp an Anthropic tool_use without executing it. Useful for pre-\n * authorization flows or batch stamping.\n */\nexport async function stampToolUse(input: StampToolUseInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;\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 `stampToolUse: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeToolUse(input.toolUse);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolUseHash(input.toolUse), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.anthropic-tool-use+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// invokeWithStamp — call the tool AND produce the stamp around it\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface InvokeWithStampInput<T> extends StampToolUseInput {\n /** Your handler that actually runs the tool. Returns whatever the tool returns. */\n call: (toolUse: AnthropicToolUse) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n}\n\n/**\n * Stamp the tool_use first, then execute the handler. The stamped envelope\n * is returned alongside the result so the caller can ship them together —\n * e.g. as a side-channel to an audit log, or as the `_oc_agent` metadata on\n * the tool_result content block back to Claude.\n */\nexport async function invokeWithStamp<T>(\n input: InvokeWithStampInput<T>\n): Promise<InvokeWithStampResult<T>> {\n const action = await stampToolUse(input);\n const result = await input.call(input.toolUse);\n return { result, action };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Console integration: POST stamped actions to console.ochk.io/api/actions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ConsoleClient {\n /** Defaults to https://console.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 (Node 18 LTS w/o global fetch, etc). */\n fetch?: typeof fetch;\n}\n\nexport interface PostActionResult {\n id: string;\n project_id: string;\n delegation_id: string;\n}\n\n/**\n * POST a stamped action envelope to console.ochk.io's /api/actions\n * registry. The console:\n * - re-derives the action id from canonical inputs and rejects on\n * mismatch (tamper defense)\n * - validates that the agent address matches the parent delegation's\n * agent (forgery defense)\n * - persists, fans out to Nostr (kind 30084), submits to OC Stamp,\n * and triggers any subscribed webhooks (action.registered)\n *\n * Returns the registered action's id + project_id + delegation_id.\n * Throws on non-2xx with the server's reason string.\n */\nexport async function postActionToConsole(\n action: ActionEnvelope,\n client: ConsoleClient\n): Promise<PostActionResult> {\n const baseUrl = client.baseUrl ?? 'https://console.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; keep the http_<n> reason\n }\n throw new Error(`postActionToConsole failed: ${reason}`);\n }\n const j = (await r.json()) as { ok: true; action: PostActionResult };\n return j.action;\n}\n\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp (no posting). */\n console?: ConsoleClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when console is configured AND the POST succeeded. null otherwise. */\n posted: PostActionResult | null;\n}\n\n/**\n * Convenience: stamp the tool_use, execute the handler, AND post the\n * stamped envelope to console. Three roundtrips → one call. If `console`\n * is omitted, this is identical to invokeWithStamp + posted=null.\n *\n * The post happens AFTER the call so a failing post doesn't prevent the\n * tool from running. If you need pre-authorization (the post must\n * succeed before the call runs), use stampToolUse + postActionToConsole\n * + your call() in that order.\n */\nexport async function invokeWithStampAndPost<T>(\n input: InvokeWithStampAndPostInput<T>\n): Promise<InvokeWithStampAndPostResult<T>> {\n const { result, action } = await invokeWithStamp(input);\n let posted: PostActionResult | null = null;\n if (input.console) {\n try {\n posted = await postActionToConsole(action, input.console);\n } catch (err) {\n // Best-effort post — surfacing the error here would force\n // every caller to handle \"tool ran fine but post failed\",\n // which is rarely what they want. Log and continue.\n // eslint-disable-next-line no-console\n console.error('[oc-agent-anthropic] postActionToConsole failed:', err);\n }\n }\n return { result, action, posted };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@orangecheck/agent-anthropic",
3
+ "version": "0.1.0",
4
+ "description": "Wrap Anthropic Tool Use (Claude tool_use blocks) with OC Agent scope enforcement and emit a signed agent-action envelope per tool execution.",
5
+ "keywords": [
6
+ "bitcoin",
7
+ "agent",
8
+ "authority",
9
+ "anthropic",
10
+ "claude",
11
+ "tool-use",
12
+ "bip322",
13
+ "oc-agent",
14
+ "orangecheck"
15
+ ],
16
+ "author": "OrangeCheck",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/orangecheck/oc-packages.git",
21
+ "directory": "agent-anthropic"
22
+ },
23
+ "homepage": "https://github.com/orangecheck/oc-agent-protocol",
24
+ "bugs": {
25
+ "url": "https://github.com/orangecheck/oc-agent-protocol/issues"
26
+ },
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.mjs",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.mjs",
34
+ "require": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "dev": "tsup --watch",
46
+ "type-check": "tsc --noEmit",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "clean": "rm -rf dist",
50
+ "prepublishOnly": "npm run clean && npm run build"
51
+ },
52
+ "dependencies": {
53
+ "@noble/hashes": "^1.5.0",
54
+ "@orangecheck/agent-core": "^0.3.0",
55
+ "@orangecheck/agent-signer": "^0.1.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^22.10.2",
59
+ "tsup": "^8.3.5",
60
+ "typescript": "^5.7.2",
61
+ "vitest": "^3.2.4"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }
@@ -0,0 +1,159 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ canonicalizeToolUse,
5
+ postActionToConsole,
6
+ toolUseHash,
7
+ } from './index.js';
8
+ import type { ActionEnvelope } from '@orangecheck/agent-signer';
9
+
10
+ describe('canonicalizeToolUse', () => {
11
+ it('produces byte-identical output for identical inputs (key reorder)', () => {
12
+ const a = canonicalizeToolUse({
13
+ id: 'toolu_01abc',
14
+ name: 'invoice.create',
15
+ input: { customer: 'acme', amount: 14.2 },
16
+ });
17
+ const b = canonicalizeToolUse({
18
+ id: 'toolu_01abc',
19
+ input: { amount: 14.2, customer: 'acme' }, // keys reordered
20
+ name: 'invoice.create',
21
+ });
22
+ expect(a).toEqual(b);
23
+ });
24
+
25
+ it('produces different hashes for different argument values', () => {
26
+ const h1 = toolUseHash({
27
+ id: 'toolu_01abc',
28
+ name: 'invoice.create',
29
+ input: { amount: 14.2 },
30
+ });
31
+ const h2 = toolUseHash({
32
+ id: 'toolu_01abc',
33
+ name: 'invoice.create',
34
+ input: { amount: 28.0 },
35
+ });
36
+ expect(h1).not.toBe(h2);
37
+ });
38
+
39
+ it('changes when the tool name changes', () => {
40
+ const h1 = toolUseHash({ id: 't1', name: 'a', input: {} });
41
+ const h2 = toolUseHash({ id: 't1', name: 'b', input: {} });
42
+ expect(h1).not.toBe(h2);
43
+ });
44
+
45
+ it('changes when the tool_use id changes', () => {
46
+ const h1 = toolUseHash({ id: 't1', name: 'a', input: {} });
47
+ const h2 = toolUseHash({ id: 't2', name: 'a', input: {} });
48
+ expect(h1).not.toBe(h2);
49
+ });
50
+ });
51
+
52
+ describe('postActionToConsole', () => {
53
+ const fakeAction: ActionEnvelope = {
54
+ v: 1,
55
+ kind: 'agent-action',
56
+ id: 'a'.repeat(64),
57
+ signer: { address: 'bc1qbot00000000000000000000000000000000000a', alg: 'bip322' },
58
+ delegation_id: 'd'.repeat(64),
59
+ scope_exercised: 'anthropic:tool(name=invoice.create)',
60
+ content: {
61
+ hash: 'sha256:' + 'b'.repeat(64),
62
+ length: 1024,
63
+ mime: 'application/vnd.oc-agent.anthropic-tool-use+json',
64
+ },
65
+ signed_at: '2026-04-30T12:00:00Z',
66
+ ots: null,
67
+ sig: { alg: 'bip322', pubkey: 'bc1qbot00000000000000000000000000000000000a', value: 'CCCC' },
68
+ };
69
+
70
+ it('POSTs to /api/actions with Bearer auth + the action body fields', async () => {
71
+ const fakeFetch = vi.fn(async (url: string | URL, init?: RequestInit) => {
72
+ expect(String(url)).toBe('https://console.example/api/actions');
73
+ const headers = init!.headers as Record<string, string>;
74
+ expect(headers.Authorization).toBe('Bearer ock_xxxx');
75
+ expect(headers['Content-Type']).toBe('application/json');
76
+ const body = JSON.parse(init!.body as string);
77
+ expect(body.id).toBe(fakeAction.id);
78
+ expect(body.delegation_id).toBe(fakeAction.delegation_id);
79
+ expect(body.agent_address).toBe(fakeAction.signer.address);
80
+ expect(body.scope_exercised).toBe(fakeAction.scope_exercised);
81
+ expect(body.content_hash).toBe(fakeAction.content.hash);
82
+ expect(body.signature).toBe(fakeAction.sig.value);
83
+ expect(body.project_id).toBe('proj_test');
84
+ return new Response(
85
+ JSON.stringify({
86
+ ok: true,
87
+ action: {
88
+ id: fakeAction.id,
89
+ project_id: 'proj_test',
90
+ delegation_id: fakeAction.delegation_id,
91
+ },
92
+ }),
93
+ { status: 201, headers: { 'Content-Type': 'application/json' } }
94
+ );
95
+ });
96
+ const result = await postActionToConsole(fakeAction, {
97
+ apiToken: 'ock_xxxx',
98
+ projectId: 'proj_test',
99
+ baseUrl: 'https://console.example',
100
+ fetch: fakeFetch as unknown as typeof fetch,
101
+ });
102
+ expect(result.id).toBe(fakeAction.id);
103
+ expect(result.project_id).toBe('proj_test');
104
+ expect(fakeFetch).toHaveBeenCalledOnce();
105
+ });
106
+
107
+ it('throws with the server reason on non-2xx', async () => {
108
+ const fakeFetch = vi.fn(async () =>
109
+ new Response(JSON.stringify({ ok: false, reason: 'agent_must_match_delegation' }), {
110
+ status: 403,
111
+ headers: { 'Content-Type': 'application/json' },
112
+ })
113
+ );
114
+ await expect(
115
+ postActionToConsole(fakeAction, {
116
+ apiToken: 'ock_xxxx',
117
+ projectId: 'proj_test',
118
+ baseUrl: 'https://console.example',
119
+ fetch: fakeFetch as unknown as typeof fetch,
120
+ })
121
+ ).rejects.toThrow(/agent_must_match_delegation/);
122
+ });
123
+
124
+ it('falls back to http_<status> when the body is not JSON', async () => {
125
+ const fakeFetch = vi.fn(async () =>
126
+ new Response('upstream error', { status: 502, headers: { 'Content-Type': 'text/plain' } })
127
+ );
128
+ await expect(
129
+ postActionToConsole(fakeAction, {
130
+ apiToken: 'ock_xxxx',
131
+ projectId: 'proj_test',
132
+ baseUrl: 'https://console.example',
133
+ fetch: fakeFetch as unknown as typeof fetch,
134
+ })
135
+ ).rejects.toThrow(/http_502/);
136
+ });
137
+
138
+ it('defaults to https://console.ochk.io when baseUrl is omitted', async () => {
139
+ const fakeFetch = vi.fn(async (url: string | URL) => {
140
+ expect(String(url)).toBe('https://console.ochk.io/api/actions');
141
+ return new Response(
142
+ JSON.stringify({
143
+ ok: true,
144
+ action: {
145
+ id: fakeAction.id,
146
+ project_id: 'proj_test',
147
+ delegation_id: fakeAction.delegation_id,
148
+ },
149
+ }),
150
+ { status: 201, headers: { 'Content-Type': 'application/json' } }
151
+ );
152
+ });
153
+ await postActionToConsole(fakeAction, {
154
+ apiToken: 'ock_xxxx',
155
+ projectId: 'proj_test',
156
+ fetch: fakeFetch as unknown as typeof fetch,
157
+ });
158
+ });
159
+ });
package/src/index.ts ADDED
@@ -0,0 +1,260 @@
1
+ // @orangecheck/agent-anthropic — wrap Anthropic Tool Use invocations in
2
+ // OC Agent action envelopes.
3
+ //
4
+ // Anthropic's Messages API surfaces tool calls as `tool_use` content
5
+ // blocks: { type: 'tool_use', id, name, input }. This module canonicalizes
6
+ // each tool_use, hashes it, and produces an agent-action envelope binding
7
+ // the BIP-322-authenticated agent to the (tool_name, input) tuple — at a
8
+ // scope of `anthropic:tool(name=<tool>)`.
9
+
10
+ import { sha256 } from '@noble/hashes/sha256';
11
+ import {
12
+ canonicalize,
13
+ computeActionId,
14
+ hexEncode,
15
+ isSubScope,
16
+ parseScope,
17
+ type DelegationEnvelope,
18
+ } from '@orangecheck/agent-core';
19
+ import {
20
+ signAsAgent,
21
+ type ActionEnvelope,
22
+ type SignerRef,
23
+ } from '@orangecheck/agent-signer';
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // Canonicalization of an Anthropic tool_use
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ export interface AnthropicToolUse {
30
+ /** The `id` Anthropic assigns to this tool_use block (passed back as tool_use_id on result). */
31
+ id: string;
32
+ /** The tool name registered in your `tools` array on messages.create. */
33
+ name: string;
34
+ /** The model's structured input for this tool — JSON-serializable. */
35
+ input: Record<string, unknown>;
36
+ }
37
+
38
+ /**
39
+ * Produce the canonical bytes for an Anthropic tool_use. The bytes are what
40
+ * the agent-action envelope's `content.hash` is a SHA-256 of.
41
+ *
42
+ * Shape (RFC 8785 canonical JSON, then trailing LF):
43
+ * {
44
+ * "id": <tool_use_id>,
45
+ * "input": <canonical JSON of input>,
46
+ * "name": <tool name>
47
+ * }
48
+ */
49
+ export function canonicalizeToolUse(t: AnthropicToolUse): Uint8Array {
50
+ const canon = {
51
+ id: t.id,
52
+ input: t.input,
53
+ name: t.name,
54
+ };
55
+ const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);
56
+ return new TextEncoder().encode(str + '\n');
57
+ }
58
+
59
+ export function toolUseHash(t: AnthropicToolUse): string {
60
+ return 'sha256:' + hexEncode(sha256(canonicalizeToolUse(t)));
61
+ }
62
+
63
+ export function toolUseId(
64
+ agentAddress: string,
65
+ signedAt: string,
66
+ delegationId: string,
67
+ scopeExercised: string,
68
+ t: AnthropicToolUse
69
+ ): string {
70
+ return computeActionId({
71
+ address: agentAddress,
72
+ content_hash: toolUseHash(t),
73
+ content_length: canonicalizeToolUse(t).byteLength,
74
+ content_mime: 'application/vnd.oc-agent.anthropic-tool-use+json',
75
+ signed_at: signedAt,
76
+ delegation_id: delegationId,
77
+ scope_exercised: scopeExercised,
78
+ });
79
+ }
80
+
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ // Stamped tool_use
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+
85
+ export interface StampToolUseInput {
86
+ agent: SignerRef;
87
+ delegation: DelegationEnvelope;
88
+ toolUse: AnthropicToolUse;
89
+ /** Defaults to `anthropic:tool(name=<tool>)` — the tightest admissible sub-scope. */
90
+ scopeExercised?: string;
91
+ signedAt?: Date;
92
+ }
93
+
94
+ /**
95
+ * Stamp an Anthropic tool_use without executing it. Useful for pre-
96
+ * authorization flows or batch stamping.
97
+ */
98
+ export async function stampToolUse(input: StampToolUseInput): Promise<ActionEnvelope> {
99
+ const scopeExercised =
100
+ input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;
101
+
102
+ const granted = (input.delegation.scopes ?? []).map(parseScope);
103
+ const exercisedParsed = parseScope(scopeExercised);
104
+ if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
105
+ throw new Error(
106
+ `stampToolUse: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
107
+ );
108
+ }
109
+
110
+ const bytes = canonicalizeToolUse(input.toolUse);
111
+ return signAsAgent({
112
+ agent: input.agent,
113
+ delegation: input.delegation as never,
114
+ content: { hash: toolUseHash(input.toolUse), length: bytes.byteLength },
115
+ mime: 'application/vnd.oc-agent.anthropic-tool-use+json',
116
+ scopeExercised,
117
+ signedAt: input.signedAt,
118
+ });
119
+ }
120
+
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+ // invokeWithStamp — call the tool AND produce the stamp around it
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+
125
+ export interface InvokeWithStampInput<T> extends StampToolUseInput {
126
+ /** Your handler that actually runs the tool. Returns whatever the tool returns. */
127
+ call: (toolUse: AnthropicToolUse) => Promise<T>;
128
+ }
129
+
130
+ export interface InvokeWithStampResult<T> {
131
+ result: T;
132
+ action: ActionEnvelope;
133
+ }
134
+
135
+ /**
136
+ * Stamp the tool_use first, then execute the handler. The stamped envelope
137
+ * is returned alongside the result so the caller can ship them together —
138
+ * e.g. as a side-channel to an audit log, or as the `_oc_agent` metadata on
139
+ * the tool_result content block back to Claude.
140
+ */
141
+ export async function invokeWithStamp<T>(
142
+ input: InvokeWithStampInput<T>
143
+ ): Promise<InvokeWithStampResult<T>> {
144
+ const action = await stampToolUse(input);
145
+ const result = await input.call(input.toolUse);
146
+ return { result, action };
147
+ }
148
+
149
+ // ─────────────────────────────────────────────────────────────────────────────
150
+ // Console integration: POST stamped actions to console.ochk.io/api/actions
151
+ // ─────────────────────────────────────────────────────────────────────────────
152
+
153
+ export interface ConsoleClient {
154
+ /** Defaults to https://console.ochk.io. */
155
+ baseUrl?: string;
156
+ /** Bearer token from /settings § 03 (`ock_<hex>`). */
157
+ apiToken: string;
158
+ /** Project the action belongs to (proj_*). */
159
+ projectId: string;
160
+ /** Optional fetch override for runtimes that need it (Node 18 LTS w/o global fetch, etc). */
161
+ fetch?: typeof fetch;
162
+ }
163
+
164
+ export interface PostActionResult {
165
+ id: string;
166
+ project_id: string;
167
+ delegation_id: string;
168
+ }
169
+
170
+ /**
171
+ * POST a stamped action envelope to console.ochk.io's /api/actions
172
+ * registry. The console:
173
+ * - re-derives the action id from canonical inputs and rejects on
174
+ * mismatch (tamper defense)
175
+ * - validates that the agent address matches the parent delegation's
176
+ * agent (forgery defense)
177
+ * - persists, fans out to Nostr (kind 30084), submits to OC Stamp,
178
+ * and triggers any subscribed webhooks (action.registered)
179
+ *
180
+ * Returns the registered action's id + project_id + delegation_id.
181
+ * Throws on non-2xx with the server's reason string.
182
+ */
183
+ export async function postActionToConsole(
184
+ action: ActionEnvelope,
185
+ client: ConsoleClient
186
+ ): Promise<PostActionResult> {
187
+ const baseUrl = client.baseUrl ?? 'https://console.ochk.io';
188
+ const f = client.fetch ?? fetch;
189
+ const body = {
190
+ project_id: client.projectId,
191
+ delegation_id: action.delegation_id,
192
+ agent_address: action.signer.address,
193
+ scope_exercised: action.scope_exercised,
194
+ content_hash: action.content.hash,
195
+ content_length: action.content.length,
196
+ content_mime: action.content.mime,
197
+ signed_at: action.signed_at,
198
+ signature: action.sig.value,
199
+ id: action.id,
200
+ };
201
+ const r = await f(`${baseUrl}/api/actions`, {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ Authorization: `Bearer ${client.apiToken}`,
206
+ },
207
+ body: JSON.stringify(body),
208
+ });
209
+ if (!r.ok) {
210
+ let reason = `http_${r.status}`;
211
+ try {
212
+ const j = (await r.json()) as { reason?: string };
213
+ if (j.reason) reason = j.reason;
214
+ } catch {
215
+ // body wasn't json; keep the http_<n> reason
216
+ }
217
+ throw new Error(`postActionToConsole failed: ${reason}`);
218
+ }
219
+ const j = (await r.json()) as { ok: true; action: PostActionResult };
220
+ return j.action;
221
+ }
222
+
223
+ export interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
224
+ /** Console credentials. If absent, behaves like invokeWithStamp (no posting). */
225
+ console?: ConsoleClient;
226
+ }
227
+
228
+ export interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
229
+ /** Set when console is configured AND the POST succeeded. null otherwise. */
230
+ posted: PostActionResult | null;
231
+ }
232
+
233
+ /**
234
+ * Convenience: stamp the tool_use, execute the handler, AND post the
235
+ * stamped envelope to console. Three roundtrips → one call. If `console`
236
+ * is omitted, this is identical to invokeWithStamp + posted=null.
237
+ *
238
+ * The post happens AFTER the call so a failing post doesn't prevent the
239
+ * tool from running. If you need pre-authorization (the post must
240
+ * succeed before the call runs), use stampToolUse + postActionToConsole
241
+ * + your call() in that order.
242
+ */
243
+ export async function invokeWithStampAndPost<T>(
244
+ input: InvokeWithStampAndPostInput<T>
245
+ ): Promise<InvokeWithStampAndPostResult<T>> {
246
+ const { result, action } = await invokeWithStamp(input);
247
+ let posted: PostActionResult | null = null;
248
+ if (input.console) {
249
+ try {
250
+ posted = await postActionToConsole(action, input.console);
251
+ } catch (err) {
252
+ // Best-effort post — surfacing the error here would force
253
+ // every caller to handle "tool ran fine but post failed",
254
+ // which is rarely what they want. Log and continue.
255
+ // eslint-disable-next-line no-console
256
+ console.error('[oc-agent-anthropic] postActionToConsole failed:', err);
257
+ }
258
+ }
259
+ return { result, action, posted };
260
+ }