@orangecheck/agent-vercel 1.1.2 → 1.1.4
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 +2 -2
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/default-scope.test.ts +52 -0
- package/src/index.ts +8 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The Vercel AI SDK has converged on a clean `tool({description, parameters, execu
|
|
|
12
12
|
|
|
13
13
|
This module wraps `execute` so every successful call produces a signed `agent-action` envelope citing the active delegation:
|
|
14
14
|
|
|
15
|
-
- Pre-call: scope check (`vercel:tool(verb
|
|
15
|
+
- Pre-call: scope check (`vercel:tool(verb="<verb>")` against the delegation; refuses if not admissible).
|
|
16
16
|
- Canonicalize the `(verb, args, callId)` tuple, BIP-322 sign as the agent address, emit envelope.
|
|
17
17
|
- Run the real handler.
|
|
18
18
|
- Return both the result (back to the model via the SDK) and the envelope (for your audit pipeline).
|
|
@@ -77,7 +77,7 @@ Stamp's `content.hash` is a SHA-256 of:
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Default `scope_exercised` is `vercel:tool(verb
|
|
80
|
+
Default `scope_exercised` is `vercel:tool(verb="<verb>")`, the name quoted so it is read as written. Override via `scopeExercised`.
|
|
81
81
|
|
|
82
82
|
## API
|
|
83
83
|
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,12 @@ function toolCallActionId(agentAddress, signedAt, delegationId, scopeExercised,
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
async function stampToolCall(input) {
|
|
32
|
-
const scopeExercised = input.scopeExercised ??
|
|
32
|
+
const scopeExercised = input.scopeExercised ?? agentCore.canonicalizeScope({
|
|
33
|
+
product: "vercel",
|
|
34
|
+
verb: "tool",
|
|
35
|
+
// A quoted value, read as written: a tool called "*" is that tool.
|
|
36
|
+
constraints: [{ key: "verb", op: "=", value: input.call.verb, quoted: true }]
|
|
37
|
+
});
|
|
33
38
|
agentCore.assertScopeGranted(input.delegation, scopeExercised, "stampToolCall");
|
|
34
39
|
const bytes = canonicalizeToolCall(input.call);
|
|
35
40
|
return agentSigner.signAsAgent({
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","assertScopeGranted","signAsAgent"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","canonicalizeScope","assertScopeGranted","signAsAgent"],"mappings":";;;;;;;AA8DO,SAAS,qBAAqB,CAAA,EAA+B;AAChE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,SAAS,CAAA,CAAE,MAAA;AAAA,IACX,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,aAAa,CAAA,EAA2B;AACpD,EAAA,OAAO,YAAYC,mBAAA,CAAUC,aAAA,CAAO,oBAAA,CAAqB,CAAC,CAAC,CAAC,CAAA;AAChE;AAEO,SAAS,gBAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,CAAA,EACM;AACN,EAAA,OAAOC,yBAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,aAAa,CAAC,CAAA;AAAA,IAC5B,cAAA,EAAgB,oBAAA,CAAqB,CAAC,CAAA,CAAE,UAAA;AAAA,IACxC,YAAA,EAAc,gDAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAkBA,eAAsB,cAAc,KAAA,EAAoD;AACpF,EAAA,MAAM,cAAA,GACF,KAAA,CAAM,cAAA,IAAkBC,2BAAA,CAAkB;AAAA,IACtC,OAAA,EAAS,QAAA;AAAA,IACT,IAAA,EAAM,MAAA;AAAA;AAAA,IAEN,WAAA,EAAa,CAAC,EAAE,GAAA,EAAK,MAAA,EAAQ,EAAA,EAAI,GAAA,EAAK,KAAA,EAAO,KAAA,CAAM,IAAA,CAAK,IAAA,EAAM,MAAA,EAAQ,MAAM;AAAA,GAC/E,CAAA;AAEL,EAAAC,4BAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,eAAe,CAAA;AAEpE,EAAA,MAAM,KAAA,GAAQ,oBAAA,CAAqB,KAAA,CAAM,IAAI,CAAA;AAC7C,EAAA,OAAOC,uBAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,YAAA,CAAa,MAAM,IAAI,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IACpE,IAAA,EAAM,gDAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAgEO,SAAS,OACZ,KAAA,EAC6B;AAC7B,EAAA,OAAO;AAAA,IACH,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,aAAa,KAAA,CAAM,WAAA;AAAA,IACnB,OAAA,EAAS,OAAO,IAAA,EAAM,GAAA,KAAQ;AAC1B,MAAA,MAAM,IAAA,GAAuB;AAAA,QACzB,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ;AAAA,OACJ;AACA,MAAA,MAAM,MAAA,GAAS,MAAM,aAAA,CAAc;AAAA,QAC/B,OAAO,GAAA,CAAI,KAAA;AAAA,QACX,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB;AAAA,OACH,CAAA;AACD,MAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA;AACvC,MAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAAA,IAC5B;AAAA,GACJ;AACJ","file":"index.js","sourcesContent":["// @orangecheck/agent-vercel — wrap Vercel AI SDK `tool()` invocations in\n// OC Agent action envelopes.\n//\n// Provider-agnostic: the AI SDK is the abstraction layer. Whatever model\n// you use underneath (Anthropic, OpenAI, Cohere, …) the tool primitive is\n// the same {description, parameters, execute} shape. This adapter wraps\n// `execute` so every successful call produces a signed agent-action\n// envelope citing the active delegation.\n//\n// Two integration shapes:\n//\n// 1. ocTool({verb, parameters, execute})\n// A drop-in replacement for `tool()`. The wrapped execute runs the\n// scope check, signs the canonical (verb, args, callId) tuple,\n// then runs your real handler.\n//\n// 2. stampToolCall(input)\n// Lower-level stamping helper for cases where you have your own\n// tool-execution loop and want to emit envelopes manually.\n//\n// The agent + delegation are passed via a per-request context object\n// (or threaded through closures) — we don't depend on AsyncLocalStorage\n// to keep edge-runtime compatibility maximal.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n assertScopeGranted,\n canonicalize,\n canonicalizeScope,\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 a tool call\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface VercelToolCall {\n /** Unique id assigned by the AI SDK for this tool invocation. */\n callId: string;\n /** Tool verb / name — passed in via ocTool() or set per-call by your wrapper. */\n verb: string;\n /** JSON-serializable arguments produced by the model. */\n args: Record<string, unknown>;\n}\n\n/**\n * Produce the canonical bytes for a Vercel AI SDK tool call.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"args\": <canonical JSON of args>,\n * \"call_id\": <callId>,\n * \"verb\": <verb>\n * }\n */\nexport function canonicalizeToolCall(c: VercelToolCall): Uint8Array {\n const canon = {\n args: c.args,\n call_id: c.callId,\n verb: c.verb,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function toolCallHash(c: VercelToolCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeToolCall(c)));\n}\n\nexport function toolCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: VercelToolCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: toolCallHash(c),\n content_length: canonicalizeToolCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.vercel-tool-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping primitive\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AgentContext {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n}\n\nexport interface StampToolCallInput extends AgentContext {\n call: VercelToolCall;\n /** Defaults to `vercel:tool(verb=\"<verb>\")` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ?? canonicalizeScope({\n product: 'vercel',\n verb: 'tool',\n // A quoted value, read as written: a tool called \"*\" is that tool.\n constraints: [{ key: 'verb', op: '=', value: input.call.verb, quoted: true }],\n });\n\n assertScopeGranted(input.delegation, scopeExercised, 'stampToolCall');\n\n const bytes = canonicalizeToolCall(input.call);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolCallHash(input.call), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.vercel-tool-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ocTool — drop-in tool() replacement\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OcToolInput<TArgs extends Record<string, unknown>, TResult> {\n /** OC Agent verb under which this tool is exercised. */\n verb: string;\n /** Pass-through to AI SDK's `tool()` — the JSON Schema for inputs. */\n parameters?: unknown;\n /** Pass-through description shown to the model. */\n description?: string;\n /** Your real handler. Returns whatever the tool returns. */\n execute: (args: TArgs) => Promise<TResult>;\n}\n\nexport interface OcToolWrapped<TArgs extends Record<string, unknown>, TResult> {\n verb: string;\n parameters?: unknown;\n description?: string;\n /**\n * The execute fn the AI SDK calls. Receives args + a callId (the AI\n * SDK passes its own callId through — adapter glue passes it via\n * context).\n */\n execute: (\n args: TArgs,\n ctx: AgentContext & { callId: string }\n ) => Promise<{\n result: TResult;\n action: ActionEnvelope;\n }>;\n}\n\n/**\n * Wrap a tool() definition so its execute path is scope-checked and\n * envelope-emitting. The exact glue depends on which AI-SDK release you're\n * on — the typical pattern is:\n *\n * const tools = {\n * 'invoice.create': tool({\n * description: '…',\n * parameters: schema,\n * execute: async (args, { toolCallId }) => {\n * const { result, action } = await invoiceCreate.execute(args, {\n * agent, delegation, callId: toolCallId,\n * });\n * await yourAuditPipeline.append(action);\n * return result;\n * },\n * }),\n * };\n *\n * const invoiceCreate = ocTool({\n * verb: 'invoice.create',\n * parameters: schema,\n * execute: async (args) => myInvoiceCreateImpl(args),\n * });\n *\n * The `ocTool` value is provider/SDK-agnostic — bring your own glue around\n * it. v1 of the adapter intentionally does not import `ai` so it works on\n * any release.\n */\nexport function ocTool<TArgs extends Record<string, unknown>, TResult>(\n input: OcToolInput<TArgs, TResult>\n): OcToolWrapped<TArgs, TResult> {\n return {\n verb: input.verb,\n parameters: input.parameters,\n description: input.description,\n execute: async (args, ctx) => {\n const call: VercelToolCall = {\n callId: ctx.callId,\n verb: input.verb,\n args,\n };\n const action = await stampToolCall({\n agent: ctx.agent,\n delegation: ctx.delegation,\n call,\n });\n const result = await input.execute(args);\n return { result, action };\n },\n };\n}\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
-
import { canonicalize, hexEncode, computeActionId, assertScopeGranted } from '@orangecheck/agent-core';
|
|
2
|
+
import { canonicalize, hexEncode, computeActionId, canonicalizeScope, assertScopeGranted } from '@orangecheck/agent-core';
|
|
3
3
|
import { signAsAgent } from '@orangecheck/agent-signer';
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
@@ -27,7 +27,12 @@ function toolCallActionId(agentAddress, signedAt, delegationId, scopeExercised,
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
async function stampToolCall(input) {
|
|
30
|
-
const scopeExercised = input.scopeExercised ??
|
|
30
|
+
const scopeExercised = input.scopeExercised ?? canonicalizeScope({
|
|
31
|
+
product: "vercel",
|
|
32
|
+
verb: "tool",
|
|
33
|
+
// A quoted value, read as written: a tool called "*" is that tool.
|
|
34
|
+
constraints: [{ key: "verb", op: "=", value: input.call.verb, quoted: true }]
|
|
35
|
+
});
|
|
31
36
|
assertScopeGranted(input.delegation, scopeExercised, "stampToolCall");
|
|
32
37
|
const bytes = canonicalizeToolCall(input.call);
|
|
33
38
|
return signAsAgent({
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;AA8DO,SAAS,qBAAqB,CAAA,EAA+B;AAChE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,SAAS,CAAA,CAAE,MAAA;AAAA,IACX,MAAM,CAAA,CAAE;AAAA,GACZ;AACA,EAAA,MAAM,GAAA,GAAM,aAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,aAAa,CAAA,EAA2B;AACpD,EAAA,OAAO,YAAY,SAAA,CAAU,MAAA,CAAO,oBAAA,CAAqB,CAAC,CAAC,CAAC,CAAA;AAChE;AAEO,SAAS,gBAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,CAAA,EACM;AACN,EAAA,OAAO,eAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,aAAa,CAAC,CAAA;AAAA,IAC5B,cAAA,EAAgB,oBAAA,CAAqB,CAAC,CAAA,CAAE,UAAA;AAAA,IACxC,YAAA,EAAc,gDAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAkBA,eAAsB,cAAc,KAAA,EAAoD;AACpF,EAAA,MAAM,cAAA,GACF,KAAA,CAAM,cAAA,IAAkB,iBAAA,CAAkB;AAAA,IACtC,OAAA,EAAS,QAAA;AAAA,IACT,IAAA,EAAM,MAAA;AAAA;AAAA,IAEN,WAAA,EAAa,CAAC,EAAE,GAAA,EAAK,MAAA,EAAQ,EAAA,EAAI,GAAA,EAAK,KAAA,EAAO,KAAA,CAAM,IAAA,CAAK,IAAA,EAAM,MAAA,EAAQ,MAAM;AAAA,GAC/E,CAAA;AAEL,EAAA,kBAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,eAAe,CAAA;AAEpE,EAAA,MAAM,KAAA,GAAQ,oBAAA,CAAqB,KAAA,CAAM,IAAI,CAAA;AAC7C,EAAA,OAAO,WAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,YAAA,CAAa,MAAM,IAAI,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IACpE,IAAA,EAAM,gDAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAgEO,SAAS,OACZ,KAAA,EAC6B;AAC7B,EAAA,OAAO;AAAA,IACH,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,aAAa,KAAA,CAAM,WAAA;AAAA,IACnB,OAAA,EAAS,OAAO,IAAA,EAAM,GAAA,KAAQ;AAC1B,MAAA,MAAM,IAAA,GAAuB;AAAA,QACzB,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ;AAAA,OACJ;AACA,MAAA,MAAM,MAAA,GAAS,MAAM,aAAA,CAAc;AAAA,QAC/B,OAAO,GAAA,CAAI,KAAA;AAAA,QACX,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB;AAAA,OACH,CAAA;AACD,MAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA;AACvC,MAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAAA,IAC5B;AAAA,GACJ;AACJ","file":"index.mjs","sourcesContent":["// @orangecheck/agent-vercel — wrap Vercel AI SDK `tool()` invocations in\n// OC Agent action envelopes.\n//\n// Provider-agnostic: the AI SDK is the abstraction layer. Whatever model\n// you use underneath (Anthropic, OpenAI, Cohere, …) the tool primitive is\n// the same {description, parameters, execute} shape. This adapter wraps\n// `execute` so every successful call produces a signed agent-action\n// envelope citing the active delegation.\n//\n// Two integration shapes:\n//\n// 1. ocTool({verb, parameters, execute})\n// A drop-in replacement for `tool()`. The wrapped execute runs the\n// scope check, signs the canonical (verb, args, callId) tuple,\n// then runs your real handler.\n//\n// 2. stampToolCall(input)\n// Lower-level stamping helper for cases where you have your own\n// tool-execution loop and want to emit envelopes manually.\n//\n// The agent + delegation are passed via a per-request context object\n// (or threaded through closures) — we don't depend on AsyncLocalStorage\n// to keep edge-runtime compatibility maximal.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n assertScopeGranted,\n canonicalize,\n canonicalizeScope,\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 a tool call\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface VercelToolCall {\n /** Unique id assigned by the AI SDK for this tool invocation. */\n callId: string;\n /** Tool verb / name — passed in via ocTool() or set per-call by your wrapper. */\n verb: string;\n /** JSON-serializable arguments produced by the model. */\n args: Record<string, unknown>;\n}\n\n/**\n * Produce the canonical bytes for a Vercel AI SDK tool call.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"args\": <canonical JSON of args>,\n * \"call_id\": <callId>,\n * \"verb\": <verb>\n * }\n */\nexport function canonicalizeToolCall(c: VercelToolCall): Uint8Array {\n const canon = {\n args: c.args,\n call_id: c.callId,\n verb: c.verb,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function toolCallHash(c: VercelToolCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeToolCall(c)));\n}\n\nexport function toolCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: VercelToolCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: toolCallHash(c),\n content_length: canonicalizeToolCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.vercel-tool-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping primitive\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface AgentContext {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n}\n\nexport interface StampToolCallInput extends AgentContext {\n call: VercelToolCall;\n /** Defaults to `vercel:tool(verb=\"<verb>\")` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ?? canonicalizeScope({\n product: 'vercel',\n verb: 'tool',\n // A quoted value, read as written: a tool called \"*\" is that tool.\n constraints: [{ key: 'verb', op: '=', value: input.call.verb, quoted: true }],\n });\n\n assertScopeGranted(input.delegation, scopeExercised, 'stampToolCall');\n\n const bytes = canonicalizeToolCall(input.call);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolCallHash(input.call), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.vercel-tool-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ocTool — drop-in tool() replacement\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OcToolInput<TArgs extends Record<string, unknown>, TResult> {\n /** OC Agent verb under which this tool is exercised. */\n verb: string;\n /** Pass-through to AI SDK's `tool()` — the JSON Schema for inputs. */\n parameters?: unknown;\n /** Pass-through description shown to the model. */\n description?: string;\n /** Your real handler. Returns whatever the tool returns. */\n execute: (args: TArgs) => Promise<TResult>;\n}\n\nexport interface OcToolWrapped<TArgs extends Record<string, unknown>, TResult> {\n verb: string;\n parameters?: unknown;\n description?: string;\n /**\n * The execute fn the AI SDK calls. Receives args + a callId (the AI\n * SDK passes its own callId through — adapter glue passes it via\n * context).\n */\n execute: (\n args: TArgs,\n ctx: AgentContext & { callId: string }\n ) => Promise<{\n result: TResult;\n action: ActionEnvelope;\n }>;\n}\n\n/**\n * Wrap a tool() definition so its execute path is scope-checked and\n * envelope-emitting. The exact glue depends on which AI-SDK release you're\n * on — the typical pattern is:\n *\n * const tools = {\n * 'invoice.create': tool({\n * description: '…',\n * parameters: schema,\n * execute: async (args, { toolCallId }) => {\n * const { result, action } = await invoiceCreate.execute(args, {\n * agent, delegation, callId: toolCallId,\n * });\n * await yourAuditPipeline.append(action);\n * return result;\n * },\n * }),\n * };\n *\n * const invoiceCreate = ocTool({\n * verb: 'invoice.create',\n * parameters: schema,\n * execute: async (args) => myInvoiceCreateImpl(args),\n * });\n *\n * The `ocTool` value is provider/SDK-agnostic — bring your own glue around\n * it. v1 of the adapter intentionally does not import `ai` so it works on\n * any release.\n */\nexport function ocTool<TArgs extends Record<string, unknown>, TResult>(\n input: OcToolInput<TArgs, TResult>\n): OcToolWrapped<TArgs, TResult> {\n return {\n verb: input.verb,\n parameters: input.parameters,\n description: input.description,\n execute: async (args, ctx) => {\n const call: VercelToolCall = {\n callId: ctx.callId,\n verb: input.verb,\n args,\n };\n const action = await stampToolCall({\n agent: ctx.agent,\n delegation: ctx.delegation,\n call,\n });\n const result = await input.execute(args);\n return { result, action };\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/agent-vercel",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Wrap Vercel AI SDK tool() invocations with OC Agent scope enforcement and emit a signed agent-action envelope per tool execution. Provider-agnostic — works under Anthropic, OpenAI, Cohere, etc.",
|
|
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": "^2.
|
|
55
|
-
"@orangecheck/agent-signer": "^0.5.
|
|
54
|
+
"@orangecheck/agent-core": "^2.10.0",
|
|
55
|
+
"@orangecheck/agent-signer": "^0.5.2"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^22.10.2",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The default exercised scope names the tool as a quoted value, so the name is
|
|
2
|
+
// read as written: a tool called "*" is that tool, not every tool.
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { parseScope, verifyAction } from '@orangecheck/agent-core';
|
|
6
|
+
import { createDelegation } from '@orangecheck/agent-signer';
|
|
7
|
+
|
|
8
|
+
import { stampToolCall } from './index.js';
|
|
9
|
+
|
|
10
|
+
const PRINCIPAL = 'bc1qprincipal000000000000000000000000000000';
|
|
11
|
+
const AGENT = 'bc1qagent0000000000000000000000000000000000';
|
|
12
|
+
const fakeSign = (address: string) => async (msg: string) =>
|
|
13
|
+
Buffer.from(`sig:${address}:${msg}`, 'utf8').toString('base64');
|
|
14
|
+
const fakeVerify = async (msg: string, sig: string, address: string) =>
|
|
15
|
+
sig === Buffer.from(`sig:${address}:${msg}`, 'utf8').toString('base64');
|
|
16
|
+
|
|
17
|
+
const stamp = async (scope: string, name: string) => {
|
|
18
|
+
const delegation = await createDelegation({
|
|
19
|
+
principal: { address: PRINCIPAL, signMessage: fakeSign(PRINCIPAL) },
|
|
20
|
+
agentAddress: AGENT,
|
|
21
|
+
scopes: [scope],
|
|
22
|
+
ttlMs: 60 * 60 * 1000,
|
|
23
|
+
});
|
|
24
|
+
const action = await stampToolCall({
|
|
25
|
+
agent: { address: AGENT, signMessage: fakeSign(AGENT) },
|
|
26
|
+
delegation,
|
|
27
|
+
call: { callId: 'c1', verb: name, args: {} },
|
|
28
|
+
});
|
|
29
|
+
const r = await verifyAction({ action, delegation, verifyBip322: fakeVerify, skipRevocationCheck: true });
|
|
30
|
+
return { action, ok: r.ok };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('the default exercised scope', () => {
|
|
34
|
+
it('quotes the name', async () => {
|
|
35
|
+
const { action, ok } = await stamp('vercel:tool(verb=search)', 'search');
|
|
36
|
+
expect(action.scope_exercised).toBe('vercel:tool(verb="search")');
|
|
37
|
+
expect(ok).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('stamps a tool called "*" as that tool, not every tool', async () => {
|
|
41
|
+
const { action, ok } = await stamp('vercel:tool(verb="*")', '*');
|
|
42
|
+
expect(action.scope_exercised).toBe('vercel:tool(verb="*")');
|
|
43
|
+
expect(ok).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('keeps scope punctuation in a name as part of the name', async () => {
|
|
47
|
+
const { action, ok } = await stamp('vercel:tool(verb="a,b")', 'a,b');
|
|
48
|
+
const c = parseScope(action.scope_exercised).constraints[0];
|
|
49
|
+
expect(c).toEqual({ key: 'verb', op: '=', value: 'a,b', quoted: true });
|
|
50
|
+
expect(ok).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { sha256 } from '@noble/hashes/sha256';
|
|
|
26
26
|
import {
|
|
27
27
|
assertScopeGranted,
|
|
28
28
|
canonicalize,
|
|
29
|
+
canonicalizeScope,
|
|
29
30
|
computeActionId,
|
|
30
31
|
hexEncode,
|
|
31
32
|
type DelegationEnvelope,
|
|
@@ -102,14 +103,19 @@ export interface AgentContext {
|
|
|
102
103
|
|
|
103
104
|
export interface StampToolCallInput extends AgentContext {
|
|
104
105
|
call: VercelToolCall;
|
|
105
|
-
/** Defaults to `vercel:tool(verb
|
|
106
|
+
/** Defaults to `vercel:tool(verb="<verb>")` — the tightest admissible sub-scope. */
|
|
106
107
|
scopeExercised?: string;
|
|
107
108
|
signedAt?: Date;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
export async function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope> {
|
|
111
112
|
const scopeExercised =
|
|
112
|
-
input.scopeExercised ??
|
|
113
|
+
input.scopeExercised ?? canonicalizeScope({
|
|
114
|
+
product: 'vercel',
|
|
115
|
+
verb: 'tool',
|
|
116
|
+
// A quoted value, read as written: a tool called "*" is that tool.
|
|
117
|
+
constraints: [{ key: 'verb', op: '=', value: input.call.verb, quoted: true }],
|
|
118
|
+
});
|
|
113
119
|
|
|
114
120
|
assertScopeGranted(input.delegation, scopeExercised, 'stampToolCall');
|
|
115
121
|
|