@oked/ai-sdk 0.0.0-stage → 0.6.21

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,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to the Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by the Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding any notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,186 @@
1
- # Temporary Holding Version
1
+ # @oked/ai-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@oked/ai-sdk.svg)](https://www.npmjs.com/package/@oked/ai-sdk)
4
+ [![License: Apache-2.0](https://img.shields.io/npm/l/@oked/ai-sdk.svg)](./LICENSE)
5
+
6
+ OKed for the [Vercel AI SDK](https://ai-sdk.dev) (`ai`). Risky tool calls wait for a person, in Telegram or on the [web dashboard](https://oked.ai/dashboard), before `execute` runs. The iPhone app is coming soon. Questions: [contact@oked.ai](mailto:contact@oked.ai).
7
+
8
+ AI SDK 7 decides this on the call, through `toolApproval`. `okedToolApproval()` waits on `approve()` from [`@oked/sdk`](../sdk) and answers in that same call. A denial or a timeout comes back as `{ type: "denied", reason }`. The AI SDK does not run the tool, and the reason is the `execution-denied` result the model reads.
9
+
10
+ This package does not return `user-approval`. That status ends the generation and leaves the tool runnable until some later request answers it. Forgetting that second step, or answering it in code, would run the tool with no OKed decision.
11
+
12
+ `withOked()` and `okedTools()` wrap `execute` instead. Use them when the gate has to live on the tool object, or on AI SDK 5 and 6 (those versions have `needsApproval`, which is the same two-step pause). Do not set `toolApproval` on a tool you already wrapped. The person would be asked twice.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm install @oked/ai-sdk
18
+ ```
19
+
20
+ `ai` is an optional peer dependency (`>=5.0.0`). You already have it if you are calling `generateText`, `streamText`, or `ToolLoopAgent`. `okedToolApproval()` needs AI SDK 7. The `execute` wrappers also work on AI SDK 5 and 6. Node.js 18 or newer.
21
+
22
+ This package has no pairing command. Provide a key in either place:
23
+
24
+ - `OKED_API_KEY` in the environment, or
25
+ - `~/.oked/config.json` (`{ "apiKey": "ok_..." }`)
26
+
27
+ Create a key from the [dashboard](https://oked.ai/dashboard) after you [sign up](https://oked.ai/signup), or run `oked init --key-only` from [`@oked/claude-code`](../claude-code).
28
+
29
+ Each request sends `source: "ai-sdk"` and `client: "@oked/ai-sdk@<version>"`. Both are part of the receipt subject.
30
+
31
+ ## Quick start
32
+
33
+ Pass `okedToolApproval()` as `toolApproval`. Every tool on that call is classified. `safe` and `warning` run immediately. `review` and `high_stakes` wait. The default wait is 630 seconds, so the backend's 10 minute window is the one that answers. A timeout denies.
34
+
35
+ ```ts
36
+ import { generateText, isStepCount, tool } from "ai";
37
+ import { okedToolApproval } from "@oked/ai-sdk";
38
+ import { z } from "zod";
39
+
40
+ const runCommand = tool({
41
+ description: "Run a shell command",
42
+ inputSchema: z.object({
43
+ command: z.string(),
44
+ }),
45
+ execute: async ({ command }) => {
46
+ // Runs only after OKed has approved a sensitive command.
47
+ return { command, exitCode: 0 };
48
+ },
49
+ });
50
+
51
+ const result = await generateText({
52
+ model, // your AI SDK model
53
+ tools: { runCommand },
54
+ toolApproval: okedToolApproval(),
55
+ stopWhen: isStepCount(5),
56
+ instructions: "When OKed denies a tool, do not retry that action. Ask the user how to proceed.",
57
+ prompt: "Delete the old export in /var/data",
58
+ });
59
+ ```
60
+
61
+ `streamText` takes the same `toolApproval` option. The stream stays open while a person decides, then continues.
62
+
63
+ ## Agent
64
+
65
+ ```ts
66
+ import { ToolLoopAgent, isStepCount } from "ai";
67
+ import { okedToolApproval } from "@oked/ai-sdk";
68
+
69
+ const agent = new ToolLoopAgent({
70
+ model,
71
+ instructions: "When OKed denies a tool, do not retry that action. Ask the user how to proceed.",
72
+ tools: { runCommand },
73
+ toolApproval: okedToolApproval(),
74
+ stopWhen: isStepCount(10),
75
+ });
76
+
77
+ const result = await agent.generate({
78
+ prompt: "Remove /var/data/old-export",
79
+ });
80
+ ```
81
+
82
+ `agent.stream()` uses the same constructor option.
83
+
84
+ ## One tool, or a tools object
85
+
86
+ `okedToolApprovalFor(name)` is the per-tool form. A tool you leave off the map is not gated, so prefer the function above unless you mean to opt out.
87
+
88
+ ```ts
89
+ toolApproval: {
90
+ runCommand: okedToolApprovalFor("runCommand"),
91
+ readFile: "not-applicable",
92
+ }
93
+ ```
94
+
95
+ Wrap `execute` when you want the gate on the tool itself:
96
+
97
+ ```ts
98
+ import { withOked, okedTools } from "@oked/ai-sdk";
99
+
100
+ const runCommand = withOked(
101
+ tool({
102
+ description: "Run a shell command",
103
+ inputSchema: z.object({ command: z.string() }),
104
+ execute: async ({ command }) => ({ command, exitCode: 0 }),
105
+ }),
106
+ { name: "runCommand" },
107
+ );
108
+
109
+ const tools = okedTools({ runCommand, readFile });
110
+ ```
111
+
112
+ `name` is the key you put on the tools object. `withOked` returns a new tool and does not change the original. On approve, `execute` is called with the same input and the same options object, including `toolCallId` and `abortSignal`. On deny, timeout, or error, `execute` is not called.
113
+
114
+ The wrapper returns this object, which the model sees as the tool result:
115
+
116
+ ```ts
117
+ {
118
+ oked: "denied",
119
+ decision: "denied" | "timeout" | "error",
120
+ approvalId: string | null,
121
+ tier: "safe" | "warning" | "review" | "high_stakes",
122
+ reason: string
123
+ }
124
+ ```
125
+
126
+ `isOkedDenial(value)` checks that shape. If the tool defines `outputSchema`, the wrapper throws `OkedDeniedError` instead, with the same reason as `message`, so schema validation cannot replace the reason with a validation error.
127
+
128
+ A tool with no `execute` (a provider-executed tool, or a client-side tool) cannot be stopped from this package. `okedTools` copies it through and logs that it is not gated. `withOked` throws at setup time. The AI SDK's own `toolApproval` has the same limit: it only covers tools the SDK executes locally.
129
+
130
+ ## Configuration
131
+
132
+ Same ideas as the other integrations. Omit these to use the defaults.
133
+
134
+ | Option | Default | What it does |
135
+ |---|---|---|
136
+ | `apiKey` | `OKED_API_KEY`, then `~/.oked/config.json` | Used when you do not pass `client`. |
137
+ | `timeoutMs` | `630000` | How long to wait. A timeout denies. Used when you do not pass `client`. |
138
+ | `minTier` | `"review"` (`OKED_MIN_TIER`) | Lowest tier that waits. `high_stakes` asks only for that tier. |
139
+ | `alwaysApprove` | none | Tool names that always wait, including names the classifier calls safe. |
140
+ | `alwaysAllow` | none | Tool names that always run. Wins over `alwaysApprove`. Exact, case-sensitive. |
141
+ | `strictFailClosed` | `false` (`OKED_STRICT_FAIL_CLOSED=1`) | When true, an unreachable backend denies every sensitive tool. When false, it denies `high_stakes` only. |
142
+ | `cwd` | `process.cwd()` | Recorded on the approval. |
143
+ | `sessionId` | the AI SDK tool call id | Receipt session. Set it to group a whole conversation. |
144
+ | `client` | a new `OKedClient` | Pass an existing client, or a test double. |
145
+ | `log` | stderr, prefixed with `[OKed]` | Receives the message with no prefix. |
146
+
147
+ `safe` and `warning` do not wait, even if `minTier` is lower, unless `alwaysApprove` lists the tool or a saved rule matches. A matching rule sends the call to `approve()` so the rule is applied on the server and the receipt records it. If the rules lookup fails, `safe` and `warning` still run. `review` and `high_stakes` do not depend on that lookup.
148
+
149
+ ```ts
150
+ toolApproval: okedToolApproval({
151
+ minTier: "high_stakes",
152
+ alwaysApprove: ["sendEmail"],
153
+ alwaysAllow: ["readFile"],
154
+ cwd: process.cwd(),
155
+ })
156
+ ```
157
+
158
+ ## Behavior
159
+
160
+ | Situation | What the model gets | Does `execute` run? |
161
+ |---|---|---|
162
+ | `safe`, or `warning` with no matching rule | nothing extra (`toolApproval` returns `undefined`) | yes |
163
+ | Person approves | the tool's own result. The transcript also records `{ type: "approved" }`. | yes |
164
+ | Person denies | `{ type: "denied", reason }` and an `execution-denied` tool result | no |
165
+ | Approval window expires (`timeout`) | the same denial shape. The reason says nobody responded. | no |
166
+ | No API key, invalid key, bad receipt, HTTP error, unexpected error | a denial that tells the model not to retry | no |
167
+ | Backend unreachable, `high_stakes` or `strictFailClosed` | a denial | no |
168
+ | Backend unreachable, lower sensitive tier, strict mode off | the tool runs. Stderr says it was allowed degraded. | yes |
169
+
170
+ An explicit denial is final. A timeout is a denial. Nothing in those paths fails open.
171
+
172
+ Add a line to your instructions: "When OKed denies a tool, do not retry that action." The reason already says not to retry. Models still retry a bare tool error more often than a sentence that tells them to stop.
173
+
174
+ ## Timeouts
175
+
176
+ The client waits 630 seconds (`DEFAULT_APPROVAL_TIMEOUT_MS`). That is 30 seconds past the 10 minute window, so the backend's `timeout` answer is the one that comes back, and the tool is denied.
177
+
178
+ Set the server's max duration, and any AI SDK `timeout` or `timeout.stepMs`, above that wait. Those budgets include the tool step. A platform that kills the process during the wait cannot deliver the denial, because the process is gone. `timeoutMs` on this package shortens the OKed wait itself. That shorter wait still denies. It does not let the tool run.
179
+
180
+ ## Receipts
181
+
182
+ `approve()` checks the signed receipt before an approval counts. A receipt that does not verify denies the tool. An approval with no receipt denies too, which is the default since 0.6.0. A deny or a timeout that arrives without a receipt stays a deny. See [docs/receipts.md](../../docs/receipts.md).
183
+
184
+ ## License
185
+
186
+ [Apache-2.0](./LICENSE)
package/dist/gate.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { OkedAiOptions, OkedGateResult } from "./types.js";
2
+ export { unexpectedMessage } from "./messages.js";
3
+ export interface GateCall {
4
+ toolName: string;
5
+ input: unknown;
6
+ toolCallId?: string;
7
+ }
8
+ /**
9
+ * Classify one call and, when the tier needs a person, wait on `approve()`.
10
+ * Safe and warning calls return a local allow unless a rule matches or
11
+ * `alwaysApprove` lists the tool. The returned denial is final: callers must
12
+ * not run the tool.
13
+ */
14
+ export declare function createGate(options?: OkedAiOptions): (call: GateCall) => Promise<OkedGateResult>;
package/dist/gate.js ADDED
@@ -0,0 +1,219 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { OKedClient, OKedAuthError, OKedBackendUnreachableError, OKedHttpError, OKedReceiptError, OKedTooManyPendingError, TIER_ORDER, applyRules, classify, degradedDecision, describe, describeFields, lookupPrTitle, } from "@oked/sdk";
5
+ import { approvedMessage, authMessage, classifierMessage, deniedMessage, httpMessage, malformedMessage, missingNameMessage, noKeyMessage, pendingMessage, receiptMessage, timeoutMessage, unexpectedMessage, unreachableMessage, } from "./messages.js";
6
+ export { unexpectedMessage } from "./messages.js";
7
+ const APPROVAL_TIERS = new Set(["review", "high_stakes"]);
8
+ const CLIENT_ID = (() => {
9
+ try {
10
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
11
+ return `@oked/ai-sdk@${JSON.parse(readFileSync(pkgPath, "utf-8")).version}`;
12
+ }
13
+ catch {
14
+ return "@oked/ai-sdk";
15
+ }
16
+ })();
17
+ function defaultLog(message) {
18
+ process.stderr.write(`[OKed] ${message}\n`);
19
+ }
20
+ function asToolInput(input) {
21
+ if (input !== null && typeof input === "object" && !Array.isArray(input)) {
22
+ return input;
23
+ }
24
+ if (input === undefined)
25
+ return {};
26
+ return { value: input };
27
+ }
28
+ function resolveMinTier(options) {
29
+ if (options.minTier && options.minTier in TIER_ORDER)
30
+ return options.minTier;
31
+ const env = process.env.OKED_MIN_TIER;
32
+ if (env && env in TIER_ORDER)
33
+ return env;
34
+ return "review";
35
+ }
36
+ function isStrict(options, client) {
37
+ if (typeof options.strictFailClosed === "boolean")
38
+ return options.strictFailClosed;
39
+ return client.strictFailClosed === true;
40
+ }
41
+ function resolveClient(options) {
42
+ if (options.client)
43
+ return options.client;
44
+ const timeout = typeof options.timeoutMs === "number" &&
45
+ Number.isFinite(options.timeoutMs) &&
46
+ options.timeoutMs > 0
47
+ ? options.timeoutMs
48
+ : undefined;
49
+ return new OKedClient({
50
+ ...(options.apiKey ? { apiKey: options.apiKey } : {}),
51
+ ...(timeout !== undefined ? { timeout } : {}),
52
+ ...(typeof options.strictFailClosed === "boolean"
53
+ ? { strictFailClosed: options.strictFailClosed }
54
+ : {}),
55
+ client: CLIENT_ID,
56
+ source: options.source ?? "ai-sdk",
57
+ });
58
+ }
59
+ function deny(decision, approvalId, reason, tier) {
60
+ return { action: "deny", decision, approvalId, reason, tier };
61
+ }
62
+ function localAllow() {
63
+ return { action: "allow", recorded: false };
64
+ }
65
+ function approvalIdOf(result) {
66
+ return typeof result.approval_id === "string" ? result.approval_id : null;
67
+ }
68
+ function mapApproval(result, tier) {
69
+ if (!result || typeof result.approved !== "boolean") {
70
+ return deny("error", null, malformedMessage(), tier);
71
+ }
72
+ const approvalId = approvalIdOf(result);
73
+ if (result.approved) {
74
+ return { action: "allow", recorded: true, reason: approvedMessage(approvalId) };
75
+ }
76
+ if (result.decision === "timeout") {
77
+ return deny("timeout", approvalId, timeoutMessage(approvalId), tier);
78
+ }
79
+ return deny("denied", approvalId, deniedMessage(approvalId), tier);
80
+ }
81
+ function mapError(err, tier, strict) {
82
+ if (err instanceof OKedReceiptError) {
83
+ return deny("error", null, receiptMessage(err.problems), tier);
84
+ }
85
+ if (err instanceof OKedAuthError) {
86
+ return deny("error", null, authMessage(), tier);
87
+ }
88
+ if (err instanceof OKedTooManyPendingError) {
89
+ return deny("error", null, pendingMessage(err.message), tier);
90
+ }
91
+ if (err instanceof OKedHttpError) {
92
+ return deny("error", null, httpMessage(err.status, err.detail), tier);
93
+ }
94
+ if (err instanceof OKedBackendUnreachableError) {
95
+ const decision = degradedDecision(tier, { strictFailClosed: strict });
96
+ if (decision === "allow")
97
+ return localAllow();
98
+ const why = strict ? "strict fail-closed" : "high-stakes";
99
+ return deny("error", null, unreachableMessage(tier, why), tier);
100
+ }
101
+ return deny("error", null, unexpectedMessage(err), tier);
102
+ }
103
+ async function rulesWantBackend(client, tier, toolName, toolInput, cwd) {
104
+ if (!client.apiKey || !client.getRules)
105
+ return false;
106
+ try {
107
+ const rules = await client.getRules();
108
+ if (!Array.isArray(rules) || rules.length === 0)
109
+ return false;
110
+ const fields = describeFields(toolName, toolInput, { cwd }) ?? {};
111
+ return Boolean(applyRules({ tier, fields }, rules, { cwd }).appliedRuleId);
112
+ }
113
+ catch {
114
+ // A failed rules lookup must not block a safe or warning call.
115
+ return false;
116
+ }
117
+ }
118
+ function summaryOf(toolName, toolInput) {
119
+ const raw = toolInput.command ?? toolInput.cmd ?? toolInput.file_path ?? toolInput.path ?? toolName;
120
+ return typeof raw === "string" && raw ? raw : toolName;
121
+ }
122
+ /**
123
+ * Classify one call and, when the tier needs a person, wait on `approve()`.
124
+ * Safe and warning calls return a local allow unless a rule matches or
125
+ * `alwaysApprove` lists the tool. The returned denial is final: callers must
126
+ * not run the tool.
127
+ */
128
+ export function createGate(options = {}) {
129
+ const client = resolveClient(options);
130
+ const log = options.log ?? defaultLog;
131
+ const cwd = options.cwd ?? process.cwd();
132
+ return async (call) => {
133
+ const toolName = typeof call.toolName === "string" ? call.toolName.trim() : "";
134
+ if (!toolName) {
135
+ return deny("error", null, missingNameMessage(), "review");
136
+ }
137
+ if (client.apiKey && client.heartbeat) {
138
+ try {
139
+ await client.heartbeat();
140
+ }
141
+ catch {
142
+ // Presence only. A failure must not change the decision.
143
+ }
144
+ }
145
+ const toolInput = asToolInput(call.input);
146
+ const sessionId = options.sessionId || call.toolCallId || undefined;
147
+ if (options.alwaysAllow?.includes(toolName)) {
148
+ return localAllow();
149
+ }
150
+ let tier;
151
+ try {
152
+ tier = classify(toolName, toolInput);
153
+ }
154
+ catch (err) {
155
+ log(`Classifier error on ${toolName}: ${err instanceof Error ? err.message : String(err)}`);
156
+ return deny("error", null, classifierMessage(), "review");
157
+ }
158
+ let forceAsk = false;
159
+ if (options.alwaysApprove?.includes(toolName)) {
160
+ if (TIER_ORDER[tier] < TIER_ORDER.review)
161
+ tier = "review";
162
+ forceAsk = true;
163
+ }
164
+ if (!forceAsk && (tier === "safe" || tier === "warning")) {
165
+ const matched = await rulesWantBackend(client, tier, toolName, toolInput, cwd);
166
+ if (!matched) {
167
+ if (tier === "warning") {
168
+ log(`WARNING ${toolName} ${summaryOf(toolName, toolInput)} allowed (inside project)`);
169
+ }
170
+ return localAllow();
171
+ }
172
+ forceAsk = true;
173
+ }
174
+ const minTier = resolveMinTier(options);
175
+ if (!forceAsk && (TIER_ORDER[tier] < TIER_ORDER[minTier] || !APPROVAL_TIERS.has(tier))) {
176
+ return localAllow();
177
+ }
178
+ if (!client.apiKey) {
179
+ log(`No API key. Denying ${toolName}.`);
180
+ return deny("error", null, noKeyMessage(), tier);
181
+ }
182
+ let fields = describeFields(toolName, toolInput, { cwd }) ?? undefined;
183
+ const prTitle = await lookupPrTitle(fields);
184
+ if (prTitle)
185
+ fields = describeFields(toolName, toolInput, { cwd, prTitle }) ?? undefined;
186
+ const description = describe(toolName, toolInput, prTitle ? { cwd, prTitle } : { cwd });
187
+ log(`Requesting approval for ${toolName} (${tier}). Check Telegram or the dashboard.`);
188
+ const strict = isStrict(options, client);
189
+ let outcome;
190
+ try {
191
+ const result = await client.approve({
192
+ action: toolName,
193
+ description,
194
+ tier,
195
+ fields,
196
+ tool_input: toolInput,
197
+ ...(sessionId ? { session_id: sessionId } : {}),
198
+ cwd,
199
+ });
200
+ outcome = mapApproval(result, tier);
201
+ }
202
+ catch (err) {
203
+ if (err instanceof OKedBackendUnreachableError) {
204
+ const decision = degradedDecision(tier, { strictFailClosed: strict });
205
+ if (decision === "allow") {
206
+ log(`Backend unreachable. ${toolName} (${tier}) allowed (degraded, non-high-stakes).`);
207
+ }
208
+ }
209
+ outcome = mapError(err, tier, strict);
210
+ }
211
+ if (outcome.action === "allow" && outcome.recorded) {
212
+ log(`${outcome.reason} for ${toolName}`);
213
+ }
214
+ else if (outcome.action === "deny") {
215
+ log(`Blocking ${toolName}: ${outcome.reason}`);
216
+ }
217
+ return outcome;
218
+ };
219
+ }
@@ -0,0 +1,71 @@
1
+ import type { OkedAiOptions, OkedDenial, OkedSingleToolApproval, OkedToolApprovalFunction, ToolExecute } from "./types.js";
2
+ import type { RiskTier } from "@oked/sdk";
3
+ export type { OkedAiOptions, OkedDenial, OkedGateClient, OkedGateResult, OkedSingleToolApproval, OkedToolApprovalArgs, OkedToolApprovalFunction, OkedToolApprovalResult, OkedToolCall, } from "./types.js";
4
+ export type { RiskTier } from "@oked/sdk";
5
+ /**
6
+ * Thrown when a wrapped tool has an `outputSchema` and the call is denied.
7
+ * The AI SDK turns the error into a tool error, so the schema does not
8
+ * replace the reason with a validation failure. `message` is the reason
9
+ * the model should read.
10
+ */
11
+ export declare class OkedDeniedError extends Error {
12
+ readonly oked: "denied";
13
+ readonly decision: OkedDenial["decision"];
14
+ readonly approvalId: string | null;
15
+ readonly reason: string;
16
+ readonly tier: RiskTier;
17
+ constructor(denial: OkedDenial);
18
+ }
19
+ export declare function isOkedDenial(value: unknown): value is OkedDenial;
20
+ /**
21
+ * `toolApproval` callback for every tool on a `generateText`, `streamText`,
22
+ * or `ToolLoopAgent` call.
23
+ *
24
+ * ```ts
25
+ * await generateText({
26
+ * model,
27
+ * tools,
28
+ * toolApproval: okedToolApproval(),
29
+ * prompt,
30
+ * });
31
+ * ```
32
+ *
33
+ * A sensitive call waits for a person. Approve returns `{ type: "approved" }`
34
+ * and the AI SDK runs `execute`. Deny or timeout returns `{ type: "denied", reason }`
35
+ * and `execute` does not run. `safe` and `warning` return `undefined` so the
36
+ * tool runs with no approval metadata, unless a rule or `alwaysApprove` says
37
+ * otherwise.
38
+ */
39
+ export declare function okedToolApproval(options?: OkedAiOptions): OkedToolApprovalFunction;
40
+ /**
41
+ * Per-tool `toolApproval` entry. Tools you leave off the map are not gated.
42
+ * Prefer {@link okedToolApproval} so every tool is classified.
43
+ *
44
+ * ```ts
45
+ * toolApproval: { runCommand: okedToolApprovalFor("runCommand") }
46
+ * ```
47
+ */
48
+ export declare function okedToolApprovalFor(toolName: string, options?: OkedAiOptions): OkedSingleToolApproval;
49
+ /**
50
+ * Wrap one tool's `execute`. The original tool is not modified.
51
+ * `name` is the tool name the model uses (the key in your tools object).
52
+ *
53
+ * On approve, `execute` is called with the same input and the same options
54
+ * object. On deny, timeout, or error, `execute` is not called. The return
55
+ * value is an {@link OkedDenial} unless the tool defines `outputSchema`, in
56
+ * which case {@link OkedDeniedError} is thrown so schema validation cannot
57
+ * hide the reason.
58
+ */
59
+ export declare function withOked<T extends {
60
+ execute?: ToolExecute;
61
+ }>(toolDef: T, options: OkedAiOptions & {
62
+ name: string;
63
+ }): T;
64
+ /**
65
+ * Wrap every tool in a tools object that has `execute`. Tools without
66
+ * `execute` are copied through and a log line says they are not gated.
67
+ * One client is shared across the set.
68
+ */
69
+ export declare function okedTools<T extends Record<string, {
70
+ execute?: ToolExecute;
71
+ } | undefined>>(tools: T, options?: OkedAiOptions): T;
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * @oked/ai-sdk. OKed for the Vercel AI SDK.
3
+ *
4
+ * AI SDK 7 asks for approval through `toolApproval` on `generateText`,
5
+ * `streamText`, and `ToolLoopAgent`. `okedToolApproval()` awaits
6
+ * `OKedClient.approve()` and returns `approved` or `denied` in that same
7
+ * call. A denial or a timeout is `{ type: "denied", reason }`. The AI SDK
8
+ * then skips `execute` and shows the reason to the model.
9
+ *
10
+ * `user-approval` is intentionally unused. That status ends the generation
11
+ * and leaves the tool runnable until some later call answers it.
12
+ *
13
+ * `withOked` and `okedTools` wrap `execute` for one tool or a tools object.
14
+ * Use those when the gate has to travel with the tool, or on AI SDK 5 and 6,
15
+ * which have no `toolApproval`. Do not also set `toolApproval` on a wrapped
16
+ * tool: the person would be asked twice.
17
+ *
18
+ * Denied, timed out, and failed approvals do not run the tool.
19
+ */
20
+ import { createGate, unexpectedMessage } from "./gate.js";
21
+ const TIERS = new Set(["safe", "warning", "review", "high_stakes"]);
22
+ /**
23
+ * Thrown when a wrapped tool has an `outputSchema` and the call is denied.
24
+ * The AI SDK turns the error into a tool error, so the schema does not
25
+ * replace the reason with a validation failure. `message` is the reason
26
+ * the model should read.
27
+ */
28
+ export class OkedDeniedError extends Error {
29
+ oked = "denied";
30
+ decision;
31
+ approvalId;
32
+ reason;
33
+ tier;
34
+ constructor(denial) {
35
+ super(denial.reason);
36
+ this.name = "OkedDeniedError";
37
+ this.decision = denial.decision;
38
+ this.approvalId = denial.approvalId;
39
+ this.reason = denial.reason;
40
+ this.tier = denial.tier;
41
+ }
42
+ }
43
+ export function isOkedDenial(value) {
44
+ if (!value || typeof value !== "object")
45
+ return false;
46
+ const v = value;
47
+ return (v.oked === "denied" &&
48
+ (v.decision === "denied" || v.decision === "timeout" || v.decision === "error") &&
49
+ typeof v.reason === "string" &&
50
+ (typeof v.approvalId === "string" || v.approvalId === null) &&
51
+ typeof v.tier === "string" &&
52
+ TIERS.has(v.tier));
53
+ }
54
+ function toDenial(result) {
55
+ return {
56
+ oked: "denied",
57
+ decision: result.decision,
58
+ approvalId: result.approvalId,
59
+ reason: result.reason,
60
+ tier: result.tier,
61
+ };
62
+ }
63
+ /** Map a gate result onto an AI SDK 7 `toolApproval` status. */
64
+ function toToolApprovalStatus(result) {
65
+ if (result.action === "deny")
66
+ return { type: "denied", reason: result.reason };
67
+ if (result.recorded)
68
+ return { type: "approved", reason: result.reason };
69
+ return undefined;
70
+ }
71
+ function readInput(toolCall) {
72
+ if (toolCall.input !== undefined)
73
+ return toolCall.input;
74
+ return toolCall.args;
75
+ }
76
+ async function gated(gate, call) {
77
+ try {
78
+ return await gate(call);
79
+ }
80
+ catch (err) {
81
+ return {
82
+ action: "deny",
83
+ decision: "error",
84
+ approvalId: null,
85
+ reason: unexpectedMessage(err),
86
+ tier: "review",
87
+ };
88
+ }
89
+ }
90
+ /**
91
+ * `toolApproval` callback for every tool on a `generateText`, `streamText`,
92
+ * or `ToolLoopAgent` call.
93
+ *
94
+ * ```ts
95
+ * await generateText({
96
+ * model,
97
+ * tools,
98
+ * toolApproval: okedToolApproval(),
99
+ * prompt,
100
+ * });
101
+ * ```
102
+ *
103
+ * A sensitive call waits for a person. Approve returns `{ type: "approved" }`
104
+ * and the AI SDK runs `execute`. Deny or timeout returns `{ type: "denied", reason }`
105
+ * and `execute` does not run. `safe` and `warning` return `undefined` so the
106
+ * tool runs with no approval metadata, unless a rule or `alwaysApprove` says
107
+ * otherwise.
108
+ */
109
+ export function okedToolApproval(options = {}) {
110
+ const gate = createGate(options);
111
+ return async (args = {}) => {
112
+ const toolCall = args.toolCall;
113
+ const result = await gated(gate, {
114
+ toolName: typeof toolCall?.toolName === "string" ? toolCall.toolName : "",
115
+ input: toolCall ? readInput(toolCall) : undefined,
116
+ toolCallId: typeof toolCall?.toolCallId === "string" ? toolCall.toolCallId : undefined,
117
+ });
118
+ return toToolApprovalStatus(result);
119
+ };
120
+ }
121
+ /**
122
+ * Per-tool `toolApproval` entry. Tools you leave off the map are not gated.
123
+ * Prefer {@link okedToolApproval} so every tool is classified.
124
+ *
125
+ * ```ts
126
+ * toolApproval: { runCommand: okedToolApprovalFor("runCommand") }
127
+ * ```
128
+ */
129
+ export function okedToolApprovalFor(toolName, options = {}) {
130
+ const name = toolName.trim();
131
+ const gate = createGate(options);
132
+ return async (input, execOptions) => {
133
+ const result = await gated(gate, {
134
+ toolName: name,
135
+ input,
136
+ toolCallId: typeof execOptions?.toolCallId === "string" ? execOptions.toolCallId : undefined,
137
+ });
138
+ return toToolApprovalStatus(result);
139
+ };
140
+ }
141
+ function hasOutputSchema(toolDef) {
142
+ return "outputSchema" in toolDef && toolDef.outputSchema != null;
143
+ }
144
+ function wrapExecute(toolDef, name, gate) {
145
+ const execute = toolDef.execute;
146
+ if (typeof execute !== "function") {
147
+ throw new Error(`OKed: tool "${name}" has no execute function, so a wrapper cannot stop it. ` +
148
+ "Provider-executed tools are not gated. Use okedToolApproval() for tools the AI SDK executes locally.");
149
+ }
150
+ const validateOutput = hasOutputSchema(toolDef);
151
+ const copy = { ...toolDef };
152
+ delete copy.needsApproval;
153
+ copy.execute = async (input, execOptions) => {
154
+ const toolCallId = execOptions && typeof execOptions === "object"
155
+ ? execOptions.toolCallId
156
+ : undefined;
157
+ const result = await gated(gate, {
158
+ toolName: name,
159
+ input,
160
+ toolCallId: typeof toolCallId === "string" ? toolCallId : undefined,
161
+ });
162
+ if (result.action === "allow") {
163
+ return await execute.call(toolDef, input, execOptions);
164
+ }
165
+ const denial = toDenial(result);
166
+ if (validateOutput)
167
+ throw new OkedDeniedError(denial);
168
+ return denial;
169
+ };
170
+ return copy;
171
+ }
172
+ /**
173
+ * Wrap one tool's `execute`. The original tool is not modified.
174
+ * `name` is the tool name the model uses (the key in your tools object).
175
+ *
176
+ * On approve, `execute` is called with the same input and the same options
177
+ * object. On deny, timeout, or error, `execute` is not called. The return
178
+ * value is an {@link OkedDenial} unless the tool defines `outputSchema`, in
179
+ * which case {@link OkedDeniedError} is thrown so schema validation cannot
180
+ * hide the reason.
181
+ */
182
+ export function withOked(toolDef, options) {
183
+ const name = options.name?.trim() ?? "";
184
+ if (!name)
185
+ throw new Error("OKed: withOked requires a tool name.");
186
+ if (!toolDef || typeof toolDef.execute !== "function") {
187
+ throw new Error(`OKed: tool "${name}" has no execute function, so a wrapper cannot stop it. ` +
188
+ "Provider-executed tools are not gated. Use okedToolApproval() for tools the AI SDK executes locally.");
189
+ }
190
+ return wrapExecute(toolDef, name, createGate(options));
191
+ }
192
+ /**
193
+ * Wrap every tool in a tools object that has `execute`. Tools without
194
+ * `execute` are copied through and a log line says they are not gated.
195
+ * One client is shared across the set.
196
+ */
197
+ export function okedTools(tools, options = {}) {
198
+ const gate = createGate(options);
199
+ const log = options.log ?? ((message) => process.stderr.write(`[OKed] ${message}\n`));
200
+ const out = {};
201
+ for (const name of Object.keys(tools)) {
202
+ const toolDef = tools[name];
203
+ if (!toolDef || typeof toolDef !== "object" || typeof toolDef.execute !== "function") {
204
+ log(`tool "${name}" has no execute function, so it is not gated. ` +
205
+ "Provider-executed tools run on the provider and OKed cannot stop them.");
206
+ out[name] = toolDef;
207
+ continue;
208
+ }
209
+ out[name] = wrapExecute(toolDef, name, gate);
210
+ }
211
+ return out;
212
+ }
@@ -0,0 +1,14 @@
1
+ export declare function idSuffix(id: string | null | undefined): string;
2
+ export declare function deniedMessage(id: string | null): string;
3
+ export declare function timeoutMessage(id: string | null): string;
4
+ export declare function noKeyMessage(): string;
5
+ export declare function authMessage(): string;
6
+ export declare function receiptMessage(problems: readonly string[]): string;
7
+ export declare function pendingMessage(detail: string): string;
8
+ export declare function httpMessage(status: number, detail: string): string;
9
+ export declare function unreachableMessage(tier: string, why: string): string;
10
+ export declare function unexpectedMessage(err: unknown): string;
11
+ export declare function malformedMessage(): string;
12
+ export declare function classifierMessage(): string;
13
+ export declare function missingNameMessage(): string;
14
+ export declare function approvedMessage(id: string | null): string;
@@ -0,0 +1,53 @@
1
+ const STOP = "Do NOT retry the same action. Stop and ask the user what to do instead.";
2
+ export function idSuffix(id) {
3
+ return id ? ` (approval ${id})` : "";
4
+ }
5
+ export function deniedMessage(id) {
6
+ return `USER DENIED this action via OKed${idSuffix(id)}. This is a final human decision. ${STOP}`;
7
+ }
8
+ export function timeoutMessage(id) {
9
+ return (`USER did not respond to this action via OKed${idSuffix(id)}. ` +
10
+ `The approval window expired, so the action was not run. ${STOP}`);
11
+ }
12
+ export function noKeyMessage() {
13
+ return ("OKed: no API key is configured, so this action was not run. " +
14
+ "Set OKED_API_KEY, or save an apiKey in ~/.oked/config.json. " +
15
+ "Create a key from the dashboard at https://oked.ai/dashboard, " +
16
+ "or run `oked init --key-only` from @oked/claude-code. " +
17
+ STOP);
18
+ }
19
+ export function authMessage() {
20
+ return ("OKed: invalid API key (configuration error, not a transient failure). " +
21
+ STOP);
22
+ }
23
+ export function receiptMessage(problems) {
24
+ const detail = problems.length > 0 ? problems.join(", ") : "invalid";
25
+ return (`OKed: the approval receipt did not verify (${detail}), so this action was not run. ` +
26
+ STOP);
27
+ }
28
+ export function pendingMessage(detail) {
29
+ return `${detail} ${STOP}`;
30
+ }
31
+ export function httpMessage(status, detail) {
32
+ const extra = detail ? `: ${detail}` : "";
33
+ return `OKed: backend error HTTP ${status}${extra} (fail-safe deny). ${STOP}`;
34
+ }
35
+ export function unreachableMessage(tier, why) {
36
+ return (`OKed backend unreachable. This ${tier} action was not run (${why}, fail-safe). ${STOP}`);
37
+ }
38
+ export function unexpectedMessage(err) {
39
+ const msg = err instanceof Error ? err.message : String(err);
40
+ return `OKed: unexpected error (fail-safe deny): ${msg}. ${STOP}`;
41
+ }
42
+ export function malformedMessage() {
43
+ return `OKed: the approval response was malformed, so this action was not run. ${STOP}`;
44
+ }
45
+ export function classifierMessage() {
46
+ return `OKed: the classifier failed, so this action was not run (fail-safe deny). ${STOP}`;
47
+ }
48
+ export function missingNameMessage() {
49
+ return `OKed: the tool call had no name, so it was not run. ${STOP}`;
50
+ }
51
+ export function approvedMessage(id) {
52
+ return `Approved via OKed${idSuffix(id)}`;
53
+ }
@@ -0,0 +1,117 @@
1
+ import type { ApprovalRequest, ApprovalResponse, RiskTier, Rule } from "@oked/sdk";
2
+ /**
3
+ * Enough of `OKedClient` for the gate. Pass `new OKedClient()` or a test double.
4
+ * `apiKey` and `timeoutMs` on {@link OkedAiOptions} apply only when `client` is omitted.
5
+ */
6
+ export interface OkedGateClient {
7
+ readonly apiKey: string;
8
+ readonly strictFailClosed?: boolean;
9
+ approve(request: ApprovalRequest): Promise<ApprovalResponse>;
10
+ getRules?(): Promise<Rule[]>;
11
+ heartbeat?(): Promise<void>;
12
+ }
13
+ export interface OkedAiOptions {
14
+ /**
15
+ * Client to call. When omitted, a new `OKedClient` is created from
16
+ * `OKED_API_KEY` or `~/.oked/config.json`.
17
+ */
18
+ client?: OkedGateClient;
19
+ /** Used only when `client` is omitted. */
20
+ apiKey?: string;
21
+ /**
22
+ * How long to wait for a person, in milliseconds. Used only when `client`
23
+ * is omitted. Default is the SDK wait, 630 seconds. A timeout denies.
24
+ */
25
+ timeoutMs?: number;
26
+ /**
27
+ * When true, an unreachable backend denies every sensitive tool. When false,
28
+ * it denies `high_stakes` only. Overrides `client.strictFailClosed` when set.
29
+ * Env: `OKED_STRICT_FAIL_CLOSED=1`.
30
+ */
31
+ strictFailClosed?: boolean;
32
+ /**
33
+ * Lowest tier that waits for a person. Default `review` (env `OKED_MIN_TIER`).
34
+ * `safe` and `warning` still run locally unless `alwaysApprove` lists the
35
+ * tool or a saved rule matches.
36
+ */
37
+ minTier?: RiskTier;
38
+ /** Tool names that always wait for a person, whatever the classifier says. */
39
+ alwaysApprove?: readonly string[];
40
+ /** Tool names that always run, with no approval request. Wins over `alwaysApprove`. */
41
+ alwaysAllow?: readonly string[];
42
+ /** Working directory recorded on the approval. Default `process.cwd()`. */
43
+ cwd?: string;
44
+ /**
45
+ * Receipt session id. When omitted, the AI SDK tool call id is used so
46
+ * each call can be told apart.
47
+ */
48
+ sessionId?: string;
49
+ /**
50
+ * Receipt subject source. Default `ai-sdk`. Used only when `client` is omitted.
51
+ */
52
+ source?: string;
53
+ /** Line logger. Default writes `[OKed] ...` to stderr. The message has no prefix. */
54
+ log?: (message: string) => void;
55
+ }
56
+ /** What the gate decided. Local allows are not recorded as approvals. */
57
+ export type OkedGateResult = {
58
+ action: "allow";
59
+ recorded: false;
60
+ } | {
61
+ action: "allow";
62
+ recorded: true;
63
+ reason: string;
64
+ } | {
65
+ action: "deny";
66
+ decision: "denied" | "timeout" | "error";
67
+ approvalId: string | null;
68
+ reason: string;
69
+ tier: RiskTier;
70
+ };
71
+ /**
72
+ * Value returned from a wrapped `execute` when the tool did not run.
73
+ * `reason` is the text to show the model.
74
+ */
75
+ export interface OkedDenial {
76
+ oked: "denied";
77
+ decision: "denied" | "timeout" | "error";
78
+ approvalId: string | null;
79
+ reason: string;
80
+ tier: RiskTier;
81
+ }
82
+ /**
83
+ * Subset of an AI SDK tool call. AI SDK 7 uses `input`. `args` is accepted
84
+ * when `input` is missing.
85
+ */
86
+ export interface OkedToolCall {
87
+ toolName: string;
88
+ input?: unknown;
89
+ args?: unknown;
90
+ toolCallId?: string;
91
+ dynamic?: boolean;
92
+ }
93
+ /**
94
+ * Arguments of an AI SDK 7 `GenericToolApprovalFunction`. Extra fields the
95
+ * SDK passes (`tools`, `messages`, `runtimeContext`) are ignored.
96
+ */
97
+ export interface OkedToolApprovalArgs {
98
+ toolCall?: OkedToolCall;
99
+ }
100
+ /**
101
+ * Status object AI SDK 7 accepts from `toolApproval`. This package never
102
+ * returns `user-approval`: that ends the generation and leaves the tool
103
+ * runnable on the next call.
104
+ */
105
+ export type OkedToolApprovalResult = undefined | {
106
+ type: "approved";
107
+ reason?: string;
108
+ } | {
109
+ type: "denied";
110
+ reason: string;
111
+ };
112
+ export type OkedToolApprovalFunction = (args: OkedToolApprovalArgs) => Promise<OkedToolApprovalResult>;
113
+ /** Per-tool `toolApproval` callback. The tool name is bound by the caller. */
114
+ export type OkedSingleToolApproval = (input: unknown, options?: {
115
+ toolCallId?: string;
116
+ }) => Promise<OkedToolApprovalResult>;
117
+ export type ToolExecute = (input: unknown, options?: unknown) => unknown;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@oked/ai-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.21",
4
+ "description": "OKed for the Vercel AI SDK. Risky tool calls wait for a person in Telegram or on the web dashboard before they run.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "prebuild": "npm run build --workspace=@oked/sdk",
21
+ "build": "tsc",
22
+ "dev": "tsc --watch",
23
+ "test": "npm run build && node --test src/gate.test.mjs",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "peerDependencies": {
27
+ "ai": ">=5.0.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "ai": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@oked/sdk": "0.6.21"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.13.10",
39
+ "typescript": "^5.6.0"
40
+ },
41
+ "keywords": [
42
+ "oked",
43
+ "ai-agent",
44
+ "approval",
45
+ "human-in-the-loop",
46
+ "hooks",
47
+ "ai-sdk",
48
+ "vercel",
49
+ "tool-calling"
50
+ ],
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/oked-ai/oked-sdk.git",
54
+ "directory": "packages/ai-sdk"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/oked-ai/oked-sdk/issues"
58
+ },
59
+ "homepage": "https://oked.ai",
60
+ "license": "Apache-2.0",
61
+ "engines": {
62
+ "node": ">=18"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }