@orangecheck/agent-openai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OrangeCheck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @orangecheck/agent-openai
2
+
3
+ Wrap [OpenAI function calls](https://platform.openai.com/docs/guides/function-calling) with [OC Agent](https://github.com/orangecheck/oc-agent-protocol) scope enforcement and emit a signed `agent-action` envelope per invocation. Covers both the new **Responses API** (`function_call` items) and the legacy **Chat Completions** tool path (`tool_calls`).
4
+
5
+ ## Why
6
+
7
+ Function calling is the OpenAI-shaped way agents reach out to the world. Without scope enforcement and a cryptographic action receipt, "the model called this function" is the entire audit trail. With this adapter, every call carries: the agent address (BIP-322 signed), the active delegation, the exercised scope, the content hash of the inputs, and the OC Stamp anchor — replayable offline forever.
8
+
9
+ ## Status
10
+
11
+ **v0.0.1 · in design.** API shape is stable; canonicalization is locked. Production wiring (per-event streaming integration with the OpenAI SDK's `responses.stream()` / `chat.completions.create()`) lands when the first design partner names the integration.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm i @orangecheck/agent-openai
17
+ # peer deps:
18
+ npm i @orangecheck/agent-core @orangecheck/agent-signer
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ ```ts
24
+ import { invokeWithStamp } from '@orangecheck/agent-openai';
25
+
26
+ // Pull a tool_call out of a Chat Completions response …
27
+ const toolCall = response.choices[0].message.tool_calls[0];
28
+
29
+ const { result, action, call } = await invokeWithStamp({
30
+ agent,
31
+ delegation,
32
+ call: toolCall, // raw OpenAI shape — we normalize
33
+ execute: async (c) => myInvoiceCreateImpl(c.arguments),
34
+ });
35
+
36
+ // `result` becomes the tool result you feed back to the model.
37
+ // `call` is the normalized OpenAiFunctionCall (id, name, arguments).
38
+ // `action` is the signed envelope; ship to your audit bundle / Nostr.
39
+ ```
40
+
41
+ The same `invokeWithStamp` works on Responses-API `function_call` items — `parseFunctionCall` handles the `call_id` / `id` and string-vs-object `arguments` differences automatically.
42
+
43
+ ## What gets stamped
44
+
45
+ The stamp's `content.hash` is a SHA-256 of the RFC-8785-canonicalized JSON:
46
+
47
+ ```json
48
+ {
49
+ "arguments": <canonical JSON of arguments>,
50
+ "id": "<call id>",
51
+ "name": "<function name>"
52
+ }
53
+ ```
54
+
55
+ The action's `scope_exercised` defaults to `openai:function(name=<name>)`. Override via `scopeExercised`.
56
+
57
+ ## API
58
+
59
+ - `parseFunctionCall(raw)` — normalizes a Chat Completions `tool_call` or Responses `function_call` into `{id, name, arguments}`.
60
+ - `canonicalizeFunctionCall(call)` — RFC 8785 canonical bytes.
61
+ - `functionCallHash(call)` — `sha256:<64-hex>` for `content.hash`.
62
+ - `stampFunctionCall(input)` — sign without executing.
63
+ - `invokeWithStamp(input)` — sign + execute + return both.
64
+
65
+ ## License
66
+
67
+ MIT.
68
+
69
+ ## Related
70
+
71
+ - [oc-agent-protocol](https://github.com/orangecheck/oc-agent-protocol) — normative wire format.
72
+ - [@orangecheck/agent-core](https://www.npmjs.com/package/@orangecheck/agent-core) — verifier library.
73
+ - [@orangecheck/agent-mcp](https://www.npmjs.com/package/@orangecheck/agent-mcp) — same pattern, for MCP.
74
+ - [@orangecheck/agent-anthropic](https://www.npmjs.com/package/@orangecheck/agent-anthropic) — same pattern, for Claude Tool Use.
75
+ - [console.ochk.io/integrations/openai](https://console.ochk.io/integrations/openai) — managed-tier story.
@@ -0,0 +1,50 @@
1
+ import { DelegationEnvelope } from '@orangecheck/agent-core';
2
+ import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
3
+
4
+ interface OpenAiFunctionCall {
5
+ id: string;
6
+ name: string;
7
+ arguments: Record<string, unknown>;
8
+ }
9
+ declare function parseFunctionCall(raw: unknown): OpenAiFunctionCall;
10
+ declare function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array;
11
+ declare function functionCallHash(c: OpenAiFunctionCall): string;
12
+ declare function functionCallActionId(agentAddress: string, signedAt: string, delegationId: string, scopeExercised: string, c: OpenAiFunctionCall): string;
13
+ interface StampFunctionCallInput {
14
+ agent: SignerRef;
15
+ delegation: DelegationEnvelope;
16
+ call: OpenAiFunctionCall | unknown;
17
+ scopeExercised?: string;
18
+ signedAt?: Date;
19
+ }
20
+ declare function stampFunctionCall(input: StampFunctionCallInput): Promise<ActionEnvelope>;
21
+ interface InvokeWithStampInput<T> extends StampFunctionCallInput {
22
+ execute: (call: OpenAiFunctionCall) => Promise<T>;
23
+ }
24
+ interface InvokeWithStampResult<T> {
25
+ result: T;
26
+ action: ActionEnvelope;
27
+ call: OpenAiFunctionCall;
28
+ }
29
+ declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
30
+ interface ConsoleClient {
31
+ baseUrl?: string;
32
+ apiToken: string;
33
+ projectId: string;
34
+ fetch?: typeof fetch;
35
+ }
36
+ interface PostActionResult {
37
+ id: string;
38
+ project_id: string;
39
+ delegation_id: string;
40
+ }
41
+ declare function postActionToConsole(action: ActionEnvelope, client: ConsoleClient): Promise<PostActionResult>;
42
+ interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
43
+ console?: ConsoleClient;
44
+ }
45
+ interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
46
+ posted: PostActionResult | null;
47
+ }
48
+ declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
49
+
50
+ export { type ConsoleClient, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type OpenAiFunctionCall, type PostActionResult, type StampFunctionCallInput, canonicalizeFunctionCall, functionCallActionId, functionCallHash, invokeWithStamp, invokeWithStampAndPost, parseFunctionCall, postActionToConsole, stampFunctionCall };
@@ -0,0 +1,50 @@
1
+ import { DelegationEnvelope } from '@orangecheck/agent-core';
2
+ import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
3
+
4
+ interface OpenAiFunctionCall {
5
+ id: string;
6
+ name: string;
7
+ arguments: Record<string, unknown>;
8
+ }
9
+ declare function parseFunctionCall(raw: unknown): OpenAiFunctionCall;
10
+ declare function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array;
11
+ declare function functionCallHash(c: OpenAiFunctionCall): string;
12
+ declare function functionCallActionId(agentAddress: string, signedAt: string, delegationId: string, scopeExercised: string, c: OpenAiFunctionCall): string;
13
+ interface StampFunctionCallInput {
14
+ agent: SignerRef;
15
+ delegation: DelegationEnvelope;
16
+ call: OpenAiFunctionCall | unknown;
17
+ scopeExercised?: string;
18
+ signedAt?: Date;
19
+ }
20
+ declare function stampFunctionCall(input: StampFunctionCallInput): Promise<ActionEnvelope>;
21
+ interface InvokeWithStampInput<T> extends StampFunctionCallInput {
22
+ execute: (call: OpenAiFunctionCall) => Promise<T>;
23
+ }
24
+ interface InvokeWithStampResult<T> {
25
+ result: T;
26
+ action: ActionEnvelope;
27
+ call: OpenAiFunctionCall;
28
+ }
29
+ declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
30
+ interface ConsoleClient {
31
+ baseUrl?: string;
32
+ apiToken: string;
33
+ projectId: string;
34
+ fetch?: typeof fetch;
35
+ }
36
+ interface PostActionResult {
37
+ id: string;
38
+ project_id: string;
39
+ delegation_id: string;
40
+ }
41
+ declare function postActionToConsole(action: ActionEnvelope, client: ConsoleClient): Promise<PostActionResult>;
42
+ interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
43
+ console?: ConsoleClient;
44
+ }
45
+ interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
46
+ posted: PostActionResult | null;
47
+ }
48
+ declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
49
+
50
+ export { type ConsoleClient, type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type OpenAiFunctionCall, type PostActionResult, type StampFunctionCallInput, canonicalizeFunctionCall, functionCallActionId, functionCallHash, invokeWithStamp, invokeWithStampAndPost, parseFunctionCall, postActionToConsole, stampFunctionCall };
package/dist/index.js ADDED
@@ -0,0 +1,147 @@
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 parseFunctionCall(raw) {
9
+ if (raw == null || typeof raw !== "object") {
10
+ throw new TypeError("parseFunctionCall: input is not an object");
11
+ }
12
+ const r = raw;
13
+ if (r.function && typeof r.function === "object") {
14
+ const f = r.function;
15
+ return {
16
+ id: String(r.id ?? ""),
17
+ name: String(f.name ?? ""),
18
+ arguments: parseArgs(f.arguments)
19
+ };
20
+ }
21
+ return {
22
+ id: String(r.id ?? r.call_id ?? ""),
23
+ name: String(r.name ?? ""),
24
+ arguments: parseArgs(r.arguments)
25
+ };
26
+ }
27
+ function parseArgs(args) {
28
+ if (args == null) return {};
29
+ if (typeof args === "object") return args;
30
+ if (typeof args === "string") {
31
+ try {
32
+ const v = JSON.parse(args);
33
+ if (v && typeof v === "object") return v;
34
+ return { _raw: args };
35
+ } catch {
36
+ return { _raw: args };
37
+ }
38
+ }
39
+ return { _raw: String(args) };
40
+ }
41
+ function canonicalizeFunctionCall(c) {
42
+ const canon = {
43
+ arguments: c.arguments,
44
+ id: c.id,
45
+ name: c.name
46
+ };
47
+ const str = agentCore.canonicalize(canon);
48
+ return new TextEncoder().encode(str + "\n");
49
+ }
50
+ function functionCallHash(c) {
51
+ return "sha256:" + agentCore.hexEncode(sha256.sha256(canonicalizeFunctionCall(c)));
52
+ }
53
+ function functionCallActionId(agentAddress, signedAt, delegationId, scopeExercised, c) {
54
+ return agentCore.computeActionId({
55
+ address: agentAddress,
56
+ content_hash: functionCallHash(c),
57
+ content_length: canonicalizeFunctionCall(c).byteLength,
58
+ content_mime: "application/vnd.oc-agent.openai-function-call+json",
59
+ signed_at: signedAt,
60
+ delegation_id: delegationId,
61
+ scope_exercised: scopeExercised
62
+ });
63
+ }
64
+ async function stampFunctionCall(input) {
65
+ const fc = typeof input.call === "object" && input.call !== null && "arguments" in input.call && typeof input.call.arguments !== "string" ? input.call : parseFunctionCall(input.call);
66
+ const scopeExercised = input.scopeExercised ?? `openai:function(name=${fc.name})`;
67
+ const granted = (input.delegation.scopes ?? []).map(agentCore.parseScope);
68
+ const exercisedParsed = agentCore.parseScope(scopeExercised);
69
+ if (!granted.some((g) => agentCore.isSubScope(exercisedParsed, g))) {
70
+ throw new Error(
71
+ `stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
72
+ );
73
+ }
74
+ const bytes = canonicalizeFunctionCall(fc);
75
+ return agentSigner.signAsAgent({
76
+ agent: input.agent,
77
+ delegation: input.delegation,
78
+ content: { hash: functionCallHash(fc), length: bytes.byteLength },
79
+ mime: "application/vnd.oc-agent.openai-function-call+json",
80
+ scopeExercised,
81
+ signedAt: input.signedAt
82
+ });
83
+ }
84
+ async function invokeWithStamp(input) {
85
+ const action = await stampFunctionCall(input);
86
+ const fc = parseFunctionCall(input.call);
87
+ const result = await input.execute(fc);
88
+ return { result, action, call: fc };
89
+ }
90
+ async function postActionToConsole(action, client) {
91
+ const baseUrl = client.baseUrl ?? "https://console.ochk.io";
92
+ const f = client.fetch ?? fetch;
93
+ const body = {
94
+ project_id: client.projectId,
95
+ delegation_id: action.delegation_id,
96
+ agent_address: action.signer.address,
97
+ scope_exercised: action.scope_exercised,
98
+ content_hash: action.content.hash,
99
+ content_length: action.content.length,
100
+ content_mime: action.content.mime,
101
+ signed_at: action.signed_at,
102
+ signature: action.sig.value,
103
+ id: action.id
104
+ };
105
+ const r = await f(`${baseUrl}/api/actions`, {
106
+ method: "POST",
107
+ headers: {
108
+ "Content-Type": "application/json",
109
+ Authorization: `Bearer ${client.apiToken}`
110
+ },
111
+ body: JSON.stringify(body)
112
+ });
113
+ if (!r.ok) {
114
+ let reason = `http_${r.status}`;
115
+ try {
116
+ const j2 = await r.json();
117
+ if (j2.reason) reason = j2.reason;
118
+ } catch {
119
+ }
120
+ throw new Error(`postActionToConsole failed: ${reason}`);
121
+ }
122
+ const j = await r.json();
123
+ return j.action;
124
+ }
125
+ async function invokeWithStampAndPost(input) {
126
+ const { result, action, call } = await invokeWithStamp(input);
127
+ let posted = null;
128
+ if (input.console) {
129
+ try {
130
+ posted = await postActionToConsole(action, input.console);
131
+ } catch (err) {
132
+ console.error("[oc-agent-openai] postActionToConsole failed:", err);
133
+ }
134
+ }
135
+ return { result, action, call, posted };
136
+ }
137
+
138
+ exports.canonicalizeFunctionCall = canonicalizeFunctionCall;
139
+ exports.functionCallActionId = functionCallActionId;
140
+ exports.functionCallHash = functionCallHash;
141
+ exports.invokeWithStamp = invokeWithStamp;
142
+ exports.invokeWithStampAndPost = invokeWithStampAndPost;
143
+ exports.parseFunctionCall = parseFunctionCall;
144
+ exports.postActionToConsole = postActionToConsole;
145
+ exports.stampFunctionCall = stampFunctionCall;
146
+ //# sourceMappingURL=index.js.map
147
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","parseScope","isSubScope","signAsAgent","j"],"mappings":";;;;;;;AAqDO,SAAS,kBAAkB,GAAA,EAAkC;AAChE,EAAA,IAAI,GAAA,IAAO,IAAA,IAAQ,OAAO,GAAA,KAAQ,QAAA,EAAU;AACxC,IAAA,MAAM,IAAI,UAAU,2CAA2C,CAAA;AAAA,EACnE;AACA,EAAA,MAAM,CAAA,GAAI,GAAA;AAGV,EAAA,IAAI,CAAA,CAAE,QAAA,IAAY,OAAO,CAAA,CAAE,aAAa,QAAA,EAAU;AAC9C,IAAA,MAAM,IAAI,CAAA,CAAE,QAAA;AACZ,IAAA,OAAO;AAAA,MACH,EAAA,EAAI,MAAA,CAAO,CAAA,CAAE,EAAA,IAAM,EAAE,CAAA;AAAA,MACrB,IAAA,EAAM,MAAA,CAAO,CAAA,CAAE,IAAA,IAAQ,EAAE,CAAA;AAAA,MACzB,SAAA,EAAW,SAAA,CAAU,CAAA,CAAE,SAAS;AAAA,KACpC;AAAA,EACJ;AAGA,EAAA,OAAO;AAAA,IACH,IAAI,MAAA,CAAO,CAAA,CAAE,EAAA,IAAM,CAAA,CAAE,WAAW,EAAE,CAAA;AAAA,IAClC,IAAA,EAAM,MAAA,CAAO,CAAA,CAAE,IAAA,IAAQ,EAAE,CAAA;AAAA,IACzB,SAAA,EAAW,SAAA,CAAU,CAAA,CAAE,SAAS;AAAA,GACpC;AACJ;AAEA,SAAS,UAAU,IAAA,EAAwC;AACvD,EAAA,IAAI,IAAA,IAAQ,IAAA,EAAM,OAAO,EAAC;AAC1B,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,EAAU,OAAO,IAAA;AACrC,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC1B,IAAA,IAAI;AACA,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AACzB,MAAA,IAAI,CAAA,IAAK,OAAO,CAAA,KAAM,QAAA,EAAU,OAAO,CAAA;AACvC,MAAA,OAAO,EAAE,MAAM,IAAA,EAAK;AAAA,IACxB,CAAA,CAAA,MAAQ;AACJ,MAAA,OAAO,EAAE,MAAM,IAAA,EAAK;AAAA,IACxB;AAAA,EACJ;AACA,EAAA,OAAO,EAAE,IAAA,EAAM,MAAA,CAAO,IAAI,CAAA,EAAE;AAChC;AAiBO,SAAS,yBAAyB,CAAA,EAAmC;AACxE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,WAAW,CAAA,CAAE,SAAA;AAAA,IACb,IAAI,CAAA,CAAE,EAAA;AAAA,IACN,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,iBAAiB,CAAA,EAA+B;AAC5D,EAAA,OAAO,YAAYC,mBAAA,CAAUC,aAAA,CAAO,wBAAA,CAAyB,CAAC,CAAC,CAAC,CAAA;AACpE;AAEO,SAAS,oBAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,CAAA,EACM;AACN,EAAA,OAAOC,yBAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,iBAAiB,CAAC,CAAA;AAAA,IAChC,cAAA,EAAgB,wBAAA,CAAyB,CAAC,CAAA,CAAE,UAAA;AAAA,IAC5C,YAAA,EAAc,oDAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAgBA,eAAsB,kBAClB,KAAA,EACuB;AACvB,EAAA,MAAM,EAAA,GACF,OAAO,KAAA,CAAM,IAAA,KAAS,YACtB,KAAA,CAAM,IAAA,KAAS,QACf,WAAA,IAAgB,KAAA,CAAM,QACtB,OAAQ,KAAA,CAAM,KAAiC,SAAA,KAAc,QAAA,GACtD,MAAM,IAAA,GACP,iBAAA,CAAkB,MAAM,IAAI,CAAA;AAEtC,EAAA,MAAM,cAAA,GACF,KAAA,CAAM,cAAA,IAAkB,CAAA,qBAAA,EAAwB,GAAG,IAAI,CAAA,CAAA,CAAA;AAE3D,EAAA,MAAM,WAAW,KAAA,CAAM,UAAA,CAAW,UAAU,EAAC,EAAG,IAAIC,oBAAU,CAAA;AAC9D,EAAA,MAAM,eAAA,GAAkBA,qBAAW,cAAc,CAAA;AACjD,EAAA,IAAI,CAAC,QAAQ,IAAA,CAAK,CAAC,MAAMC,oBAAA,CAAW,eAAA,EAAiB,CAAC,CAAC,CAAA,EAAG;AACtD,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,uCAAuC,cAAc,CAAA,yCAAA;AAAA,KACzD;AAAA,EACJ;AAEA,EAAA,MAAM,KAAA,GAAQ,yBAAyB,EAAE,CAAA;AACzC,EAAA,OAAOC,uBAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,gBAAA,CAAiB,EAAE,CAAA,EAAG,MAAA,EAAQ,MAAM,UAAA,EAAW;AAAA,IAChE,IAAA,EAAM,oDAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAaA,eAAsB,gBAClB,KAAA,EACiC;AACjC,EAAA,MAAM,MAAA,GAAS,MAAM,iBAAA,CAAkB,KAAK,CAAA;AAC5C,EAAA,MAAM,EAAA,GAAK,iBAAA,CAAkB,KAAA,CAAM,IAAI,CAAA;AACvC,EAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,OAAA,CAAQ,EAAE,CAAA;AACrC,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,IAAA,EAAM,EAAA,EAAG;AACtC;AA8BA,eAAsB,mBAAA,CAClB,QACA,MAAA,EACyB;AACzB,EAAA,MAAM,OAAA,GAAU,OAAO,OAAA,IAAW,yBAAA;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,MAAMC,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,4BAAA,EAA+B,MAAM,CAAA,CAAE,CAAA;AAAA,EAC3D;AACA,EAAA,MAAM,CAAA,GAAK,MAAM,CAAA,CAAE,IAAA,EAAK;AACxB,EAAA,OAAO,CAAA,CAAE,MAAA;AACb;AAYA,eAAsB,uBAClB,KAAA,EACwC;AACxC,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAQ,MAAK,GAAI,MAAM,gBAAgB,KAAK,CAAA;AAC5D,EAAA,IAAI,MAAA,GAAkC,IAAA;AACtC,EAAA,IAAI,MAAM,OAAA,EAAS;AACf,IAAA,IAAI;AACA,MAAA,MAAA,GAAS,MAAM,mBAAA,CAAoB,MAAA,EAAQ,KAAA,CAAM,OAAO,CAAA;AAAA,IAC5D,SAAS,GAAA,EAAK;AAEV,MAAA,OAAA,CAAQ,KAAA,CAAM,iDAAiD,GAAG,CAAA;AAAA,IACtE;AAAA,EACJ;AACA,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAC1C","file":"index.js","sourcesContent":["// @orangecheck/agent-openai — wrap OpenAI function calls in OC Agent\n// action envelopes.\n//\n// Covers both surfaces:\n// - Responses API: response.output → tool_call items with\n// { type: 'function_call', name, arguments, call_id }\n// (`arguments` is a JSON string)\n// - Chat Completions: message.tool_calls[i] = { id, function: { name, arguments } }\n// (`arguments` is also a JSON string)\n//\n// Both shapes normalize through `parseFunctionCall()` to the canonical\n// {id, name, arguments} form before hashing.\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// Shape normalization\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OpenAiFunctionCall {\n /** OpenAI's `id` for this call (Chat Completions: `tool_call.id`; Responses: `call_id`). */\n id: string;\n /** The function name. */\n name: string;\n /** The (already-parsed) function arguments. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Normalize either a Chat Completions `tool_call` or a Responses\n * `function_call` item into our canonical {id, name, arguments} shape.\n *\n * Accepts:\n * - { id, function: { name, arguments } } (Chat Completions)\n * - { call_id, name, arguments } (Responses, function_call)\n * - { id, name, arguments } (already normalized)\n *\n * `arguments` may be either a JSON string (OpenAI's wire shape) or an\n * already-parsed object (your post-processing). Both work.\n */\nexport function parseFunctionCall(raw: unknown): OpenAiFunctionCall {\n if (raw == null || typeof raw !== 'object') {\n throw new TypeError('parseFunctionCall: input is not an object');\n }\n const r = raw as Record<string, unknown>;\n\n // Chat Completions shape\n if (r.function && typeof r.function === 'object') {\n const f = r.function as Record<string, unknown>;\n return {\n id: String(r.id ?? ''),\n name: String(f.name ?? ''),\n arguments: parseArgs(f.arguments),\n };\n }\n\n // Responses API or already-normalized\n return {\n id: String(r.id ?? r.call_id ?? ''),\n name: String(r.name ?? ''),\n arguments: parseArgs(r.arguments),\n };\n}\n\nfunction parseArgs(args: unknown): Record<string, unknown> {\n if (args == null) return {};\n if (typeof args === 'object') return args as Record<string, unknown>;\n if (typeof args === 'string') {\n try {\n const v = JSON.parse(args);\n if (v && typeof v === 'object') return v as Record<string, unknown>;\n return { _raw: args };\n } catch {\n return { _raw: args };\n }\n }\n return { _raw: String(args) };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Canonicalization\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Produce the canonical bytes for an OpenAI function call. The bytes are\n * what the agent-action envelope's `content.hash` is a SHA-256 of.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"arguments\": <canonical JSON of arguments>,\n * \"id\": <call id>,\n * \"name\": <function name>\n * }\n */\nexport function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array {\n const canon = {\n arguments: c.arguments,\n id: c.id,\n name: c.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 functionCallHash(c: OpenAiFunctionCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeFunctionCall(c)));\n}\n\nexport function functionCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: OpenAiFunctionCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: functionCallHash(c),\n content_length: canonicalizeFunctionCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.openai-function-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping + invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampFunctionCallInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n /** Either a normalized `OpenAiFunctionCall` or a raw OpenAI tool_call / function_call object. */\n call: OpenAiFunctionCall | unknown;\n /** Defaults to `openai:function(name=<name>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampFunctionCall(\n input: StampFunctionCallInput\n): Promise<ActionEnvelope> {\n const fc =\n typeof input.call === 'object' &&\n input.call !== null &&\n 'arguments' in (input.call as Record<string, unknown>) &&\n typeof (input.call as Record<string, unknown>).arguments !== 'string'\n ? (input.call as OpenAiFunctionCall)\n : parseFunctionCall(input.call);\n\n const scopeExercised =\n input.scopeExercised ?? `openai:function(name=${fc.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 `stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeFunctionCall(fc);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: functionCallHash(fc), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.openai-function-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\nexport interface InvokeWithStampInput<T> extends StampFunctionCallInput {\n /** Your handler. Receives the normalized OpenAiFunctionCall. */\n execute: (call: OpenAiFunctionCall) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n call: OpenAiFunctionCall;\n}\n\nexport async function invokeWithStamp<T>(\n input: InvokeWithStampInput<T>\n): Promise<InvokeWithStampResult<T>> {\n const action = await stampFunctionCall(input);\n const fc = parseFunctionCall(input.call);\n const result = await input.execute(fc);\n return { result, action, call: fc };\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\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp. */\n console?: ConsoleClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when console is configured AND the POST succeeded. */\n posted: PostActionResult | null;\n}\n\nexport async function invokeWithStampAndPost<T>(\n input: InvokeWithStampAndPostInput<T>\n): Promise<InvokeWithStampAndPostResult<T>> {\n const { result, action, call } = await invokeWithStamp(input);\n let posted: PostActionResult | null = null;\n if (input.console) {\n try {\n posted = await postActionToConsole(action, input.console);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-openai] postActionToConsole failed:', err);\n }\n }\n return { result, action, call, posted };\n}\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,138 @@
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 parseFunctionCall(raw) {
7
+ if (raw == null || typeof raw !== "object") {
8
+ throw new TypeError("parseFunctionCall: input is not an object");
9
+ }
10
+ const r = raw;
11
+ if (r.function && typeof r.function === "object") {
12
+ const f = r.function;
13
+ return {
14
+ id: String(r.id ?? ""),
15
+ name: String(f.name ?? ""),
16
+ arguments: parseArgs(f.arguments)
17
+ };
18
+ }
19
+ return {
20
+ id: String(r.id ?? r.call_id ?? ""),
21
+ name: String(r.name ?? ""),
22
+ arguments: parseArgs(r.arguments)
23
+ };
24
+ }
25
+ function parseArgs(args) {
26
+ if (args == null) return {};
27
+ if (typeof args === "object") return args;
28
+ if (typeof args === "string") {
29
+ try {
30
+ const v = JSON.parse(args);
31
+ if (v && typeof v === "object") return v;
32
+ return { _raw: args };
33
+ } catch {
34
+ return { _raw: args };
35
+ }
36
+ }
37
+ return { _raw: String(args) };
38
+ }
39
+ function canonicalizeFunctionCall(c) {
40
+ const canon = {
41
+ arguments: c.arguments,
42
+ id: c.id,
43
+ name: c.name
44
+ };
45
+ const str = canonicalize(canon);
46
+ return new TextEncoder().encode(str + "\n");
47
+ }
48
+ function functionCallHash(c) {
49
+ return "sha256:" + hexEncode(sha256(canonicalizeFunctionCall(c)));
50
+ }
51
+ function functionCallActionId(agentAddress, signedAt, delegationId, scopeExercised, c) {
52
+ return computeActionId({
53
+ address: agentAddress,
54
+ content_hash: functionCallHash(c),
55
+ content_length: canonicalizeFunctionCall(c).byteLength,
56
+ content_mime: "application/vnd.oc-agent.openai-function-call+json",
57
+ signed_at: signedAt,
58
+ delegation_id: delegationId,
59
+ scope_exercised: scopeExercised
60
+ });
61
+ }
62
+ async function stampFunctionCall(input) {
63
+ const fc = typeof input.call === "object" && input.call !== null && "arguments" in input.call && typeof input.call.arguments !== "string" ? input.call : parseFunctionCall(input.call);
64
+ const scopeExercised = input.scopeExercised ?? `openai:function(name=${fc.name})`;
65
+ const granted = (input.delegation.scopes ?? []).map(parseScope);
66
+ const exercisedParsed = parseScope(scopeExercised);
67
+ if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
68
+ throw new Error(
69
+ `stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
70
+ );
71
+ }
72
+ const bytes = canonicalizeFunctionCall(fc);
73
+ return signAsAgent({
74
+ agent: input.agent,
75
+ delegation: input.delegation,
76
+ content: { hash: functionCallHash(fc), length: bytes.byteLength },
77
+ mime: "application/vnd.oc-agent.openai-function-call+json",
78
+ scopeExercised,
79
+ signedAt: input.signedAt
80
+ });
81
+ }
82
+ async function invokeWithStamp(input) {
83
+ const action = await stampFunctionCall(input);
84
+ const fc = parseFunctionCall(input.call);
85
+ const result = await input.execute(fc);
86
+ return { result, action, call: fc };
87
+ }
88
+ async function postActionToConsole(action, client) {
89
+ const baseUrl = client.baseUrl ?? "https://console.ochk.io";
90
+ const f = client.fetch ?? fetch;
91
+ const body = {
92
+ project_id: client.projectId,
93
+ delegation_id: action.delegation_id,
94
+ agent_address: action.signer.address,
95
+ scope_exercised: action.scope_exercised,
96
+ content_hash: action.content.hash,
97
+ content_length: action.content.length,
98
+ content_mime: action.content.mime,
99
+ signed_at: action.signed_at,
100
+ signature: action.sig.value,
101
+ id: action.id
102
+ };
103
+ const r = await f(`${baseUrl}/api/actions`, {
104
+ method: "POST",
105
+ headers: {
106
+ "Content-Type": "application/json",
107
+ Authorization: `Bearer ${client.apiToken}`
108
+ },
109
+ body: JSON.stringify(body)
110
+ });
111
+ if (!r.ok) {
112
+ let reason = `http_${r.status}`;
113
+ try {
114
+ const j2 = await r.json();
115
+ if (j2.reason) reason = j2.reason;
116
+ } catch {
117
+ }
118
+ throw new Error(`postActionToConsole failed: ${reason}`);
119
+ }
120
+ const j = await r.json();
121
+ return j.action;
122
+ }
123
+ async function invokeWithStampAndPost(input) {
124
+ const { result, action, call } = await invokeWithStamp(input);
125
+ let posted = null;
126
+ if (input.console) {
127
+ try {
128
+ posted = await postActionToConsole(action, input.console);
129
+ } catch (err) {
130
+ console.error("[oc-agent-openai] postActionToConsole failed:", err);
131
+ }
132
+ }
133
+ return { result, action, call, posted };
134
+ }
135
+
136
+ export { canonicalizeFunctionCall, functionCallActionId, functionCallHash, invokeWithStamp, invokeWithStampAndPost, parseFunctionCall, postActionToConsole, stampFunctionCall };
137
+ //# sourceMappingURL=index.mjs.map
138
+ //# 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-openai — wrap OpenAI function calls in OC Agent\n// action envelopes.\n//\n// Covers both surfaces:\n// - Responses API: response.output → tool_call items with\n// { type: 'function_call', name, arguments, call_id }\n// (`arguments` is a JSON string)\n// - Chat Completions: message.tool_calls[i] = { id, function: { name, arguments } }\n// (`arguments` is also a JSON string)\n//\n// Both shapes normalize through `parseFunctionCall()` to the canonical\n// {id, name, arguments} form before hashing.\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// Shape normalization\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface OpenAiFunctionCall {\n /** OpenAI's `id` for this call (Chat Completions: `tool_call.id`; Responses: `call_id`). */\n id: string;\n /** The function name. */\n name: string;\n /** The (already-parsed) function arguments. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Normalize either a Chat Completions `tool_call` or a Responses\n * `function_call` item into our canonical {id, name, arguments} shape.\n *\n * Accepts:\n * - { id, function: { name, arguments } } (Chat Completions)\n * - { call_id, name, arguments } (Responses, function_call)\n * - { id, name, arguments } (already normalized)\n *\n * `arguments` may be either a JSON string (OpenAI's wire shape) or an\n * already-parsed object (your post-processing). Both work.\n */\nexport function parseFunctionCall(raw: unknown): OpenAiFunctionCall {\n if (raw == null || typeof raw !== 'object') {\n throw new TypeError('parseFunctionCall: input is not an object');\n }\n const r = raw as Record<string, unknown>;\n\n // Chat Completions shape\n if (r.function && typeof r.function === 'object') {\n const f = r.function as Record<string, unknown>;\n return {\n id: String(r.id ?? ''),\n name: String(f.name ?? ''),\n arguments: parseArgs(f.arguments),\n };\n }\n\n // Responses API or already-normalized\n return {\n id: String(r.id ?? r.call_id ?? ''),\n name: String(r.name ?? ''),\n arguments: parseArgs(r.arguments),\n };\n}\n\nfunction parseArgs(args: unknown): Record<string, unknown> {\n if (args == null) return {};\n if (typeof args === 'object') return args as Record<string, unknown>;\n if (typeof args === 'string') {\n try {\n const v = JSON.parse(args);\n if (v && typeof v === 'object') return v as Record<string, unknown>;\n return { _raw: args };\n } catch {\n return { _raw: args };\n }\n }\n return { _raw: String(args) };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Canonicalization\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Produce the canonical bytes for an OpenAI function call. The bytes are\n * what the agent-action envelope's `content.hash` is a SHA-256 of.\n *\n * Shape (RFC 8785 canonical JSON, then trailing LF):\n * {\n * \"arguments\": <canonical JSON of arguments>,\n * \"id\": <call id>,\n * \"name\": <function name>\n * }\n */\nexport function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array {\n const canon = {\n arguments: c.arguments,\n id: c.id,\n name: c.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 functionCallHash(c: OpenAiFunctionCall): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeFunctionCall(c)));\n}\n\nexport function functionCallActionId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n c: OpenAiFunctionCall\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: functionCallHash(c),\n content_length: canonicalizeFunctionCall(c).byteLength,\n content_mime: 'application/vnd.oc-agent.openai-function-call+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamping + invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampFunctionCallInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n /** Either a normalized `OpenAiFunctionCall` or a raw OpenAI tool_call / function_call object. */\n call: OpenAiFunctionCall | unknown;\n /** Defaults to `openai:function(name=<name>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\nexport async function stampFunctionCall(\n input: StampFunctionCallInput\n): Promise<ActionEnvelope> {\n const fc =\n typeof input.call === 'object' &&\n input.call !== null &&\n 'arguments' in (input.call as Record<string, unknown>) &&\n typeof (input.call as Record<string, unknown>).arguments !== 'string'\n ? (input.call as OpenAiFunctionCall)\n : parseFunctionCall(input.call);\n\n const scopeExercised =\n input.scopeExercised ?? `openai:function(name=${fc.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 `stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\n\n const bytes = canonicalizeFunctionCall(fc);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation as never,\n content: { hash: functionCallHash(fc), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.openai-function-call+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\nexport interface InvokeWithStampInput<T> extends StampFunctionCallInput {\n /** Your handler. Receives the normalized OpenAiFunctionCall. */\n execute: (call: OpenAiFunctionCall) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n call: OpenAiFunctionCall;\n}\n\nexport async function invokeWithStamp<T>(\n input: InvokeWithStampInput<T>\n): Promise<InvokeWithStampResult<T>> {\n const action = await stampFunctionCall(input);\n const fc = parseFunctionCall(input.call);\n const result = await input.execute(fc);\n return { result, action, call: fc };\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\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp. */\n console?: ConsoleClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when console is configured AND the POST succeeded. */\n posted: PostActionResult | null;\n}\n\nexport async function invokeWithStampAndPost<T>(\n input: InvokeWithStampAndPostInput<T>\n): Promise<InvokeWithStampAndPostResult<T>> {\n const { result, action, call } = await invokeWithStamp(input);\n let posted: PostActionResult | null = null;\n if (input.console) {\n try {\n posted = await postActionToConsole(action, input.console);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-openai] postActionToConsole failed:', err);\n }\n }\n return { result, action, call, posted };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@orangecheck/agent-openai",
3
+ "version": "0.1.0",
4
+ "description": "Wrap OpenAI function-calling (Responses API + Chat Completions tool path) with OC Agent scope enforcement and emit a signed agent-action envelope per function invocation.",
5
+ "keywords": [
6
+ "bitcoin",
7
+ "agent",
8
+ "authority",
9
+ "openai",
10
+ "responses",
11
+ "function-calling",
12
+ "tool-use",
13
+ "bip322",
14
+ "oc-agent",
15
+ "orangecheck"
16
+ ],
17
+ "author": "OrangeCheck",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/orangecheck/oc-packages.git",
22
+ "directory": "agent-openai"
23
+ },
24
+ "homepage": "https://github.com/orangecheck/oc-agent-protocol",
25
+ "bugs": {
26
+ "url": "https://github.com/orangecheck/oc-agent-protocol/issues"
27
+ },
28
+ "main": "./dist/index.js",
29
+ "module": "./dist/index.mjs",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.mjs",
35
+ "require": "./dist/index.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "dev": "tsup --watch",
47
+ "type-check": "tsc --noEmit",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest",
50
+ "clean": "rm -rf dist",
51
+ "prepublishOnly": "npm run clean && npm run build"
52
+ },
53
+ "dependencies": {
54
+ "@noble/hashes": "^1.5.0",
55
+ "@orangecheck/agent-core": "^0.3.0",
56
+ "@orangecheck/agent-signer": "^0.1.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^22.10.2",
60
+ "tsup": "^8.3.5",
61
+ "typescript": "^5.7.2",
62
+ "vitest": "^3.2.4"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ canonicalizeFunctionCall,
5
+ functionCallHash,
6
+ parseFunctionCall,
7
+ } from './index.js';
8
+
9
+ describe('parseFunctionCall', () => {
10
+ it('parses a Chat Completions tool_call', () => {
11
+ const c = parseFunctionCall({
12
+ id: 'call_abc',
13
+ type: 'function',
14
+ function: {
15
+ name: 'invoice.create',
16
+ arguments: '{"customer":"acme","amount":14.2}',
17
+ },
18
+ });
19
+ expect(c.id).toBe('call_abc');
20
+ expect(c.name).toBe('invoice.create');
21
+ expect(c.arguments).toEqual({ customer: 'acme', amount: 14.2 });
22
+ });
23
+
24
+ it('parses a Responses function_call item', () => {
25
+ const c = parseFunctionCall({
26
+ type: 'function_call',
27
+ call_id: 'call_xyz',
28
+ name: 'invoice.refund',
29
+ arguments: '{"order":"o_99","amount":22.5}',
30
+ });
31
+ expect(c.id).toBe('call_xyz');
32
+ expect(c.name).toBe('invoice.refund');
33
+ expect(c.arguments).toEqual({ order: 'o_99', amount: 22.5 });
34
+ });
35
+
36
+ it('handles already-parsed arguments', () => {
37
+ const c = parseFunctionCall({
38
+ id: 'call_abc',
39
+ name: 'invoice.create',
40
+ arguments: { customer: 'acme', amount: 14.2 },
41
+ });
42
+ expect(c.arguments).toEqual({ customer: 'acme', amount: 14.2 });
43
+ });
44
+
45
+ it('handles malformed argument strings without throwing', () => {
46
+ const c = parseFunctionCall({
47
+ id: 'call_abc',
48
+ name: 'invoice.create',
49
+ arguments: 'not-json',
50
+ });
51
+ expect(c.arguments).toEqual({ _raw: 'not-json' });
52
+ });
53
+ });
54
+
55
+ describe('canonicalizeFunctionCall', () => {
56
+ it('produces byte-identical output across Chat Completions vs Responses shapes', () => {
57
+ const cc = parseFunctionCall({
58
+ id: 'call_abc',
59
+ type: 'function',
60
+ function: {
61
+ name: 'invoice.create',
62
+ arguments: '{"customer":"acme","amount":14.2}',
63
+ },
64
+ });
65
+ const rsp = parseFunctionCall({
66
+ type: 'function_call',
67
+ call_id: 'call_abc',
68
+ name: 'invoice.create',
69
+ arguments: '{"amount":14.2,"customer":"acme"}',
70
+ });
71
+ expect(canonicalizeFunctionCall(cc)).toEqual(canonicalizeFunctionCall(rsp));
72
+ });
73
+
74
+ it('differs when arguments differ', () => {
75
+ const a = functionCallHash(
76
+ parseFunctionCall({
77
+ id: 'c',
78
+ name: 'x',
79
+ arguments: '{"a":1}',
80
+ })
81
+ );
82
+ const b = functionCallHash(
83
+ parseFunctionCall({
84
+ id: 'c',
85
+ name: 'x',
86
+ arguments: '{"a":2}',
87
+ })
88
+ );
89
+ expect(a).not.toBe(b);
90
+ });
91
+ });
package/src/index.ts ADDED
@@ -0,0 +1,299 @@
1
+ // @orangecheck/agent-openai — wrap OpenAI function calls in OC Agent
2
+ // action envelopes.
3
+ //
4
+ // Covers both surfaces:
5
+ // - Responses API: response.output → tool_call items with
6
+ // { type: 'function_call', name, arguments, call_id }
7
+ // (`arguments` is a JSON string)
8
+ // - Chat Completions: message.tool_calls[i] = { id, function: { name, arguments } }
9
+ // (`arguments` is also a JSON string)
10
+ //
11
+ // Both shapes normalize through `parseFunctionCall()` to the canonical
12
+ // {id, name, arguments} form before hashing.
13
+
14
+ import { sha256 } from '@noble/hashes/sha256';
15
+ import {
16
+ canonicalize,
17
+ computeActionId,
18
+ hexEncode,
19
+ isSubScope,
20
+ parseScope,
21
+ type DelegationEnvelope,
22
+ } from '@orangecheck/agent-core';
23
+ import {
24
+ signAsAgent,
25
+ type ActionEnvelope,
26
+ type SignerRef,
27
+ } from '@orangecheck/agent-signer';
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Shape normalization
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ export interface OpenAiFunctionCall {
34
+ /** OpenAI's `id` for this call (Chat Completions: `tool_call.id`; Responses: `call_id`). */
35
+ id: string;
36
+ /** The function name. */
37
+ name: string;
38
+ /** The (already-parsed) function arguments. */
39
+ arguments: Record<string, unknown>;
40
+ }
41
+
42
+ /**
43
+ * Normalize either a Chat Completions `tool_call` or a Responses
44
+ * `function_call` item into our canonical {id, name, arguments} shape.
45
+ *
46
+ * Accepts:
47
+ * - { id, function: { name, arguments } } (Chat Completions)
48
+ * - { call_id, name, arguments } (Responses, function_call)
49
+ * - { id, name, arguments } (already normalized)
50
+ *
51
+ * `arguments` may be either a JSON string (OpenAI's wire shape) or an
52
+ * already-parsed object (your post-processing). Both work.
53
+ */
54
+ export function parseFunctionCall(raw: unknown): OpenAiFunctionCall {
55
+ if (raw == null || typeof raw !== 'object') {
56
+ throw new TypeError('parseFunctionCall: input is not an object');
57
+ }
58
+ const r = raw as Record<string, unknown>;
59
+
60
+ // Chat Completions shape
61
+ if (r.function && typeof r.function === 'object') {
62
+ const f = r.function as Record<string, unknown>;
63
+ return {
64
+ id: String(r.id ?? ''),
65
+ name: String(f.name ?? ''),
66
+ arguments: parseArgs(f.arguments),
67
+ };
68
+ }
69
+
70
+ // Responses API or already-normalized
71
+ return {
72
+ id: String(r.id ?? r.call_id ?? ''),
73
+ name: String(r.name ?? ''),
74
+ arguments: parseArgs(r.arguments),
75
+ };
76
+ }
77
+
78
+ function parseArgs(args: unknown): Record<string, unknown> {
79
+ if (args == null) return {};
80
+ if (typeof args === 'object') return args as Record<string, unknown>;
81
+ if (typeof args === 'string') {
82
+ try {
83
+ const v = JSON.parse(args);
84
+ if (v && typeof v === 'object') return v as Record<string, unknown>;
85
+ return { _raw: args };
86
+ } catch {
87
+ return { _raw: args };
88
+ }
89
+ }
90
+ return { _raw: String(args) };
91
+ }
92
+
93
+ // ─────────────────────────────────────────────────────────────────────────────
94
+ // Canonicalization
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+
97
+ /**
98
+ * Produce the canonical bytes for an OpenAI function call. The bytes are
99
+ * what the agent-action envelope's `content.hash` is a SHA-256 of.
100
+ *
101
+ * Shape (RFC 8785 canonical JSON, then trailing LF):
102
+ * {
103
+ * "arguments": <canonical JSON of arguments>,
104
+ * "id": <call id>,
105
+ * "name": <function name>
106
+ * }
107
+ */
108
+ export function canonicalizeFunctionCall(c: OpenAiFunctionCall): Uint8Array {
109
+ const canon = {
110
+ arguments: c.arguments,
111
+ id: c.id,
112
+ name: c.name,
113
+ };
114
+ const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);
115
+ return new TextEncoder().encode(str + '\n');
116
+ }
117
+
118
+ export function functionCallHash(c: OpenAiFunctionCall): string {
119
+ return 'sha256:' + hexEncode(sha256(canonicalizeFunctionCall(c)));
120
+ }
121
+
122
+ export function functionCallActionId(
123
+ agentAddress: string,
124
+ signedAt: string,
125
+ delegationId: string,
126
+ scopeExercised: string,
127
+ c: OpenAiFunctionCall
128
+ ): string {
129
+ return computeActionId({
130
+ address: agentAddress,
131
+ content_hash: functionCallHash(c),
132
+ content_length: canonicalizeFunctionCall(c).byteLength,
133
+ content_mime: 'application/vnd.oc-agent.openai-function-call+json',
134
+ signed_at: signedAt,
135
+ delegation_id: delegationId,
136
+ scope_exercised: scopeExercised,
137
+ });
138
+ }
139
+
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+ // Stamping + invocation
142
+ // ─────────────────────────────────────────────────────────────────────────────
143
+
144
+ export interface StampFunctionCallInput {
145
+ agent: SignerRef;
146
+ delegation: DelegationEnvelope;
147
+ /** Either a normalized `OpenAiFunctionCall` or a raw OpenAI tool_call / function_call object. */
148
+ call: OpenAiFunctionCall | unknown;
149
+ /** Defaults to `openai:function(name=<name>)` — the tightest admissible sub-scope. */
150
+ scopeExercised?: string;
151
+ signedAt?: Date;
152
+ }
153
+
154
+ export async function stampFunctionCall(
155
+ input: StampFunctionCallInput
156
+ ): Promise<ActionEnvelope> {
157
+ const fc =
158
+ typeof input.call === 'object' &&
159
+ input.call !== null &&
160
+ 'arguments' in (input.call as Record<string, unknown>) &&
161
+ typeof (input.call as Record<string, unknown>).arguments !== 'string'
162
+ ? (input.call as OpenAiFunctionCall)
163
+ : parseFunctionCall(input.call);
164
+
165
+ const scopeExercised =
166
+ input.scopeExercised ?? `openai:function(name=${fc.name})`;
167
+
168
+ const granted = (input.delegation.scopes ?? []).map(parseScope);
169
+ const exercisedParsed = parseScope(scopeExercised);
170
+ if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
171
+ throw new Error(
172
+ `stampFunctionCall: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
173
+ );
174
+ }
175
+
176
+ const bytes = canonicalizeFunctionCall(fc);
177
+ return signAsAgent({
178
+ agent: input.agent,
179
+ delegation: input.delegation as never,
180
+ content: { hash: functionCallHash(fc), length: bytes.byteLength },
181
+ mime: 'application/vnd.oc-agent.openai-function-call+json',
182
+ scopeExercised,
183
+ signedAt: input.signedAt,
184
+ });
185
+ }
186
+
187
+ export interface InvokeWithStampInput<T> extends StampFunctionCallInput {
188
+ /** Your handler. Receives the normalized OpenAiFunctionCall. */
189
+ execute: (call: OpenAiFunctionCall) => Promise<T>;
190
+ }
191
+
192
+ export interface InvokeWithStampResult<T> {
193
+ result: T;
194
+ action: ActionEnvelope;
195
+ call: OpenAiFunctionCall;
196
+ }
197
+
198
+ export async function invokeWithStamp<T>(
199
+ input: InvokeWithStampInput<T>
200
+ ): Promise<InvokeWithStampResult<T>> {
201
+ const action = await stampFunctionCall(input);
202
+ const fc = parseFunctionCall(input.call);
203
+ const result = await input.execute(fc);
204
+ return { result, action, call: fc };
205
+ }
206
+
207
+ // ─────────────────────────────────────────────────────────────────────────────
208
+ // Console integration: POST stamped actions to console.ochk.io/api/actions
209
+ // ─────────────────────────────────────────────────────────────────────────────
210
+
211
+ export interface ConsoleClient {
212
+ /** Defaults to https://console.ochk.io. */
213
+ baseUrl?: string;
214
+ /** Bearer token from /settings § 03 (`ock_<hex>`). */
215
+ apiToken: string;
216
+ /** Project the action belongs to (proj_*). */
217
+ projectId: string;
218
+ /** Optional fetch override for runtimes that need it. */
219
+ fetch?: typeof fetch;
220
+ }
221
+
222
+ export interface PostActionResult {
223
+ id: string;
224
+ project_id: string;
225
+ delegation_id: string;
226
+ }
227
+
228
+ /**
229
+ * POST a stamped action envelope to console.ochk.io/api/actions. The
230
+ * console re-derives the action id, validates agent-must-match-
231
+ * delegation, persists, fans out to Nostr (kind 30084), submits to
232
+ * OC Stamp, and triggers any subscribed webhooks. Throws on non-2xx
233
+ * with the server's reason string.
234
+ */
235
+ export async function postActionToConsole(
236
+ action: ActionEnvelope,
237
+ client: ConsoleClient
238
+ ): Promise<PostActionResult> {
239
+ const baseUrl = client.baseUrl ?? 'https://console.ochk.io';
240
+ const f = client.fetch ?? fetch;
241
+ const body = {
242
+ project_id: client.projectId,
243
+ delegation_id: action.delegation_id,
244
+ agent_address: action.signer.address,
245
+ scope_exercised: action.scope_exercised,
246
+ content_hash: action.content.hash,
247
+ content_length: action.content.length,
248
+ content_mime: action.content.mime,
249
+ signed_at: action.signed_at,
250
+ signature: action.sig.value,
251
+ id: action.id,
252
+ };
253
+ const r = await f(`${baseUrl}/api/actions`, {
254
+ method: 'POST',
255
+ headers: {
256
+ 'Content-Type': 'application/json',
257
+ Authorization: `Bearer ${client.apiToken}`,
258
+ },
259
+ body: JSON.stringify(body),
260
+ });
261
+ if (!r.ok) {
262
+ let reason = `http_${r.status}`;
263
+ try {
264
+ const j = (await r.json()) as { reason?: string };
265
+ if (j.reason) reason = j.reason;
266
+ } catch {
267
+ // body wasn't json
268
+ }
269
+ throw new Error(`postActionToConsole failed: ${reason}`);
270
+ }
271
+ const j = (await r.json()) as { ok: true; action: PostActionResult };
272
+ return j.action;
273
+ }
274
+
275
+ export interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
276
+ /** Console credentials. If absent, behaves like invokeWithStamp. */
277
+ console?: ConsoleClient;
278
+ }
279
+
280
+ export interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
281
+ /** Set when console is configured AND the POST succeeded. */
282
+ posted: PostActionResult | null;
283
+ }
284
+
285
+ export async function invokeWithStampAndPost<T>(
286
+ input: InvokeWithStampAndPostInput<T>
287
+ ): Promise<InvokeWithStampAndPostResult<T>> {
288
+ const { result, action, call } = await invokeWithStamp(input);
289
+ let posted: PostActionResult | null = null;
290
+ if (input.console) {
291
+ try {
292
+ posted = await postActionToConsole(action, input.console);
293
+ } catch (err) {
294
+ // eslint-disable-next-line no-console
295
+ console.error('[oc-agent-openai] postActionToConsole failed:', err);
296
+ }
297
+ }
298
+ return { result, action, call, posted };
299
+ }