@orangecheck/agent-vercel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/index.d.mts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +115 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
- package/src/index.test.ts +44 -0
- package/src/index.ts +298 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OrangeCheck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @orangecheck/agent-vercel
|
|
2
|
+
|
|
3
|
+
Wrap [Vercel AI SDK](https://sdk.vercel.ai/) `tool()` invocations with [OC Agent](https://github.com/orangecheck/oc-agent-protocol) scope enforcement and emit a signed `agent-action` envelope per tool execution. Provider-agnostic — works under Anthropic, OpenAI, Cohere, or any other model the AI SDK speaks.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The Vercel AI SDK has converged on a clean `tool({description, parameters, execute})` primitive that serializes naturally across providers. A single adapter at this layer covers a large fraction of teams shipping production agents.
|
|
8
|
+
|
|
9
|
+
This module wraps `execute` so every successful call produces a signed `agent-action` envelope citing the active delegation:
|
|
10
|
+
|
|
11
|
+
- Pre-call: scope check (`vercel:tool(verb=<verb>)` against the delegation; refuses if not admissible).
|
|
12
|
+
- Canonicalize the `(verb, args, callId)` tuple, BIP-322 sign as the agent address, emit envelope.
|
|
13
|
+
- Run the real handler.
|
|
14
|
+
- Return both the result (back to the model via the SDK) and the envelope (for your audit pipeline).
|
|
15
|
+
|
|
16
|
+
## Status
|
|
17
|
+
|
|
18
|
+
**v0.0.1 · in design.** API shape is stable; the canonicalization layout (`{args, call_id, verb}` lexicographic, RFC 8785 canonical JSON, trailing LF) is locked. Production wiring with specific AI-SDK releases is intentionally not baked in — `ocTool()` is provider/SDK-agnostic so you can call it from any release of `ai` you're already on.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm i @orangecheck/agent-vercel
|
|
24
|
+
# peer deps:
|
|
25
|
+
npm i @orangecheck/agent-core @orangecheck/agent-signer
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { tool } from 'ai';
|
|
32
|
+
import { ocTool } from '@orangecheck/agent-vercel';
|
|
33
|
+
|
|
34
|
+
const invoiceCreate = ocTool({
|
|
35
|
+
verb: 'invoice.create',
|
|
36
|
+
description: 'create a new invoice',
|
|
37
|
+
parameters: invoiceSchema,
|
|
38
|
+
execute: async (args) => myInvoiceCreateImpl(args),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const tools = {
|
|
42
|
+
'invoice.create': tool({
|
|
43
|
+
description: invoiceCreate.description,
|
|
44
|
+
parameters: invoiceCreate.parameters as never,
|
|
45
|
+
execute: async (args, { toolCallId }) => {
|
|
46
|
+
const { result, action } = await invoiceCreate.execute(args, {
|
|
47
|
+
agent, delegation, callId: toolCallId,
|
|
48
|
+
});
|
|
49
|
+
await yourAuditPipeline.append(action);
|
|
50
|
+
return result;
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const { text } = await generateText({
|
|
56
|
+
model: anthropic('claude-sonnet-4-6'),
|
|
57
|
+
tools,
|
|
58
|
+
prompt: 'invoice acme for $14.20',
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The pattern: `ocTool` defines the OC-Agent-shaped wrapper; the AI SDK's `tool()` calls into that wrapper plus your audit pipeline.
|
|
63
|
+
|
|
64
|
+
## What gets stamped
|
|
65
|
+
|
|
66
|
+
Stamp's `content.hash` is a SHA-256 of:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"args": <canonical JSON of args>,
|
|
71
|
+
"call_id": "<sdk-assigned tool call id>",
|
|
72
|
+
"verb": "<verb>"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Default `scope_exercised` is `vercel:tool(verb=<verb>)`. Override via `scopeExercised`.
|
|
77
|
+
|
|
78
|
+
## API
|
|
79
|
+
|
|
80
|
+
- `canonicalizeToolCall(call)` — RFC 8785 canonical bytes.
|
|
81
|
+
- `toolCallHash(call)` — `sha256:<64-hex>`.
|
|
82
|
+
- `stampToolCall(input)` — sign without executing.
|
|
83
|
+
- `ocTool({verb, parameters, description, execute})` — provider-agnostic wrapped tool.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT.
|
|
88
|
+
|
|
89
|
+
## Related
|
|
90
|
+
|
|
91
|
+
- [oc-agent-protocol](https://github.com/orangecheck/oc-agent-protocol) — normative wire format.
|
|
92
|
+
- [@orangecheck/agent-core](https://www.npmjs.com/package/@orangecheck/agent-core) — verifier library.
|
|
93
|
+
- [@orangecheck/agent-mcp](https://www.npmjs.com/package/@orangecheck/agent-mcp) / [-anthropic](https://www.npmjs.com/package/@orangecheck/agent-anthropic) / [-openai](https://www.npmjs.com/package/@orangecheck/agent-openai) — sibling adapters.
|
|
94
|
+
- [console.ochk.io/integrations/vercel-ai](https://console.ochk.io/integrations/vercel-ai) — managed-tier story.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DelegationEnvelope } from '@orangecheck/agent-core';
|
|
2
|
+
import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
|
|
3
|
+
|
|
4
|
+
interface VercelToolCall {
|
|
5
|
+
callId: string;
|
|
6
|
+
verb: string;
|
|
7
|
+
args: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
declare function canonicalizeToolCall(c: VercelToolCall): Uint8Array;
|
|
10
|
+
declare function toolCallHash(c: VercelToolCall): string;
|
|
11
|
+
declare function toolCallActionId(agentAddress: string, signedAt: string, delegationId: string, scopeExercised: string, c: VercelToolCall): string;
|
|
12
|
+
interface AgentContext {
|
|
13
|
+
agent: SignerRef;
|
|
14
|
+
delegation: DelegationEnvelope;
|
|
15
|
+
}
|
|
16
|
+
interface StampToolCallInput extends AgentContext {
|
|
17
|
+
call: VercelToolCall;
|
|
18
|
+
scopeExercised?: string;
|
|
19
|
+
signedAt?: Date;
|
|
20
|
+
}
|
|
21
|
+
declare function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope>;
|
|
22
|
+
interface OcToolInput<TArgs extends Record<string, unknown>, TResult> {
|
|
23
|
+
verb: string;
|
|
24
|
+
parameters?: unknown;
|
|
25
|
+
description?: string;
|
|
26
|
+
execute: (args: TArgs) => Promise<TResult>;
|
|
27
|
+
}
|
|
28
|
+
interface OcToolWrapped<TArgs extends Record<string, unknown>, TResult> {
|
|
29
|
+
verb: string;
|
|
30
|
+
parameters?: unknown;
|
|
31
|
+
description?: string;
|
|
32
|
+
execute: (args: TArgs, ctx: AgentContext & {
|
|
33
|
+
callId: string;
|
|
34
|
+
console?: ConsoleClient;
|
|
35
|
+
}) => Promise<{
|
|
36
|
+
result: TResult;
|
|
37
|
+
action: ActionEnvelope;
|
|
38
|
+
posted: PostActionResult | null;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
declare function ocTool<TArgs extends Record<string, unknown>, TResult>(input: OcToolInput<TArgs, TResult>): OcToolWrapped<TArgs, TResult>;
|
|
42
|
+
interface ConsoleClient {
|
|
43
|
+
baseUrl?: string;
|
|
44
|
+
apiToken: string;
|
|
45
|
+
projectId: string;
|
|
46
|
+
fetch?: typeof fetch;
|
|
47
|
+
}
|
|
48
|
+
interface PostActionResult {
|
|
49
|
+
id: string;
|
|
50
|
+
project_id: string;
|
|
51
|
+
delegation_id: string;
|
|
52
|
+
}
|
|
53
|
+
declare function postActionToConsole(action: ActionEnvelope, client: ConsoleClient): Promise<PostActionResult>;
|
|
54
|
+
|
|
55
|
+
export { type AgentContext, type ConsoleClient, type OcToolInput, type OcToolWrapped, type PostActionResult, type StampToolCallInput, type VercelToolCall, canonicalizeToolCall, ocTool, postActionToConsole, stampToolCall, toolCallActionId, toolCallHash };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DelegationEnvelope } from '@orangecheck/agent-core';
|
|
2
|
+
import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
|
|
3
|
+
|
|
4
|
+
interface VercelToolCall {
|
|
5
|
+
callId: string;
|
|
6
|
+
verb: string;
|
|
7
|
+
args: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
declare function canonicalizeToolCall(c: VercelToolCall): Uint8Array;
|
|
10
|
+
declare function toolCallHash(c: VercelToolCall): string;
|
|
11
|
+
declare function toolCallActionId(agentAddress: string, signedAt: string, delegationId: string, scopeExercised: string, c: VercelToolCall): string;
|
|
12
|
+
interface AgentContext {
|
|
13
|
+
agent: SignerRef;
|
|
14
|
+
delegation: DelegationEnvelope;
|
|
15
|
+
}
|
|
16
|
+
interface StampToolCallInput extends AgentContext {
|
|
17
|
+
call: VercelToolCall;
|
|
18
|
+
scopeExercised?: string;
|
|
19
|
+
signedAt?: Date;
|
|
20
|
+
}
|
|
21
|
+
declare function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope>;
|
|
22
|
+
interface OcToolInput<TArgs extends Record<string, unknown>, TResult> {
|
|
23
|
+
verb: string;
|
|
24
|
+
parameters?: unknown;
|
|
25
|
+
description?: string;
|
|
26
|
+
execute: (args: TArgs) => Promise<TResult>;
|
|
27
|
+
}
|
|
28
|
+
interface OcToolWrapped<TArgs extends Record<string, unknown>, TResult> {
|
|
29
|
+
verb: string;
|
|
30
|
+
parameters?: unknown;
|
|
31
|
+
description?: string;
|
|
32
|
+
execute: (args: TArgs, ctx: AgentContext & {
|
|
33
|
+
callId: string;
|
|
34
|
+
console?: ConsoleClient;
|
|
35
|
+
}) => Promise<{
|
|
36
|
+
result: TResult;
|
|
37
|
+
action: ActionEnvelope;
|
|
38
|
+
posted: PostActionResult | null;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
declare function ocTool<TArgs extends Record<string, unknown>, TResult>(input: OcToolInput<TArgs, TResult>): OcToolWrapped<TArgs, TResult>;
|
|
42
|
+
interface ConsoleClient {
|
|
43
|
+
baseUrl?: string;
|
|
44
|
+
apiToken: string;
|
|
45
|
+
projectId: string;
|
|
46
|
+
fetch?: typeof fetch;
|
|
47
|
+
}
|
|
48
|
+
interface PostActionResult {
|
|
49
|
+
id: string;
|
|
50
|
+
project_id: string;
|
|
51
|
+
delegation_id: string;
|
|
52
|
+
}
|
|
53
|
+
declare function postActionToConsole(action: ActionEnvelope, client: ConsoleClient): Promise<PostActionResult>;
|
|
54
|
+
|
|
55
|
+
export { type AgentContext, type ConsoleClient, type OcToolInput, type OcToolWrapped, type PostActionResult, type StampToolCallInput, type VercelToolCall, canonicalizeToolCall, ocTool, postActionToConsole, stampToolCall, toolCallActionId, toolCallHash };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var sha256 = require('@noble/hashes/sha256');
|
|
4
|
+
var agentCore = require('@orangecheck/agent-core');
|
|
5
|
+
var agentSigner = require('@orangecheck/agent-signer');
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
function canonicalizeToolCall(c) {
|
|
9
|
+
const canon = {
|
|
10
|
+
args: c.args,
|
|
11
|
+
call_id: c.callId,
|
|
12
|
+
verb: c.verb
|
|
13
|
+
};
|
|
14
|
+
const str = agentCore.canonicalize(canon);
|
|
15
|
+
return new TextEncoder().encode(str + "\n");
|
|
16
|
+
}
|
|
17
|
+
function toolCallHash(c) {
|
|
18
|
+
return "sha256:" + agentCore.hexEncode(sha256.sha256(canonicalizeToolCall(c)));
|
|
19
|
+
}
|
|
20
|
+
function toolCallActionId(agentAddress, signedAt, delegationId, scopeExercised, c) {
|
|
21
|
+
return agentCore.computeActionId({
|
|
22
|
+
address: agentAddress,
|
|
23
|
+
content_hash: toolCallHash(c),
|
|
24
|
+
content_length: canonicalizeToolCall(c).byteLength,
|
|
25
|
+
content_mime: "application/vnd.oc-agent.vercel-tool-call+json",
|
|
26
|
+
signed_at: signedAt,
|
|
27
|
+
delegation_id: delegationId,
|
|
28
|
+
scope_exercised: scopeExercised
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function stampToolCall(input) {
|
|
32
|
+
const scopeExercised = input.scopeExercised ?? `vercel:tool(verb=${input.call.verb})`;
|
|
33
|
+
const granted = (input.delegation.scopes ?? []).map(agentCore.parseScope);
|
|
34
|
+
const exercisedParsed = agentCore.parseScope(scopeExercised);
|
|
35
|
+
if (!granted.some((g) => agentCore.isSubScope(exercisedParsed, g))) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const bytes = canonicalizeToolCall(input.call);
|
|
41
|
+
return agentSigner.signAsAgent({
|
|
42
|
+
agent: input.agent,
|
|
43
|
+
delegation: input.delegation,
|
|
44
|
+
content: { hash: toolCallHash(input.call), length: bytes.byteLength },
|
|
45
|
+
mime: "application/vnd.oc-agent.vercel-tool-call+json",
|
|
46
|
+
scopeExercised,
|
|
47
|
+
signedAt: input.signedAt
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function ocTool(input) {
|
|
51
|
+
return {
|
|
52
|
+
verb: input.verb,
|
|
53
|
+
parameters: input.parameters,
|
|
54
|
+
description: input.description,
|
|
55
|
+
execute: async (args, ctx) => {
|
|
56
|
+
const call = {
|
|
57
|
+
callId: ctx.callId,
|
|
58
|
+
verb: input.verb,
|
|
59
|
+
args
|
|
60
|
+
};
|
|
61
|
+
const action = await stampToolCall({
|
|
62
|
+
agent: ctx.agent,
|
|
63
|
+
delegation: ctx.delegation,
|
|
64
|
+
call
|
|
65
|
+
});
|
|
66
|
+
const result = await input.execute(args);
|
|
67
|
+
let posted = null;
|
|
68
|
+
if (ctx.console) {
|
|
69
|
+
try {
|
|
70
|
+
posted = await postActionToConsole(action, ctx.console);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("[oc-agent-vercel] postActionToConsole failed:", err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { result, action, posted };
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function postActionToConsole(action, client) {
|
|
80
|
+
const baseUrl = client.baseUrl ?? "https://console.ochk.io";
|
|
81
|
+
const f = client.fetch ?? fetch;
|
|
82
|
+
const body = {
|
|
83
|
+
project_id: client.projectId,
|
|
84
|
+
delegation_id: action.delegation_id,
|
|
85
|
+
agent_address: action.signer.address,
|
|
86
|
+
scope_exercised: action.scope_exercised,
|
|
87
|
+
content_hash: action.content.hash,
|
|
88
|
+
content_length: action.content.length,
|
|
89
|
+
content_mime: action.content.mime,
|
|
90
|
+
signed_at: action.signed_at,
|
|
91
|
+
signature: action.sig.value,
|
|
92
|
+
id: action.id
|
|
93
|
+
};
|
|
94
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
Authorization: `Bearer ${client.apiToken}`
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(body)
|
|
101
|
+
});
|
|
102
|
+
if (!r.ok) {
|
|
103
|
+
let reason = `http_${r.status}`;
|
|
104
|
+
try {
|
|
105
|
+
const j2 = await r.json();
|
|
106
|
+
if (j2.reason) reason = j2.reason;
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`postActionToConsole failed: ${reason}`);
|
|
110
|
+
}
|
|
111
|
+
const j = await r.json();
|
|
112
|
+
return j.action;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
exports.canonicalizeToolCall = canonicalizeToolCall;
|
|
116
|
+
exports.ocTool = ocTool;
|
|
117
|
+
exports.postActionToConsole = postActionToConsole;
|
|
118
|
+
exports.stampToolCall = stampToolCall;
|
|
119
|
+
exports.toolCallActionId = toolCallActionId;
|
|
120
|
+
exports.toolCallHash = toolCallHash;
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
122
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","parseScope","isSubScope","signAsAgent","j"],"mappings":"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","file":"index.js","sourcesContent":["// @orangecheck/agent-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 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 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 ?? `vercel:tool(verb=${input.call.verb})`;\n\n const granted = (input.delegation.scopes ?? []).map(parseScope);\n const exercisedParsed = parseScope(scopeExercised);\n if (!granted.some((g) => isSubScope(exercisedParsed, g))) {\n throw new Error(\n `stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeToolCall(input.call);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolCallHash(input.call), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.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). Optional `console` field on the context tells the\n * wrapper to fire-and-forget POST the stamped action to console.\n * ochk.io/api/actions after the underlying execute() returns.\n */\n execute: (\n args: TArgs,\n ctx: AgentContext & { callId: string; console?: ConsoleClient }\n ) => Promise<{\n result: TResult;\n action: ActionEnvelope;\n posted: PostActionResult | null;\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 let posted: PostActionResult | null = null;\n if (ctx.console) {\n try {\n posted = await postActionToConsole(action, ctx.console);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-vercel] postActionToConsole failed:', err);\n }\n }\n return { result, action, posted };\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Console integration: POST stamped actions to console.ochk.io/api/actions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ConsoleClient {\n /** Defaults to https://console.ochk.io. */\n baseUrl?: string;\n /** Bearer token from /settings § 03 (`ock_<hex>`). */\n apiToken: string;\n /** Project the action belongs to (proj_*). */\n projectId: string;\n /** Optional fetch override for runtimes that need it. */\n fetch?: typeof fetch;\n}\n\nexport interface PostActionResult {\n id: string;\n project_id: string;\n delegation_id: string;\n}\n\n/**\n * POST a stamped action envelope to console.ochk.io/api/actions. The\n * console re-derives the action id, validates agent-must-match-\n * delegation, persists, fans out to Nostr (kind 30084), submits to\n * OC Stamp, and triggers any subscribed webhooks. Throws on non-2xx\n * with the server's reason string.\n */\nexport async function postActionToConsole(\n action: ActionEnvelope,\n client: ConsoleClient\n): Promise<PostActionResult> {\n const baseUrl = client.baseUrl ?? 'https://console.ochk.io';\n const f = client.fetch ?? fetch;\n const body = {\n project_id: client.projectId,\n delegation_id: action.delegation_id,\n agent_address: action.signer.address,\n scope_exercised: action.scope_exercised,\n content_hash: action.content.hash,\n content_length: action.content.length,\n content_mime: action.content.mime,\n signed_at: action.signed_at,\n signature: action.sig.value,\n id: action.id,\n };\n const r = await f(`${baseUrl}/api/actions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${client.apiToken}`,\n },\n body: JSON.stringify(body),\n });\n if (!r.ok) {\n let reason = `http_${r.status}`;\n try {\n const j = (await r.json()) as { reason?: string };\n if (j.reason) reason = j.reason;\n } catch {\n // body wasn't json\n }\n throw new Error(`postActionToConsole failed: ${reason}`);\n }\n const j = (await r.json()) as { ok: true; action: PostActionResult };\n return j.action;\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { canonicalize, hexEncode, computeActionId, parseScope, isSubScope } from '@orangecheck/agent-core';
|
|
3
|
+
import { signAsAgent } from '@orangecheck/agent-signer';
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
function canonicalizeToolCall(c) {
|
|
7
|
+
const canon = {
|
|
8
|
+
args: c.args,
|
|
9
|
+
call_id: c.callId,
|
|
10
|
+
verb: c.verb
|
|
11
|
+
};
|
|
12
|
+
const str = canonicalize(canon);
|
|
13
|
+
return new TextEncoder().encode(str + "\n");
|
|
14
|
+
}
|
|
15
|
+
function toolCallHash(c) {
|
|
16
|
+
return "sha256:" + hexEncode(sha256(canonicalizeToolCall(c)));
|
|
17
|
+
}
|
|
18
|
+
function toolCallActionId(agentAddress, signedAt, delegationId, scopeExercised, c) {
|
|
19
|
+
return computeActionId({
|
|
20
|
+
address: agentAddress,
|
|
21
|
+
content_hash: toolCallHash(c),
|
|
22
|
+
content_length: canonicalizeToolCall(c).byteLength,
|
|
23
|
+
content_mime: "application/vnd.oc-agent.vercel-tool-call+json",
|
|
24
|
+
signed_at: signedAt,
|
|
25
|
+
delegation_id: delegationId,
|
|
26
|
+
scope_exercised: scopeExercised
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function stampToolCall(input) {
|
|
30
|
+
const scopeExercised = input.scopeExercised ?? `vercel:tool(verb=${input.call.verb})`;
|
|
31
|
+
const granted = (input.delegation.scopes ?? []).map(parseScope);
|
|
32
|
+
const exercisedParsed = parseScope(scopeExercised);
|
|
33
|
+
if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const bytes = canonicalizeToolCall(input.call);
|
|
39
|
+
return signAsAgent({
|
|
40
|
+
agent: input.agent,
|
|
41
|
+
delegation: input.delegation,
|
|
42
|
+
content: { hash: toolCallHash(input.call), length: bytes.byteLength },
|
|
43
|
+
mime: "application/vnd.oc-agent.vercel-tool-call+json",
|
|
44
|
+
scopeExercised,
|
|
45
|
+
signedAt: input.signedAt
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function ocTool(input) {
|
|
49
|
+
return {
|
|
50
|
+
verb: input.verb,
|
|
51
|
+
parameters: input.parameters,
|
|
52
|
+
description: input.description,
|
|
53
|
+
execute: async (args, ctx) => {
|
|
54
|
+
const call = {
|
|
55
|
+
callId: ctx.callId,
|
|
56
|
+
verb: input.verb,
|
|
57
|
+
args
|
|
58
|
+
};
|
|
59
|
+
const action = await stampToolCall({
|
|
60
|
+
agent: ctx.agent,
|
|
61
|
+
delegation: ctx.delegation,
|
|
62
|
+
call
|
|
63
|
+
});
|
|
64
|
+
const result = await input.execute(args);
|
|
65
|
+
let posted = null;
|
|
66
|
+
if (ctx.console) {
|
|
67
|
+
try {
|
|
68
|
+
posted = await postActionToConsole(action, ctx.console);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error("[oc-agent-vercel] postActionToConsole failed:", err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { result, action, posted };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function postActionToConsole(action, client) {
|
|
78
|
+
const baseUrl = client.baseUrl ?? "https://console.ochk.io";
|
|
79
|
+
const f = client.fetch ?? fetch;
|
|
80
|
+
const body = {
|
|
81
|
+
project_id: client.projectId,
|
|
82
|
+
delegation_id: action.delegation_id,
|
|
83
|
+
agent_address: action.signer.address,
|
|
84
|
+
scope_exercised: action.scope_exercised,
|
|
85
|
+
content_hash: action.content.hash,
|
|
86
|
+
content_length: action.content.length,
|
|
87
|
+
content_mime: action.content.mime,
|
|
88
|
+
signed_at: action.signed_at,
|
|
89
|
+
signature: action.sig.value,
|
|
90
|
+
id: action.id
|
|
91
|
+
};
|
|
92
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: {
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
Authorization: `Bearer ${client.apiToken}`
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(body)
|
|
99
|
+
});
|
|
100
|
+
if (!r.ok) {
|
|
101
|
+
let reason = `http_${r.status}`;
|
|
102
|
+
try {
|
|
103
|
+
const j2 = await r.json();
|
|
104
|
+
if (j2.reason) reason = j2.reason;
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`postActionToConsole failed: ${reason}`);
|
|
108
|
+
}
|
|
109
|
+
const j = await r.json();
|
|
110
|
+
return j.action;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { canonicalizeToolCall, ocTool, postActionToConsole, stampToolCall, toolCallActionId, toolCallHash };
|
|
114
|
+
//# sourceMappingURL=index.mjs.map
|
|
115
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["j"],"mappings":"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","file":"index.mjs","sourcesContent":["// @orangecheck/agent-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 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 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 ?? `vercel:tool(verb=${input.call.verb})`;\n\n const granted = (input.delegation.scopes ?? []).map(parseScope);\n const exercisedParsed = parseScope(scopeExercised);\n if (!granted.some((g) => isSubScope(exercisedParsed, g))) {\n throw new Error(\n `stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeToolCall(input.call);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: toolCallHash(input.call), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.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). Optional `console` field on the context tells the\n * wrapper to fire-and-forget POST the stamped action to console.\n * ochk.io/api/actions after the underlying execute() returns.\n */\n execute: (\n args: TArgs,\n ctx: AgentContext & { callId: string; console?: ConsoleClient }\n ) => Promise<{\n result: TResult;\n action: ActionEnvelope;\n posted: PostActionResult | null;\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 let posted: PostActionResult | null = null;\n if (ctx.console) {\n try {\n posted = await postActionToConsole(action, ctx.console);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-vercel] postActionToConsole failed:', err);\n }\n }\n return { result, action, posted };\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Console integration: POST stamped actions to console.ochk.io/api/actions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ConsoleClient {\n /** Defaults to https://console.ochk.io. */\n baseUrl?: string;\n /** Bearer token from /settings § 03 (`ock_<hex>`). */\n apiToken: string;\n /** Project the action belongs to (proj_*). */\n projectId: string;\n /** Optional fetch override for runtimes that need it. */\n fetch?: typeof fetch;\n}\n\nexport interface PostActionResult {\n id: string;\n project_id: string;\n delegation_id: string;\n}\n\n/**\n * POST a stamped action envelope to console.ochk.io/api/actions. The\n * console re-derives the action id, validates agent-must-match-\n * delegation, persists, fans out to Nostr (kind 30084), submits to\n * OC Stamp, and triggers any subscribed webhooks. Throws on non-2xx\n * with the server's reason string.\n */\nexport async function postActionToConsole(\n action: ActionEnvelope,\n client: ConsoleClient\n): Promise<PostActionResult> {\n const baseUrl = client.baseUrl ?? 'https://console.ochk.io';\n const f = client.fetch ?? fetch;\n const body = {\n project_id: client.projectId,\n delegation_id: action.delegation_id,\n agent_address: action.signer.address,\n scope_exercised: action.scope_exercised,\n content_hash: action.content.hash,\n content_length: action.content.length,\n content_mime: action.content.mime,\n signed_at: action.signed_at,\n signature: action.sig.value,\n id: action.id,\n };\n const r = await f(`${baseUrl}/api/actions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${client.apiToken}`,\n },\n body: JSON.stringify(body),\n });\n if (!r.ok) {\n let reason = `http_${r.status}`;\n try {\n const j = (await r.json()) as { reason?: string };\n if (j.reason) reason = j.reason;\n } catch {\n // body wasn't json\n }\n throw new Error(`postActionToConsole failed: ${reason}`);\n }\n const j = (await r.json()) as { ok: true; action: PostActionResult };\n return j.action;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orangecheck/agent-vercel",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"bitcoin",
|
|
7
|
+
"agent",
|
|
8
|
+
"authority",
|
|
9
|
+
"vercel",
|
|
10
|
+
"ai-sdk",
|
|
11
|
+
"tool-calls",
|
|
12
|
+
"bip322",
|
|
13
|
+
"oc-agent",
|
|
14
|
+
"orangecheck"
|
|
15
|
+
],
|
|
16
|
+
"author": "OrangeCheck",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/orangecheck/oc-packages.git",
|
|
21
|
+
"directory": "agent-vercel"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/orangecheck/oc-agent-protocol",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/orangecheck/oc-agent-protocol/issues"
|
|
26
|
+
},
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"module": "./dist/index.mjs",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.mjs",
|
|
34
|
+
"require": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"src",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"dev": "tsup --watch",
|
|
46
|
+
"type-check": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"clean": "rm -rf dist",
|
|
50
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@noble/hashes": "^1.5.0",
|
|
54
|
+
"@orangecheck/agent-core": "^0.3.0",
|
|
55
|
+
"@orangecheck/agent-signer": "^0.1.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^22.10.2",
|
|
59
|
+
"tsup": "^8.3.5",
|
|
60
|
+
"typescript": "^5.7.2",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { canonicalizeToolCall, toolCallHash, ocTool } from './index.js';
|
|
4
|
+
|
|
5
|
+
describe('canonicalizeToolCall', () => {
|
|
6
|
+
it('produces byte-identical output for identical inputs (key reorder)', () => {
|
|
7
|
+
const a = canonicalizeToolCall({
|
|
8
|
+
callId: 'tc_1',
|
|
9
|
+
verb: 'invoice.create',
|
|
10
|
+
args: { customer: 'acme', amount: 14.2 },
|
|
11
|
+
});
|
|
12
|
+
const b = canonicalizeToolCall({
|
|
13
|
+
verb: 'invoice.create',
|
|
14
|
+
callId: 'tc_1',
|
|
15
|
+
args: { amount: 14.2, customer: 'acme' },
|
|
16
|
+
});
|
|
17
|
+
expect(a).toEqual(b);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('changes when verb changes', () => {
|
|
21
|
+
const a = toolCallHash({ callId: 't', verb: 'a', args: {} });
|
|
22
|
+
const b = toolCallHash({ callId: 't', verb: 'b', args: {} });
|
|
23
|
+
expect(a).not.toBe(b);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('changes when call id changes', () => {
|
|
27
|
+
const a = toolCallHash({ callId: 'a', verb: 'x', args: {} });
|
|
28
|
+
const b = toolCallHash({ callId: 'b', verb: 'x', args: {} });
|
|
29
|
+
expect(a).not.toBe(b);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('ocTool', () => {
|
|
34
|
+
it('returns a wrapped tool with the right metadata', () => {
|
|
35
|
+
const t = ocTool({
|
|
36
|
+
verb: 'invoice.create',
|
|
37
|
+
description: 'create',
|
|
38
|
+
parameters: {} as unknown,
|
|
39
|
+
execute: async () => 'ok',
|
|
40
|
+
});
|
|
41
|
+
expect(t.verb).toBe('invoice.create');
|
|
42
|
+
expect(t.description).toBe('create');
|
|
43
|
+
});
|
|
44
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// @orangecheck/agent-vercel — wrap Vercel AI SDK `tool()` invocations in
|
|
2
|
+
// OC Agent action envelopes.
|
|
3
|
+
//
|
|
4
|
+
// Provider-agnostic: the AI SDK is the abstraction layer. Whatever model
|
|
5
|
+
// you use underneath (Anthropic, OpenAI, Cohere, …) the tool primitive is
|
|
6
|
+
// the same {description, parameters, execute} shape. This adapter wraps
|
|
7
|
+
// `execute` so every successful call produces a signed agent-action
|
|
8
|
+
// envelope citing the active delegation.
|
|
9
|
+
//
|
|
10
|
+
// Two integration shapes:
|
|
11
|
+
//
|
|
12
|
+
// 1. ocTool({verb, parameters, execute})
|
|
13
|
+
// A drop-in replacement for `tool()`. The wrapped execute runs the
|
|
14
|
+
// scope check, signs the canonical (verb, args, callId) tuple,
|
|
15
|
+
// then runs your real handler.
|
|
16
|
+
//
|
|
17
|
+
// 2. stampToolCall(input)
|
|
18
|
+
// Lower-level stamping helper for cases where you have your own
|
|
19
|
+
// tool-execution loop and want to emit envelopes manually.
|
|
20
|
+
//
|
|
21
|
+
// The agent + delegation are passed via a per-request context object
|
|
22
|
+
// (or threaded through closures) — we don't depend on AsyncLocalStorage
|
|
23
|
+
// to keep edge-runtime compatibility maximal.
|
|
24
|
+
|
|
25
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
26
|
+
import {
|
|
27
|
+
canonicalize,
|
|
28
|
+
computeActionId,
|
|
29
|
+
hexEncode,
|
|
30
|
+
isSubScope,
|
|
31
|
+
parseScope,
|
|
32
|
+
type DelegationEnvelope,
|
|
33
|
+
} from '@orangecheck/agent-core';
|
|
34
|
+
import {
|
|
35
|
+
signAsAgent,
|
|
36
|
+
type ActionEnvelope,
|
|
37
|
+
type SignerRef,
|
|
38
|
+
} from '@orangecheck/agent-signer';
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Canonicalization of a tool call
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export interface VercelToolCall {
|
|
45
|
+
/** Unique id assigned by the AI SDK for this tool invocation. */
|
|
46
|
+
callId: string;
|
|
47
|
+
/** Tool verb / name — passed in via ocTool() or set per-call by your wrapper. */
|
|
48
|
+
verb: string;
|
|
49
|
+
/** JSON-serializable arguments produced by the model. */
|
|
50
|
+
args: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Produce the canonical bytes for a Vercel AI SDK tool call.
|
|
55
|
+
*
|
|
56
|
+
* Shape (RFC 8785 canonical JSON, then trailing LF):
|
|
57
|
+
* {
|
|
58
|
+
* "args": <canonical JSON of args>,
|
|
59
|
+
* "call_id": <callId>,
|
|
60
|
+
* "verb": <verb>
|
|
61
|
+
* }
|
|
62
|
+
*/
|
|
63
|
+
export function canonicalizeToolCall(c: VercelToolCall): Uint8Array {
|
|
64
|
+
const canon = {
|
|
65
|
+
args: c.args,
|
|
66
|
+
call_id: c.callId,
|
|
67
|
+
verb: c.verb,
|
|
68
|
+
};
|
|
69
|
+
const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);
|
|
70
|
+
return new TextEncoder().encode(str + '\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function toolCallHash(c: VercelToolCall): string {
|
|
74
|
+
return 'sha256:' + hexEncode(sha256(canonicalizeToolCall(c)));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function toolCallActionId(
|
|
78
|
+
agentAddress: string,
|
|
79
|
+
signedAt: string,
|
|
80
|
+
delegationId: string,
|
|
81
|
+
scopeExercised: string,
|
|
82
|
+
c: VercelToolCall
|
|
83
|
+
): string {
|
|
84
|
+
return computeActionId({
|
|
85
|
+
address: agentAddress,
|
|
86
|
+
content_hash: toolCallHash(c),
|
|
87
|
+
content_length: canonicalizeToolCall(c).byteLength,
|
|
88
|
+
content_mime: 'application/vnd.oc-agent.vercel-tool-call+json',
|
|
89
|
+
signed_at: signedAt,
|
|
90
|
+
delegation_id: delegationId,
|
|
91
|
+
scope_exercised: scopeExercised,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// Stamping primitive
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export interface AgentContext {
|
|
100
|
+
agent: SignerRef;
|
|
101
|
+
delegation: DelegationEnvelope;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface StampToolCallInput extends AgentContext {
|
|
105
|
+
call: VercelToolCall;
|
|
106
|
+
/** Defaults to `vercel:tool(verb=<verb>)` — the tightest admissible sub-scope. */
|
|
107
|
+
scopeExercised?: string;
|
|
108
|
+
signedAt?: Date;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function stampToolCall(input: StampToolCallInput): Promise<ActionEnvelope> {
|
|
112
|
+
const scopeExercised =
|
|
113
|
+
input.scopeExercised ?? `vercel:tool(verb=${input.call.verb})`;
|
|
114
|
+
|
|
115
|
+
const granted = (input.delegation.scopes ?? []).map(parseScope);
|
|
116
|
+
const exercisedParsed = parseScope(scopeExercised);
|
|
117
|
+
if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`stampToolCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const bytes = canonicalizeToolCall(input.call);
|
|
124
|
+
return signAsAgent({
|
|
125
|
+
agent: input.agent,
|
|
126
|
+
delegation: input.delegation as never,
|
|
127
|
+
content: { hash: toolCallHash(input.call), length: bytes.byteLength },
|
|
128
|
+
mime: 'application/vnd.oc-agent.vercel-tool-call+json',
|
|
129
|
+
scopeExercised,
|
|
130
|
+
signedAt: input.signedAt,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
135
|
+
// ocTool — drop-in tool() replacement
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
export interface OcToolInput<TArgs extends Record<string, unknown>, TResult> {
|
|
139
|
+
/** OC Agent verb under which this tool is exercised. */
|
|
140
|
+
verb: string;
|
|
141
|
+
/** Pass-through to AI SDK's `tool()` — the JSON Schema for inputs. */
|
|
142
|
+
parameters?: unknown;
|
|
143
|
+
/** Pass-through description shown to the model. */
|
|
144
|
+
description?: string;
|
|
145
|
+
/** Your real handler. Returns whatever the tool returns. */
|
|
146
|
+
execute: (args: TArgs) => Promise<TResult>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface OcToolWrapped<TArgs extends Record<string, unknown>, TResult> {
|
|
150
|
+
verb: string;
|
|
151
|
+
parameters?: unknown;
|
|
152
|
+
description?: string;
|
|
153
|
+
/**
|
|
154
|
+
* The execute fn the AI SDK calls. Receives args + a callId (the AI
|
|
155
|
+
* SDK passes its own callId through — adapter glue passes it via
|
|
156
|
+
* context). Optional `console` field on the context tells the
|
|
157
|
+
* wrapper to fire-and-forget POST the stamped action to console.
|
|
158
|
+
* ochk.io/api/actions after the underlying execute() returns.
|
|
159
|
+
*/
|
|
160
|
+
execute: (
|
|
161
|
+
args: TArgs,
|
|
162
|
+
ctx: AgentContext & { callId: string; console?: ConsoleClient }
|
|
163
|
+
) => Promise<{
|
|
164
|
+
result: TResult;
|
|
165
|
+
action: ActionEnvelope;
|
|
166
|
+
posted: PostActionResult | null;
|
|
167
|
+
}>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Wrap a tool() definition so its execute path is scope-checked and
|
|
172
|
+
* envelope-emitting. The exact glue depends on which AI-SDK release you're
|
|
173
|
+
* on — the typical pattern is:
|
|
174
|
+
*
|
|
175
|
+
* const tools = {
|
|
176
|
+
* 'invoice.create': tool({
|
|
177
|
+
* description: '…',
|
|
178
|
+
* parameters: schema,
|
|
179
|
+
* execute: async (args, { toolCallId }) => {
|
|
180
|
+
* const { result, action } = await invoiceCreate.execute(args, {
|
|
181
|
+
* agent, delegation, callId: toolCallId,
|
|
182
|
+
* });
|
|
183
|
+
* await yourAuditPipeline.append(action);
|
|
184
|
+
* return result;
|
|
185
|
+
* },
|
|
186
|
+
* }),
|
|
187
|
+
* };
|
|
188
|
+
*
|
|
189
|
+
* const invoiceCreate = ocTool({
|
|
190
|
+
* verb: 'invoice.create',
|
|
191
|
+
* parameters: schema,
|
|
192
|
+
* execute: async (args) => myInvoiceCreateImpl(args),
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* The `ocTool` value is provider/SDK-agnostic — bring your own glue around
|
|
196
|
+
* it. v1 of the adapter intentionally does not import `ai` so it works on
|
|
197
|
+
* any release.
|
|
198
|
+
*/
|
|
199
|
+
export function ocTool<TArgs extends Record<string, unknown>, TResult>(
|
|
200
|
+
input: OcToolInput<TArgs, TResult>
|
|
201
|
+
): OcToolWrapped<TArgs, TResult> {
|
|
202
|
+
return {
|
|
203
|
+
verb: input.verb,
|
|
204
|
+
parameters: input.parameters,
|
|
205
|
+
description: input.description,
|
|
206
|
+
execute: async (args, ctx) => {
|
|
207
|
+
const call: VercelToolCall = {
|
|
208
|
+
callId: ctx.callId,
|
|
209
|
+
verb: input.verb,
|
|
210
|
+
args,
|
|
211
|
+
};
|
|
212
|
+
const action = await stampToolCall({
|
|
213
|
+
agent: ctx.agent,
|
|
214
|
+
delegation: ctx.delegation,
|
|
215
|
+
call,
|
|
216
|
+
});
|
|
217
|
+
const result = await input.execute(args);
|
|
218
|
+
let posted: PostActionResult | null = null;
|
|
219
|
+
if (ctx.console) {
|
|
220
|
+
try {
|
|
221
|
+
posted = await postActionToConsole(action, ctx.console);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
// eslint-disable-next-line no-console
|
|
224
|
+
console.error('[oc-agent-vercel] postActionToConsole failed:', err);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { result, action, posted };
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
233
|
+
// Console integration: POST stamped actions to console.ochk.io/api/actions
|
|
234
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
export interface ConsoleClient {
|
|
237
|
+
/** Defaults to https://console.ochk.io. */
|
|
238
|
+
baseUrl?: string;
|
|
239
|
+
/** Bearer token from /settings § 03 (`ock_<hex>`). */
|
|
240
|
+
apiToken: string;
|
|
241
|
+
/** Project the action belongs to (proj_*). */
|
|
242
|
+
projectId: string;
|
|
243
|
+
/** Optional fetch override for runtimes that need it. */
|
|
244
|
+
fetch?: typeof fetch;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface PostActionResult {
|
|
248
|
+
id: string;
|
|
249
|
+
project_id: string;
|
|
250
|
+
delegation_id: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* POST a stamped action envelope to console.ochk.io/api/actions. The
|
|
255
|
+
* console re-derives the action id, validates agent-must-match-
|
|
256
|
+
* delegation, persists, fans out to Nostr (kind 30084), submits to
|
|
257
|
+
* OC Stamp, and triggers any subscribed webhooks. Throws on non-2xx
|
|
258
|
+
* with the server's reason string.
|
|
259
|
+
*/
|
|
260
|
+
export async function postActionToConsole(
|
|
261
|
+
action: ActionEnvelope,
|
|
262
|
+
client: ConsoleClient
|
|
263
|
+
): Promise<PostActionResult> {
|
|
264
|
+
const baseUrl = client.baseUrl ?? 'https://console.ochk.io';
|
|
265
|
+
const f = client.fetch ?? fetch;
|
|
266
|
+
const body = {
|
|
267
|
+
project_id: client.projectId,
|
|
268
|
+
delegation_id: action.delegation_id,
|
|
269
|
+
agent_address: action.signer.address,
|
|
270
|
+
scope_exercised: action.scope_exercised,
|
|
271
|
+
content_hash: action.content.hash,
|
|
272
|
+
content_length: action.content.length,
|
|
273
|
+
content_mime: action.content.mime,
|
|
274
|
+
signed_at: action.signed_at,
|
|
275
|
+
signature: action.sig.value,
|
|
276
|
+
id: action.id,
|
|
277
|
+
};
|
|
278
|
+
const r = await f(`${baseUrl}/api/actions`, {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: {
|
|
281
|
+
'Content-Type': 'application/json',
|
|
282
|
+
Authorization: `Bearer ${client.apiToken}`,
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify(body),
|
|
285
|
+
});
|
|
286
|
+
if (!r.ok) {
|
|
287
|
+
let reason = `http_${r.status}`;
|
|
288
|
+
try {
|
|
289
|
+
const j = (await r.json()) as { reason?: string };
|
|
290
|
+
if (j.reason) reason = j.reason;
|
|
291
|
+
} catch {
|
|
292
|
+
// body wasn't json
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`postActionToConsole failed: ${reason}`);
|
|
295
|
+
}
|
|
296
|
+
const j = (await r.json()) as { ok: true; action: PostActionResult };
|
|
297
|
+
return j.action;
|
|
298
|
+
}
|