@orangecheck/agent-anthropic 0.1.1 → 0.3.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 +5 -1
- package/dist/index.d.mts +14 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +40 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +41 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -4
- package/src/index.test.ts +12 -12
- package/src/index.ts +82 -31
- package/src/scope-preflight.test.ts +55 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @orangecheck/agent-anthropic
|
|
2
2
|
|
|
3
|
+
> **Full reference:** [docs.ochk.io/sdk/agent-anthropic](https://docs.ochk.io/sdk/agent-anthropic) — 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 [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
8
|
|
|
5
9
|
## Why
|
|
@@ -94,4 +98,4 @@ MIT.
|
|
|
94
98
|
- [@orangecheck/agent-core](https://www.npmjs.com/package/@orangecheck/agent-core) — verifier library.
|
|
95
99
|
- [@orangecheck/agent-mcp](https://www.npmjs.com/package/@orangecheck/agent-mcp) — same pattern, for MCP.
|
|
96
100
|
- [@orangecheck/agent-openai](https://www.npmjs.com/package/@orangecheck/agent-openai) — same pattern, for OpenAI Responses + Chat Completions.
|
|
97
|
-
- [
|
|
101
|
+
- [fleet.ochk.io/integrations/anthropic](https://fleet.ochk.io/integrations/anthropic) — the managed-tier story this slots into.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { DelegationEnvelope } from '@orangecheck/agent-core';
|
|
2
2
|
import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
|
|
3
|
-
import { ConsoleClient, PostActionResult } from '@orangecheck/agent-console-client';
|
|
4
|
-
export { ConsoleClient, PostActionResult, postActionToConsole } from '@orangecheck/agent-console-client';
|
|
5
3
|
|
|
6
4
|
interface AnthropicToolUse {
|
|
7
5
|
id: string;
|
|
@@ -27,13 +25,24 @@ interface InvokeWithStampResult<T> {
|
|
|
27
25
|
action: ActionEnvelope;
|
|
28
26
|
}
|
|
29
27
|
declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
|
|
30
|
-
|
|
28
|
+
interface FleetClient {
|
|
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 postActionToFleet(action: ActionEnvelope, client: FleetClient): Promise<PostActionResult>;
|
|
31
40
|
interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
32
|
-
|
|
41
|
+
fleet?: FleetClient;
|
|
33
42
|
}
|
|
34
43
|
interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
35
44
|
posted: PostActionResult | null;
|
|
36
45
|
}
|
|
37
46
|
declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
|
|
38
47
|
|
|
39
|
-
export { type AnthropicToolUse, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type StampToolUseInput, canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, stampToolUse, toolUseHash, toolUseId };
|
|
48
|
+
export { type AnthropicToolUse, type FleetClient, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type PostActionResult, type StampToolUseInput, canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, postActionToFleet, stampToolUse, toolUseHash, toolUseId };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { DelegationEnvelope } from '@orangecheck/agent-core';
|
|
2
2
|
import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
|
|
3
|
-
import { ConsoleClient, PostActionResult } from '@orangecheck/agent-console-client';
|
|
4
|
-
export { ConsoleClient, PostActionResult, postActionToConsole } from '@orangecheck/agent-console-client';
|
|
5
3
|
|
|
6
4
|
interface AnthropicToolUse {
|
|
7
5
|
id: string;
|
|
@@ -27,13 +25,24 @@ interface InvokeWithStampResult<T> {
|
|
|
27
25
|
action: ActionEnvelope;
|
|
28
26
|
}
|
|
29
27
|
declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
|
|
30
|
-
|
|
28
|
+
interface FleetClient {
|
|
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 postActionToFleet(action: ActionEnvelope, client: FleetClient): Promise<PostActionResult>;
|
|
31
40
|
interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
32
|
-
|
|
41
|
+
fleet?: FleetClient;
|
|
33
42
|
}
|
|
34
43
|
interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
35
44
|
posted: PostActionResult | null;
|
|
36
45
|
}
|
|
37
46
|
declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
|
|
38
47
|
|
|
39
|
-
export { type AnthropicToolUse, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type StampToolUseInput, canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, stampToolUse, toolUseHash, toolUseId };
|
|
48
|
+
export { type AnthropicToolUse, type FleetClient, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type PostActionResult, type StampToolUseInput, canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, postActionToFleet, stampToolUse, toolUseHash, toolUseId };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
var sha256 = require('@noble/hashes/sha256');
|
|
4
4
|
var agentCore = require('@orangecheck/agent-core');
|
|
5
5
|
var agentSigner = require('@orangecheck/agent-signer');
|
|
6
|
-
var agentConsoleClient = require('@orangecheck/agent-console-client');
|
|
7
6
|
|
|
8
7
|
// src/index.ts
|
|
9
8
|
function canonicalizeToolUse(t) {
|
|
@@ -31,13 +30,7 @@ function toolUseId(agentAddress, signedAt, delegationId, scopeExercised, t) {
|
|
|
31
30
|
}
|
|
32
31
|
async function stampToolUse(input) {
|
|
33
32
|
const scopeExercised = input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;
|
|
34
|
-
|
|
35
|
-
const exercisedParsed = agentCore.parseScope(scopeExercised);
|
|
36
|
-
if (!granted.some((g) => agentCore.isSubScope(exercisedParsed, g))) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
`stampToolUse: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
39
|
-
);
|
|
40
|
-
}
|
|
33
|
+
agentCore.assertScopeGranted(input.delegation, scopeExercised, "stampToolUse");
|
|
41
34
|
const bytes = canonicalizeToolUse(input.toolUse);
|
|
42
35
|
return agentSigner.signAsAgent({
|
|
43
36
|
agent: input.agent,
|
|
@@ -53,26 +46,58 @@ async function invokeWithStamp(input) {
|
|
|
53
46
|
const result = await input.call(input.toolUse);
|
|
54
47
|
return { result, action };
|
|
55
48
|
}
|
|
49
|
+
async function postActionToFleet(action, client) {
|
|
50
|
+
const baseUrl = client.baseUrl ?? "https://fleet.ochk.io";
|
|
51
|
+
const f = client.fetch ?? fetch;
|
|
52
|
+
const body = {
|
|
53
|
+
project_id: client.projectId,
|
|
54
|
+
delegation_id: action.delegation_id,
|
|
55
|
+
agent_address: action.signer.address,
|
|
56
|
+
scope_exercised: action.scope_exercised,
|
|
57
|
+
content_hash: action.content.hash,
|
|
58
|
+
content_length: action.content.length,
|
|
59
|
+
content_mime: action.content.mime,
|
|
60
|
+
signed_at: action.signed_at,
|
|
61
|
+
signature: action.sig.value,
|
|
62
|
+
id: action.id
|
|
63
|
+
};
|
|
64
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
Authorization: `Bearer ${client.apiToken}`
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify(body)
|
|
71
|
+
});
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
let reason = `http_${r.status}`;
|
|
74
|
+
try {
|
|
75
|
+
const j2 = await r.json();
|
|
76
|
+
if (j2.reason) reason = j2.reason;
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`postActionToFleet failed: ${reason}`);
|
|
80
|
+
}
|
|
81
|
+
const j = await r.json();
|
|
82
|
+
return j.action;
|
|
83
|
+
}
|
|
56
84
|
async function invokeWithStampAndPost(input) {
|
|
57
85
|
const { result, action } = await invokeWithStamp(input);
|
|
58
86
|
let posted = null;
|
|
59
|
-
if (input.
|
|
87
|
+
if (input.fleet) {
|
|
60
88
|
try {
|
|
61
|
-
posted = await
|
|
89
|
+
posted = await postActionToFleet(action, input.fleet);
|
|
62
90
|
} catch (err) {
|
|
63
|
-
console.error("[oc-agent-anthropic]
|
|
91
|
+
console.error("[oc-agent-anthropic] postActionToFleet failed:", err);
|
|
64
92
|
}
|
|
65
93
|
}
|
|
66
94
|
return { result, action, posted };
|
|
67
95
|
}
|
|
68
96
|
|
|
69
|
-
Object.defineProperty(exports, "postActionToConsole", {
|
|
70
|
-
enumerable: true,
|
|
71
|
-
get: function () { return agentConsoleClient.postActionToConsole; }
|
|
72
|
-
});
|
|
73
97
|
exports.canonicalizeToolUse = canonicalizeToolUse;
|
|
74
98
|
exports.invokeWithStamp = invokeWithStamp;
|
|
75
99
|
exports.invokeWithStampAndPost = invokeWithStampAndPost;
|
|
100
|
+
exports.postActionToFleet = postActionToFleet;
|
|
76
101
|
exports.stampToolUse = stampToolUse;
|
|
77
102
|
exports.toolUseHash = toolUseHash;
|
|
78
103
|
exports.toolUseId = toolUseId;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","parseScope","isSubScope","signAsAgent","_post"],"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 — re-export from the shared client\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport {\n postActionToConsole,\n type ConsoleClient,\n type PostActionResult,\n} from '@orangecheck/agent-console-client';\n\nimport {\n postActionToConsole as _post,\n type ConsoleClient as _ConsoleClient,\n type PostActionResult as _PostActionResult,\n} from '@orangecheck/agent-console-client';\n\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp + posted: null. */\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 _post(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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","assertScopeGranted","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 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// 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 assertScopeGranted(input.delegation, scopeExercised, 'stampToolUse');\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// 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 (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 fleet.ochk.io's /api/actions\n * registry. The fleet:\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 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; keep the http_<n> reason\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\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Fleet credentials. If absent, behaves like invokeWithStamp (no posting). */\n fleet?: FleetClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when fleet 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 fleet. Three roundtrips → one call. If `fleet`\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 + postActionToFleet\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.fleet) {\n try {\n posted = await postActionToFleet(action, input.fleet);\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] postActionToFleet failed:', err);\n }\n }\n return { result, action, posted };\n}\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
-
import { canonicalize, hexEncode, computeActionId,
|
|
2
|
+
import { canonicalize, hexEncode, computeActionId, assertScopeGranted } from '@orangecheck/agent-core';
|
|
3
3
|
import { signAsAgent } from '@orangecheck/agent-signer';
|
|
4
|
-
import { postActionToConsole } from '@orangecheck/agent-console-client';
|
|
5
|
-
export { postActionToConsole } from '@orangecheck/agent-console-client';
|
|
6
4
|
|
|
7
5
|
// src/index.ts
|
|
8
6
|
function canonicalizeToolUse(t) {
|
|
@@ -30,13 +28,7 @@ function toolUseId(agentAddress, signedAt, delegationId, scopeExercised, t) {
|
|
|
30
28
|
}
|
|
31
29
|
async function stampToolUse(input) {
|
|
32
30
|
const scopeExercised = input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;
|
|
33
|
-
|
|
34
|
-
const exercisedParsed = parseScope(scopeExercised);
|
|
35
|
-
if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
`stampToolUse: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
31
|
+
assertScopeGranted(input.delegation, scopeExercised, "stampToolUse");
|
|
40
32
|
const bytes = canonicalizeToolUse(input.toolUse);
|
|
41
33
|
return signAsAgent({
|
|
42
34
|
agent: input.agent,
|
|
@@ -52,19 +44,54 @@ async function invokeWithStamp(input) {
|
|
|
52
44
|
const result = await input.call(input.toolUse);
|
|
53
45
|
return { result, action };
|
|
54
46
|
}
|
|
47
|
+
async function postActionToFleet(action, client) {
|
|
48
|
+
const baseUrl = client.baseUrl ?? "https://fleet.ochk.io";
|
|
49
|
+
const f = client.fetch ?? fetch;
|
|
50
|
+
const body = {
|
|
51
|
+
project_id: client.projectId,
|
|
52
|
+
delegation_id: action.delegation_id,
|
|
53
|
+
agent_address: action.signer.address,
|
|
54
|
+
scope_exercised: action.scope_exercised,
|
|
55
|
+
content_hash: action.content.hash,
|
|
56
|
+
content_length: action.content.length,
|
|
57
|
+
content_mime: action.content.mime,
|
|
58
|
+
signed_at: action.signed_at,
|
|
59
|
+
signature: action.sig.value,
|
|
60
|
+
id: action.id
|
|
61
|
+
};
|
|
62
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
Authorization: `Bearer ${client.apiToken}`
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(body)
|
|
69
|
+
});
|
|
70
|
+
if (!r.ok) {
|
|
71
|
+
let reason = `http_${r.status}`;
|
|
72
|
+
try {
|
|
73
|
+
const j2 = await r.json();
|
|
74
|
+
if (j2.reason) reason = j2.reason;
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`postActionToFleet failed: ${reason}`);
|
|
78
|
+
}
|
|
79
|
+
const j = await r.json();
|
|
80
|
+
return j.action;
|
|
81
|
+
}
|
|
55
82
|
async function invokeWithStampAndPost(input) {
|
|
56
83
|
const { result, action } = await invokeWithStamp(input);
|
|
57
84
|
let posted = null;
|
|
58
|
-
if (input.
|
|
85
|
+
if (input.fleet) {
|
|
59
86
|
try {
|
|
60
|
-
posted = await
|
|
87
|
+
posted = await postActionToFleet(action, input.fleet);
|
|
61
88
|
} catch (err) {
|
|
62
|
-
console.error("[oc-agent-anthropic]
|
|
89
|
+
console.error("[oc-agent-anthropic] postActionToFleet failed:", err);
|
|
63
90
|
}
|
|
64
91
|
}
|
|
65
92
|
return { result, action, posted };
|
|
66
93
|
}
|
|
67
94
|
|
|
68
|
-
export { canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, stampToolUse, toolUseHash, toolUseId };
|
|
95
|
+
export { canonicalizeToolUse, invokeWithStamp, invokeWithStampAndPost, postActionToFleet, stampToolUse, toolUseHash, toolUseId };
|
|
69
96
|
//# sourceMappingURL=index.mjs.map
|
|
70
97
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["_post"],"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 — re-export from the shared client\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport {\n postActionToConsole,\n type ConsoleClient,\n type PostActionResult,\n} from '@orangecheck/agent-console-client';\n\nimport {\n postActionToConsole as _post,\n type ConsoleClient as _ConsoleClient,\n type PostActionResult as _PostActionResult,\n} from '@orangecheck/agent-console-client';\n\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp + posted: null. */\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 _post(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"]}
|
|
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 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// 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 assertScopeGranted(input.delegation, scopeExercised, 'stampToolUse');\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// 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 (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 fleet.ochk.io's /api/actions\n * registry. The fleet:\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 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; keep the http_<n> reason\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\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Fleet credentials. If absent, behaves like invokeWithStamp (no posting). */\n fleet?: FleetClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when fleet 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 fleet. Three roundtrips → one call. If `fleet`\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 + postActionToFleet\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.fleet) {\n try {\n posted = await postActionToFleet(action, input.fleet);\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] postActionToFleet failed:', err);\n }\n }\n return { result, action, posted };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/agent-anthropic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"bitcoin",
|
|
@@ -51,9 +51,8 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@noble/hashes": "^1.5.0",
|
|
54
|
-
"@orangecheck/agent-
|
|
55
|
-
"@orangecheck/agent-
|
|
56
|
-
"@orangecheck/agent-signer": "^0.1.0"
|
|
54
|
+
"@orangecheck/agent-core": "^1.2.0",
|
|
55
|
+
"@orangecheck/agent-signer": "^0.3.0"
|
|
57
56
|
},
|
|
58
57
|
"devDependencies": {
|
|
59
58
|
"@types/node": "^22.10.2",
|
package/src/index.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
canonicalizeToolUse,
|
|
5
|
-
|
|
5
|
+
postActionToFleet,
|
|
6
6
|
toolUseHash,
|
|
7
7
|
} from './index.js';
|
|
8
8
|
import type { ActionEnvelope } from '@orangecheck/agent-signer';
|
|
@@ -49,7 +49,7 @@ describe('canonicalizeToolUse', () => {
|
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
describe('
|
|
52
|
+
describe('postActionToFleet', () => {
|
|
53
53
|
const fakeAction: ActionEnvelope = {
|
|
54
54
|
v: 1,
|
|
55
55
|
kind: 'agent-action',
|
|
@@ -69,7 +69,7 @@ describe('postActionToConsole', () => {
|
|
|
69
69
|
|
|
70
70
|
it('POSTs to /api/actions with Bearer auth + the action body fields', async () => {
|
|
71
71
|
const fakeFetch = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
|
72
|
-
expect(String(url)).toBe('https://
|
|
72
|
+
expect(String(url)).toBe('https://fleet.example/api/actions');
|
|
73
73
|
const headers = init!.headers as Record<string, string>;
|
|
74
74
|
expect(headers.Authorization).toBe('Bearer ock_xxxx');
|
|
75
75
|
expect(headers['Content-Type']).toBe('application/json');
|
|
@@ -93,10 +93,10 @@ describe('postActionToConsole', () => {
|
|
|
93
93
|
{ status: 201, headers: { 'Content-Type': 'application/json' } }
|
|
94
94
|
);
|
|
95
95
|
});
|
|
96
|
-
const result = await
|
|
96
|
+
const result = await postActionToFleet(fakeAction, {
|
|
97
97
|
apiToken: 'ock_xxxx',
|
|
98
98
|
projectId: 'proj_test',
|
|
99
|
-
baseUrl: 'https://
|
|
99
|
+
baseUrl: 'https://fleet.example',
|
|
100
100
|
fetch: fakeFetch as unknown as typeof fetch,
|
|
101
101
|
});
|
|
102
102
|
expect(result.id).toBe(fakeAction.id);
|
|
@@ -112,10 +112,10 @@ describe('postActionToConsole', () => {
|
|
|
112
112
|
})
|
|
113
113
|
);
|
|
114
114
|
await expect(
|
|
115
|
-
|
|
115
|
+
postActionToFleet(fakeAction, {
|
|
116
116
|
apiToken: 'ock_xxxx',
|
|
117
117
|
projectId: 'proj_test',
|
|
118
|
-
baseUrl: 'https://
|
|
118
|
+
baseUrl: 'https://fleet.example',
|
|
119
119
|
fetch: fakeFetch as unknown as typeof fetch,
|
|
120
120
|
})
|
|
121
121
|
).rejects.toThrow(/agent_must_match_delegation/);
|
|
@@ -126,18 +126,18 @@ describe('postActionToConsole', () => {
|
|
|
126
126
|
new Response('upstream error', { status: 502, headers: { 'Content-Type': 'text/plain' } })
|
|
127
127
|
);
|
|
128
128
|
await expect(
|
|
129
|
-
|
|
129
|
+
postActionToFleet(fakeAction, {
|
|
130
130
|
apiToken: 'ock_xxxx',
|
|
131
131
|
projectId: 'proj_test',
|
|
132
|
-
baseUrl: 'https://
|
|
132
|
+
baseUrl: 'https://fleet.example',
|
|
133
133
|
fetch: fakeFetch as unknown as typeof fetch,
|
|
134
134
|
})
|
|
135
135
|
).rejects.toThrow(/http_502/);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
it('defaults to https://
|
|
138
|
+
it('defaults to https://fleet.ochk.io when baseUrl is omitted', async () => {
|
|
139
139
|
const fakeFetch = vi.fn(async (url: string | URL) => {
|
|
140
|
-
expect(String(url)).toBe('https://
|
|
140
|
+
expect(String(url)).toBe('https://fleet.ochk.io/api/actions');
|
|
141
141
|
return new Response(
|
|
142
142
|
JSON.stringify({
|
|
143
143
|
ok: true,
|
|
@@ -150,7 +150,7 @@ describe('postActionToConsole', () => {
|
|
|
150
150
|
{ status: 201, headers: { 'Content-Type': 'application/json' } }
|
|
151
151
|
);
|
|
152
152
|
});
|
|
153
|
-
await
|
|
153
|
+
await postActionToFleet(fakeAction, {
|
|
154
154
|
apiToken: 'ock_xxxx',
|
|
155
155
|
projectId: 'proj_test',
|
|
156
156
|
fetch: fakeFetch as unknown as typeof fetch,
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { sha256 } from '@noble/hashes/sha256';
|
|
11
11
|
import {
|
|
12
|
+
assertScopeGranted,
|
|
12
13
|
canonicalize,
|
|
13
14
|
computeActionId,
|
|
14
15
|
hexEncode,
|
|
15
|
-
isSubScope,
|
|
16
|
-
parseScope,
|
|
17
16
|
type DelegationEnvelope,
|
|
18
17
|
} from '@orangecheck/agent-core';
|
|
19
18
|
import {
|
|
@@ -99,13 +98,7 @@ export async function stampToolUse(input: StampToolUseInput): Promise<ActionEnve
|
|
|
99
98
|
const scopeExercised =
|
|
100
99
|
input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;
|
|
101
100
|
|
|
102
|
-
|
|
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
|
-
}
|
|
101
|
+
assertScopeGranted(input.delegation, scopeExercised, 'stampToolUse');
|
|
109
102
|
|
|
110
103
|
const bytes = canonicalizeToolUse(input.toolUse);
|
|
111
104
|
return signAsAgent({
|
|
@@ -147,55 +140,113 @@ export async function invokeWithStamp<T>(
|
|
|
147
140
|
}
|
|
148
141
|
|
|
149
142
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
-
//
|
|
143
|
+
// Fleet integration: POST stamped actions to fleet.ochk.io/api/actions
|
|
151
144
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
145
|
|
|
153
|
-
export {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
export interface FleetClient {
|
|
147
|
+
/** Defaults to https://fleet.ochk.io. */
|
|
148
|
+
baseUrl?: string;
|
|
149
|
+
/** Bearer token from /settings § 03 (`ock_<hex>`). */
|
|
150
|
+
apiToken: string;
|
|
151
|
+
/** Project the action belongs to (proj_*). */
|
|
152
|
+
projectId: string;
|
|
153
|
+
/** Optional fetch override for runtimes that need it (Node 18 LTS w/o global fetch, etc). */
|
|
154
|
+
fetch?: typeof fetch;
|
|
155
|
+
}
|
|
158
156
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
157
|
+
export interface PostActionResult {
|
|
158
|
+
id: string;
|
|
159
|
+
project_id: string;
|
|
160
|
+
delegation_id: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* POST a stamped action envelope to fleet.ochk.io's /api/actions
|
|
165
|
+
* registry. The fleet:
|
|
166
|
+
* - re-derives the action id from canonical inputs and rejects on
|
|
167
|
+
* mismatch (tamper defense)
|
|
168
|
+
* - validates that the agent address matches the parent delegation's
|
|
169
|
+
* agent (forgery defense)
|
|
170
|
+
* - persists, fans out to Nostr (kind 30084), submits to OC Stamp,
|
|
171
|
+
* and triggers any subscribed webhooks (action.registered)
|
|
172
|
+
*
|
|
173
|
+
* Returns the registered action's id + project_id + delegation_id.
|
|
174
|
+
* Throws on non-2xx with the server's reason string.
|
|
175
|
+
*/
|
|
176
|
+
export async function postActionToFleet(
|
|
177
|
+
action: ActionEnvelope,
|
|
178
|
+
client: FleetClient
|
|
179
|
+
): Promise<PostActionResult> {
|
|
180
|
+
const baseUrl = client.baseUrl ?? 'https://fleet.ochk.io';
|
|
181
|
+
const f = client.fetch ?? fetch;
|
|
182
|
+
const body = {
|
|
183
|
+
project_id: client.projectId,
|
|
184
|
+
delegation_id: action.delegation_id,
|
|
185
|
+
agent_address: action.signer.address,
|
|
186
|
+
scope_exercised: action.scope_exercised,
|
|
187
|
+
content_hash: action.content.hash,
|
|
188
|
+
content_length: action.content.length,
|
|
189
|
+
content_mime: action.content.mime,
|
|
190
|
+
signed_at: action.signed_at,
|
|
191
|
+
signature: action.sig.value,
|
|
192
|
+
id: action.id,
|
|
193
|
+
};
|
|
194
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: {
|
|
197
|
+
'Content-Type': 'application/json',
|
|
198
|
+
Authorization: `Bearer ${client.apiToken}`,
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify(body),
|
|
201
|
+
});
|
|
202
|
+
if (!r.ok) {
|
|
203
|
+
let reason = `http_${r.status}`;
|
|
204
|
+
try {
|
|
205
|
+
const j = (await r.json()) as { reason?: string };
|
|
206
|
+
if (j.reason) reason = j.reason;
|
|
207
|
+
} catch {
|
|
208
|
+
// body wasn't json; keep the http_<n> reason
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`postActionToFleet failed: ${reason}`);
|
|
211
|
+
}
|
|
212
|
+
const j = (await r.json()) as { ok: true; action: PostActionResult };
|
|
213
|
+
return j.action;
|
|
214
|
+
}
|
|
164
215
|
|
|
165
216
|
export interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
166
|
-
/**
|
|
167
|
-
|
|
217
|
+
/** Fleet credentials. If absent, behaves like invokeWithStamp (no posting). */
|
|
218
|
+
fleet?: FleetClient;
|
|
168
219
|
}
|
|
169
220
|
|
|
170
221
|
export interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
171
|
-
/** Set when
|
|
172
|
-
posted:
|
|
222
|
+
/** Set when fleet is configured AND the POST succeeded. null otherwise. */
|
|
223
|
+
posted: PostActionResult | null;
|
|
173
224
|
}
|
|
174
225
|
|
|
175
226
|
/**
|
|
176
227
|
* Convenience: stamp the tool_use, execute the handler, AND post the
|
|
177
|
-
* stamped envelope to
|
|
178
|
-
* is omitted, this is identical to invokeWithStamp + posted
|
|
228
|
+
* stamped envelope to fleet. Three roundtrips → one call. If `fleet`
|
|
229
|
+
* is omitted, this is identical to invokeWithStamp + posted=null.
|
|
179
230
|
*
|
|
180
231
|
* The post happens AFTER the call so a failing post doesn't prevent the
|
|
181
232
|
* tool from running. If you need pre-authorization (the post must
|
|
182
|
-
* succeed before the call runs), use stampToolUse +
|
|
233
|
+
* succeed before the call runs), use stampToolUse + postActionToFleet
|
|
183
234
|
* + your call() in that order.
|
|
184
235
|
*/
|
|
185
236
|
export async function invokeWithStampAndPost<T>(
|
|
186
237
|
input: InvokeWithStampAndPostInput<T>
|
|
187
238
|
): Promise<InvokeWithStampAndPostResult<T>> {
|
|
188
239
|
const { result, action } = await invokeWithStamp(input);
|
|
189
|
-
let posted:
|
|
190
|
-
if (input.
|
|
240
|
+
let posted: PostActionResult | null = null;
|
|
241
|
+
if (input.fleet) {
|
|
191
242
|
try {
|
|
192
|
-
posted = await
|
|
243
|
+
posted = await postActionToFleet(action, input.fleet);
|
|
193
244
|
} catch (err) {
|
|
194
245
|
// Best-effort post — surfacing the error here would force
|
|
195
246
|
// every caller to handle "tool ran fine but post failed",
|
|
196
247
|
// which is rarely what they want. Log and continue.
|
|
197
248
|
// eslint-disable-next-line no-console
|
|
198
|
-
console.error('[oc-agent-anthropic]
|
|
249
|
+
console.error('[oc-agent-anthropic] postActionToFleet failed:', err);
|
|
199
250
|
}
|
|
200
251
|
}
|
|
201
252
|
return { result, action, posted };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ScopeNotGrantedError } from '@orangecheck/agent-core';
|
|
4
|
+
|
|
5
|
+
import { stampToolUse } 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('stampToolUse scope pre-flight', () => {
|
|
29
|
+
const base = {
|
|
30
|
+
agent: { address: AGENT, sign: sign() },
|
|
31
|
+
toolUse: { type: 'tool_use', id: 't1', name: 'search', input: { q: 'x' } },
|
|
32
|
+
} as never;
|
|
33
|
+
|
|
34
|
+
it('names encrypted scopes rather than blaming the scope string', async () => {
|
|
35
|
+
const err = await stampToolUse({
|
|
36
|
+
...(base as object),
|
|
37
|
+
delegation: { scopes_encrypted: SEALED },
|
|
38
|
+
scopeExercised: 'anthropic: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 stampToolUse({
|
|
48
|
+
...(base as object),
|
|
49
|
+
delegation: {},
|
|
50
|
+
scopeExercised: 'anthropic:tool(name=search)',
|
|
51
|
+
} as never).catch((e: unknown) => e);
|
|
52
|
+
|
|
53
|
+
expect((err as ScopeNotGrantedError).reason).toBe('no_scopes');
|
|
54
|
+
});
|
|
55
|
+
});
|