@orangecheck/agent-anthropic 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/index.d.mts +1 -20
- package/dist/index.d.ts +1 -20
- package/dist/index.js +1 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -56
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.test.ts +0 -109
- package/src/index.ts +2 -122
- 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,3 @@ 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
|
-
- [fleet.ochk.io/integrations/anthropic](https://fleet.ochk.io/integrations/anthropic) — the managed-tier story this slots into.
|
package/dist/index.d.mts
CHANGED
|
@@ -25,24 +25,5 @@ interface InvokeWithStampResult<T> {
|
|
|
25
25
|
action: ActionEnvelope;
|
|
26
26
|
}
|
|
27
27
|
declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
|
|
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>;
|
|
40
|
-
interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
41
|
-
fleet?: FleetClient;
|
|
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
28
|
|
|
48
|
-
export { type AnthropicToolUse, type
|
|
29
|
+
export { type AnthropicToolUse, type InvokeWithStampInput, type InvokeWithStampResult, type StampToolUseInput, canonicalizeToolUse, invokeWithStamp, stampToolUse, toolUseHash, toolUseId };
|
package/dist/index.d.ts
CHANGED
|
@@ -25,24 +25,5 @@ interface InvokeWithStampResult<T> {
|
|
|
25
25
|
action: ActionEnvelope;
|
|
26
26
|
}
|
|
27
27
|
declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
|
|
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>;
|
|
40
|
-
interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
41
|
-
fleet?: FleetClient;
|
|
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
28
|
|
|
48
|
-
export { type AnthropicToolUse, type
|
|
29
|
+
export { type AnthropicToolUse, type InvokeWithStampInput, type InvokeWithStampResult, type StampToolUseInput, canonicalizeToolUse, invokeWithStamp, stampToolUse, toolUseHash, toolUseId };
|
package/dist/index.js
CHANGED
|
@@ -30,13 +30,7 @@ function toolUseId(agentAddress, signedAt, delegationId, scopeExercised, t) {
|
|
|
30
30
|
}
|
|
31
31
|
async function stampToolUse(input) {
|
|
32
32
|
const scopeExercised = input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;
|
|
33
|
-
|
|
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
|
-
}
|
|
33
|
+
agentCore.assertScopeGranted(input.delegation, scopeExercised, "stampToolUse");
|
|
40
34
|
const bytes = canonicalizeToolUse(input.toolUse);
|
|
41
35
|
return agentSigner.signAsAgent({
|
|
42
36
|
agent: input.agent,
|
|
@@ -52,58 +46,9 @@ async function invokeWithStamp(input) {
|
|
|
52
46
|
const result = await input.call(input.toolUse);
|
|
53
47
|
return { result, action };
|
|
54
48
|
}
|
|
55
|
-
async function postActionToFleet(action, client) {
|
|
56
|
-
const baseUrl = client.baseUrl ?? "https://fleet.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(`postActionToFleet 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.fleet) {
|
|
94
|
-
try {
|
|
95
|
-
posted = await postActionToFleet(action, input.fleet);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.error("[oc-agent-anthropic] postActionToFleet failed:", err);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return { result, action, posted };
|
|
101
|
-
}
|
|
102
49
|
|
|
103
50
|
exports.canonicalizeToolUse = canonicalizeToolUse;
|
|
104
51
|
exports.invokeWithStamp = invokeWithStamp;
|
|
105
|
-
exports.invokeWithStampAndPost = invokeWithStampAndPost;
|
|
106
|
-
exports.postActionToFleet = postActionToFleet;
|
|
107
52
|
exports.stampToolUse = stampToolUse;
|
|
108
53
|
exports.toolUseHash = toolUseHash;
|
|
109
54
|
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","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// 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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","assertScopeGranted","signAsAgent"],"mappings":";;;;;;;AA+CO,SAAS,oBAAoB,CAAA,EAAiC;AACjE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,OAAO,CAAA,CAAE,KAAA;AAAA,IACT,MAAM,CAAA,CAAE;AAAA,GACZ;AACA,EAAA,MAAM,GAAA,GAAMA,uBAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,YAAY,CAAA,EAA6B;AACrD,EAAA,OAAO,YAAYC,mBAAA,CAAUC,aAAA,CAAO,mBAAA,CAAoB,CAAC,CAAC,CAAC,CAAA;AAC/D;AAEO,SAAS,SAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,CAAA,EACM;AACN,EAAA,OAAOC,yBAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,YAAY,CAAC,CAAA;AAAA,IAC3B,cAAA,EAAgB,mBAAA,CAAoB,CAAC,CAAA,CAAE,UAAA;AAAA,IACvC,YAAA,EAAc,kDAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAmBA,eAAsB,aAAa,KAAA,EAAmD;AAClF,EAAA,MAAM,iBACF,KAAA,CAAM,cAAA,IAAkB,CAAA,oBAAA,EAAuB,KAAA,CAAM,QAAQ,IAAI,CAAA,CAAA,CAAA;AAErE,EAAAC,4BAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,cAAc,CAAA;AAEnE,EAAA,MAAM,KAAA,GAAQ,mBAAA,CAAoB,KAAA,CAAM,OAAO,CAAA;AAC/C,EAAA,OAAOC,uBAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,WAAA,CAAY,MAAM,OAAO,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IACtE,IAAA,EAAM,kDAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAsBA,eAAsB,gBAClB,KAAA,EACiC;AACjC,EAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,KAAK,CAAA;AACvC,EAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,IAAA,CAAK,MAAM,OAAO,CAAA;AAC7C,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC5B","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"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
|
|
5
5
|
// src/index.ts
|
|
@@ -28,13 +28,7 @@ function toolUseId(agentAddress, signedAt, delegationId, scopeExercised, t) {
|
|
|
28
28
|
}
|
|
29
29
|
async function stampToolUse(input) {
|
|
30
30
|
const scopeExercised = input.scopeExercised ?? `anthropic:tool(name=${input.toolUse.name})`;
|
|
31
|
-
|
|
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
|
-
}
|
|
31
|
+
assertScopeGranted(input.delegation, scopeExercised, "stampToolUse");
|
|
38
32
|
const bytes = canonicalizeToolUse(input.toolUse);
|
|
39
33
|
return signAsAgent({
|
|
40
34
|
agent: input.agent,
|
|
@@ -50,54 +44,7 @@ async function invokeWithStamp(input) {
|
|
|
50
44
|
const result = await input.call(input.toolUse);
|
|
51
45
|
return { result, action };
|
|
52
46
|
}
|
|
53
|
-
async function postActionToFleet(action, client) {
|
|
54
|
-
const baseUrl = client.baseUrl ?? "https://fleet.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(`postActionToFleet 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.fleet) {
|
|
92
|
-
try {
|
|
93
|
-
posted = await postActionToFleet(action, input.fleet);
|
|
94
|
-
} catch (err) {
|
|
95
|
-
console.error("[oc-agent-anthropic] postActionToFleet failed:", err);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return { result, action, posted };
|
|
99
|
-
}
|
|
100
47
|
|
|
101
|
-
export { canonicalizeToolUse, invokeWithStamp,
|
|
48
|
+
export { canonicalizeToolUse, invokeWithStamp, stampToolUse, toolUseHash, toolUseId };
|
|
102
49
|
//# sourceMappingURL=index.mjs.map
|
|
103
50
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["j"],"mappings":";;;;;AAgDO,SAAS,oBAAoB,CAAA,EAAiC;AACjE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,OAAO,CAAA,CAAE,KAAA;AAAA,IACT,MAAM,CAAA,CAAE;AAAA,GACZ;AACA,EAAA,MAAM,GAAA,GAAM,aAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,YAAY,CAAA,EAA6B;AACrD,EAAA,OAAO,YAAY,SAAA,CAAU,MAAA,CAAO,mBAAA,CAAoB,CAAC,CAAC,CAAC,CAAA;AAC/D;AAEO,SAAS,SAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,CAAA,EACM;AACN,EAAA,OAAO,eAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,YAAY,CAAC,CAAA;AAAA,IAC3B,cAAA,EAAgB,mBAAA,CAAoB,CAAC,CAAA,CAAE,UAAA;AAAA,IACvC,YAAA,EAAc,kDAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAmBA,eAAsB,aAAa,KAAA,EAAmD;AAClF,EAAA,MAAM,iBACF,KAAA,CAAM,cAAA,IAAkB,CAAA,oBAAA,EAAuB,KAAA,CAAM,QAAQ,IAAI,CAAA,CAAA,CAAA;AAErE,EAAA,MAAM,WAAW,KAAA,CAAM,UAAA,CAAW,UAAU,EAAC,EAAG,IAAI,UAAU,CAAA;AAC9D,EAAA,MAAM,eAAA,GAAkB,WAAW,cAAc,CAAA;AACjD,EAAA,IAAI,CAAC,QAAQ,IAAA,CAAK,CAAC,MAAM,UAAA,CAAW,eAAA,EAAiB,CAAC,CAAC,CAAA,EAAG;AACtD,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,kCAAkC,cAAc,CAAA,yCAAA;AAAA,KACpD;AAAA,EACJ;AAEA,EAAA,MAAM,KAAA,GAAQ,mBAAA,CAAoB,KAAA,CAAM,OAAO,CAAA;AAC/C,EAAA,OAAO,WAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,WAAA,CAAY,MAAM,OAAO,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IACtE,IAAA,EAAM,kDAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAsBA,eAAsB,gBAClB,KAAA,EACiC;AACjC,EAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,KAAK,CAAA;AACvC,EAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,IAAA,CAAK,MAAM,OAAO,CAAA;AAC7C,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC5B;AAoCA,eAAsB,iBAAA,CAClB,QACA,MAAA,EACyB;AACzB,EAAA,MAAM,OAAA,GAAU,OAAO,OAAA,IAAW,uBAAA;AAClC,EAAA,MAAM,CAAA,GAAI,OAAO,KAAA,IAAS,KAAA;AAC1B,EAAA,MAAM,IAAA,GAAO;AAAA,IACT,YAAY,MAAA,CAAO,SAAA;AAAA,IACnB,eAAe,MAAA,CAAO,aAAA;AAAA,IACtB,aAAA,EAAe,OAAO,MAAA,CAAO,OAAA;AAAA,IAC7B,iBAAiB,MAAA,CAAO,eAAA;AAAA,IACxB,YAAA,EAAc,OAAO,OAAA,CAAQ,IAAA;AAAA,IAC7B,cAAA,EAAgB,OAAO,OAAA,CAAQ,MAAA;AAAA,IAC/B,YAAA,EAAc,OAAO,OAAA,CAAQ,IAAA;AAAA,IAC7B,WAAW,MAAA,CAAO,SAAA;AAAA,IAClB,SAAA,EAAW,OAAO,GAAA,CAAI,KAAA;AAAA,IACtB,IAAI,MAAA,CAAO;AAAA,GACf;AACA,EAAA,MAAM,CAAA,GAAI,MAAM,CAAA,CAAE,CAAA,EAAG,OAAO,CAAA,YAAA,CAAA,EAAgB;AAAA,IACxC,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACL,cAAA,EAAgB,kBAAA;AAAA,MAChB,aAAA,EAAe,CAAA,OAAA,EAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,KAC5C;AAAA,IACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAAA,GAC5B,CAAA;AACD,EAAA,IAAI,CAAC,EAAE,EAAA,EAAI;AACP,IAAA,IAAI,MAAA,GAAS,CAAA,KAAA,EAAQ,CAAA,CAAE,MAAM,CAAA,CAAA;AAC7B,IAAA,IAAI;AACA,MAAA,MAAMA,EAAAA,GAAK,MAAM,CAAA,CAAE,IAAA,EAAK;AACxB,MAAA,IAAIA,EAAAA,CAAE,MAAA,EAAQ,MAAA,GAASA,EAAAA,CAAE,MAAA;AAAA,IAC7B,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,MAAM,CAAA,CAAE,CAAA;AAAA,EACzD;AACA,EAAA,MAAM,CAAA,GAAK,MAAM,CAAA,CAAE,IAAA,EAAK;AACxB,EAAA,OAAO,CAAA,CAAE,MAAA;AACb;AAsBA,eAAsB,uBAClB,KAAA,EACwC;AACxC,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,MAAM,gBAAgB,KAAK,CAAA;AACtD,EAAA,IAAI,MAAA,GAAkC,IAAA;AACtC,EAAA,IAAI,MAAM,KAAA,EAAO;AACb,IAAA,IAAI;AACA,MAAA,MAAA,GAAS,MAAM,iBAAA,CAAkB,MAAA,EAAQ,KAAA,CAAM,KAAK,CAAA;AAAA,IACxD,SAAS,GAAA,EAAK;AAKV,MAAA,OAAA,CAAQ,KAAA,CAAM,kDAAkD,GAAG,CAAA;AAAA,IACvE;AAAA,EACJ;AACA,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,MAAA,EAAO;AACpC","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// 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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;AA+CO,SAAS,oBAAoB,CAAA,EAAiC;AACjE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,OAAO,CAAA,CAAE,KAAA;AAAA,IACT,MAAM,CAAA,CAAE;AAAA,GACZ;AACA,EAAA,MAAM,GAAA,GAAM,aAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,YAAY,CAAA,EAA6B;AACrD,EAAA,OAAO,YAAY,SAAA,CAAU,MAAA,CAAO,mBAAA,CAAoB,CAAC,CAAC,CAAC,CAAA;AAC/D;AAEO,SAAS,SAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,CAAA,EACM;AACN,EAAA,OAAO,eAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,YAAY,CAAC,CAAA;AAAA,IAC3B,cAAA,EAAgB,mBAAA,CAAoB,CAAC,CAAA,CAAE,UAAA;AAAA,IACvC,YAAA,EAAc,kDAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAmBA,eAAsB,aAAa,KAAA,EAAmD;AAClF,EAAA,MAAM,iBACF,KAAA,CAAM,cAAA,IAAkB,CAAA,oBAAA,EAAuB,KAAA,CAAM,QAAQ,IAAI,CAAA,CAAA,CAAA;AAErE,EAAA,kBAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,cAAc,CAAA;AAEnE,EAAA,MAAM,KAAA,GAAQ,mBAAA,CAAoB,KAAA,CAAM,OAAO,CAAA;AAC/C,EAAA,OAAO,WAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,WAAA,CAAY,MAAM,OAAO,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IACtE,IAAA,EAAM,kDAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAsBA,eAAsB,gBAClB,KAAA,EACiC;AACjC,EAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,KAAK,CAAA;AACvC,EAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,IAAA,CAAK,MAAM,OAAO,CAAA;AAC7C,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC5B","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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/agent-anthropic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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,8 +51,8 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@noble/hashes": "^1.5.0",
|
|
54
|
-
"@orangecheck/agent-core": "^
|
|
55
|
-
"@orangecheck/agent-signer": "^0.
|
|
54
|
+
"@orangecheck/agent-core": "^1.2.0",
|
|
55
|
+
"@orangecheck/agent-signer": "^0.3.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^22.10.2",
|
package/src/index.test.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
canonicalizeToolUse,
|
|
5
|
-
postActionToFleet,
|
|
6
5
|
toolUseHash,
|
|
7
6
|
} from './index.js';
|
|
8
7
|
import type { ActionEnvelope } from '@orangecheck/agent-signer';
|
|
@@ -49,111 +48,3 @@ describe('canonicalizeToolUse', () => {
|
|
|
49
48
|
});
|
|
50
49
|
});
|
|
51
50
|
|
|
52
|
-
describe('postActionToFleet', () => {
|
|
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://fleet.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 postActionToFleet(fakeAction, {
|
|
97
|
-
apiToken: 'ock_xxxx',
|
|
98
|
-
projectId: 'proj_test',
|
|
99
|
-
baseUrl: 'https://fleet.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
|
-
postActionToFleet(fakeAction, {
|
|
116
|
-
apiToken: 'ock_xxxx',
|
|
117
|
-
projectId: 'proj_test',
|
|
118
|
-
baseUrl: 'https://fleet.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
|
-
postActionToFleet(fakeAction, {
|
|
130
|
-
apiToken: 'ock_xxxx',
|
|
131
|
-
projectId: 'proj_test',
|
|
132
|
-
baseUrl: 'https://fleet.example',
|
|
133
|
-
fetch: fakeFetch as unknown as typeof fetch,
|
|
134
|
-
})
|
|
135
|
-
).rejects.toThrow(/http_502/);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('defaults to https://fleet.ochk.io when baseUrl is omitted', async () => {
|
|
139
|
-
const fakeFetch = vi.fn(async (url: string | URL) => {
|
|
140
|
-
expect(String(url)).toBe('https://fleet.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 postActionToFleet(fakeAction, {
|
|
154
|
-
apiToken: 'ock_xxxx',
|
|
155
|
-
projectId: 'proj_test',
|
|
156
|
-
fetch: fakeFetch as unknown as typeof fetch,
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
});
|
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({
|
|
@@ -145,116 +138,3 @@ export async function invokeWithStamp<T>(
|
|
|
145
138
|
const result = await input.call(input.toolUse);
|
|
146
139
|
return { result, action };
|
|
147
140
|
}
|
|
148
|
-
|
|
149
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
-
// Fleet integration: POST stamped actions to fleet.ochk.io/api/actions
|
|
151
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
export interface FleetClient {
|
|
154
|
-
/** Defaults to https://fleet.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 fleet.ochk.io's /api/actions
|
|
172
|
-
* registry. The fleet:
|
|
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 postActionToFleet(
|
|
184
|
-
action: ActionEnvelope,
|
|
185
|
-
client: FleetClient
|
|
186
|
-
): Promise<PostActionResult> {
|
|
187
|
-
const baseUrl = client.baseUrl ?? 'https://fleet.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(`postActionToFleet 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
|
-
/** Fleet credentials. If absent, behaves like invokeWithStamp (no posting). */
|
|
225
|
-
fleet?: FleetClient;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
229
|
-
/** Set when fleet 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 fleet. Three roundtrips → one call. If `fleet`
|
|
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 + postActionToFleet
|
|
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.fleet) {
|
|
249
|
-
try {
|
|
250
|
-
posted = await postActionToFleet(action, input.fleet);
|
|
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] postActionToFleet failed:', err);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return { result, action, posted };
|
|
260
|
-
}
|
|
@@ -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
|
+
});
|