@decidio/sdk 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,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @decidio/sdk
2
+
3
+ One line to put a human-or-policy approval gate in front of any AI-agent action — and get a signed, verifiable Authority Receipt for every outcome.
4
+
5
+ ## Quickstart — seven steps to your first sealed receipt
6
+
7
+ Work through these in order; a competent engineer lands the whole loop in under 30 minutes.
8
+
9
+ **1. Install**
10
+
11
+ ```bash
12
+ npm install @decidio/sdk
13
+ ```
14
+
15
+ **2. Register your agent**
16
+
17
+ ```bash
18
+ npx @decidio/sdk init my-agent
19
+ ```
20
+
21
+ `init` signs you into your Decidio workspace (email + password, or paste the sign-in link from
22
+ your workspace email), generates the agent's Ed25519 keypair **locally** (only the public
23
+ `did:key` is sent), registers the agent, mints its **floor-limited API token** (it can call the
24
+ agent gate and nothing else — your workspace session never touches the agent's disk), and writes
25
+ `.env`. No workspace yet? Request access at [decidioai.com](https://decidioai.com/#early-access).
26
+
27
+ **3. Protect one function** — rename the raw function, wrap it under the original name:
28
+
29
+ ```ts
30
+ import { guard } from "@decidio/sdk";
31
+
32
+ const createOpportunityRaw = (o) => sf.create("Opportunity", o);
33
+
34
+ const createOpportunity = guard.protect(
35
+ createOpportunityRaw,
36
+ (o) => ({ action: "createOpportunity", amount: o.Amount, scope: "Opportunity" }),
37
+ );
38
+ ```
39
+
40
+ **4. Trigger a request:**
41
+
42
+ ```ts
43
+ await createOpportunity({ Amount: 86_000 });
44
+ ```
45
+
46
+ A brand-new agent matches **no** auto-approve rule, so Decidio's deny-by-default **routes every
47
+ request to a human** — that is the point: nothing executes without either a named policy rule or
48
+ a person. In durable mode the call parks its arguments agent-side and throws
49
+ `DecidioSuspendedError`; the process may exit. (To see a policy `proceed`, register the agent in
50
+ the sandbox's pre-policied lane — `DECIDIO_SOURCE_SYSTEM=salesforce npx @decidio/sdk init` — which
51
+ auto-approves small amounts under a named, versioned rule when the request carries the agent's
52
+ signed identity proof.)
53
+
54
+ **5. Approve it** — the request is now in your Decidio queue (web or phone), or from the terminal:
55
+
56
+ ```bash
57
+ npx @decidio/sdk approvals # list pending
58
+ npx @decidio/sdk approvals approve <decisionId>
59
+ ```
60
+
61
+ On approval Decidio signs a verdict back to your resume route (or `resume.worker()` picks it
62
+ up), and **your own function** executes — Decidio never holds your credentials.
63
+
64
+ **6. Inspect the receipt** — open the sealed record in the Decidio app → **Verify** →
65
+ **Download receipt (.json)**, then verify it offline with the open, zero-dependency verifier:
66
+
67
+ ```bash
68
+ npx @decidio/verify receipt.json
69
+ ```
70
+
71
+ The signature, content binding, and chain position verify with no Decidio account and no
72
+ network — the evidence is yours, not ours.
73
+
74
+ **7. Prove a replay is rejected** — the approval you just used is single-use. Try to use it again:
75
+
76
+ ```bash
77
+ npx @decidio/sdk approvals approve <the same decisionId> # → refused: already sealed
78
+ curl -X POST http://localhost:4100/decidio/resume \
79
+ -H 'content-type: application/json' -d '{"decisionId":"<id>","verdict":"approved"}'
80
+ # → refused: the resume handler fails closed without a verified x-decidio-signature,
81
+ # and the parked entry was deleted on first completion — a duplicate signal can't double-write.
82
+ ```
83
+
84
+ That's the whole contract: gate → human decision → your execution → owned, verifiable evidence.
85
+
86
+ ## What happens on a call
87
+
88
+ 1. The wrapper asks Decidio's gate to authorize `{action, amount, scope}` for your `agentId`.
89
+ 2. Decidio's policy engine (Cedar) returns **proceed | route | block**:
90
+ - **proceed** — your function runs immediately (auto-approved under a named, versioned rule).
91
+ - **route** — the action suspends (durable) or waits (blocking) for a human decision in Decidio's queue, then runs your function (or throws `DecidioRejectedError`).
92
+ - **block** — the wrapper throws `DecidioBlockedError`; your function never runs.
93
+ 3. After your function runs, the wrapper reports the **real response it captured** back to Decidio, which seals an `application_confirmed` receipt (Decidio minimizes + tokenizes before sealing — the immutable record never stores raw payloads).
94
+
95
+ Your agent executes its own action. **Decidio holds no write credentials for your system** — it authorizes the decision, records it, and (optionally) independently verifies it.
96
+
97
+ ## Real approvals take time — durable async resume
98
+
99
+ A real human approval takes minutes to days. Nobody watches a 60-second polling window, so blocking the call is only honest for short, supervised approvals. For real ones, add a **resume controller** and the *same wrap* becomes durable: a routed action **suspends** instead of blocking — it parks its call args in an **agent-side store** (Decidio stores none of your downstream payload) and throws `DecidioSuspendedError`. The requesting process can exit. When a human approves, **Decidio POSTs a signed verdict** to your agent's resume URL; the controller re-runs *your own function* and confirms the result. Decidio signals — it never executes.
100
+
101
+ ```ts
102
+ import { withApproval, createDecidioResume, FilePendingStore } from "@decidio/sdk";
103
+ import { createServer } from "node:http";
104
+
105
+ const resume = createDecidioResume({
106
+ apiUrl: process.env.DECIDIO_API_URL!,
107
+ apiToken: process.env.DECIDIO_API_TOKEN,
108
+ webhookSecret: process.env.DECIDIO_WEBHOOK_SECRET, // Decidio signs callbacks (HMAC); verified before re-exec
109
+ resumeUrl: "https://my-agent.internal/decidio/resume", // where Decidio calls back on approval
110
+ store: new FilePendingStore(".decidio-pending"), // swap for Redis/Postgres/your queue
111
+ });
112
+
113
+ const createOpportunity = withApproval(createOpportunityRaw, describe, { ...guard, resume });
114
+
115
+ // mount the resume listener (production webhook transport):
116
+ createServer((req, res) => resume.webhookHandler()(req, res)).listen(4100);
117
+ ```
118
+
119
+ No inbound URL (CLI / batch worker)? Use `resume.worker()` instead — a long-lived watcher that polls `/agent/status` for parked decisions and re-executes on approval. That polling is a durable *worker*, not the request path being kept alive. This is the same shape as HumanLayer's approval webhooks, Temporal Signals, Inngest `waitForEvent`, and Decidio's own A2A `input-required` resume. Re-execution is **single-use** (the parked entry is deleted on completion, so a duplicate signal can't double-write), and the webhook handler **fails closed** without a verified signature.
120
+
121
+ ### Already on a durable engine? Use its native wait (Inngest adapter)
122
+
123
+ If your agent runs on Inngest/Temporal/LangGraph, the engine *is* the durable store — you don't need the file store at all. `@decidio/sdk/inngest` maps the gate onto Inngest's native `step.waitForEvent`, so a routed action durably suspends on the engine (zero compute, the worker can die for days) and resumes from its checkpoint:
124
+
125
+ ```ts
126
+ import { decidioGateStep, createInngestResumeBridge } from "@decidio/sdk/inngest";
127
+
128
+ const fn = inngest.createFunction({ id: "create-opp", triggers: [{ event: "salesops/opportunity.requested" }] },
129
+ async ({ event, step }) => decidioGateStep({
130
+ step, config: { ...guard, resumeUrl: "https://my-agent/decidio/resume" },
131
+ ctx: { action: "createOpportunity", amount: event.data.Amount, scope: "Opportunity" },
132
+ run: () => sf.create("Opportunity", event.data), // YOUR write, on resume
133
+ }));
134
+
135
+ // Decidio keeps its ONE signed-webhook contract; this bridge translates it into the
136
+ // Inngest event that wakes the suspended run:
137
+ const bridge = createInngestResumeBridge({ inngest, webhookSecret: process.env.DECIDIO_WEBHOOK_SECRET });
138
+ ```
139
+
140
+ `inngest` is an **optional peer dependency** — the core SDK never imports it, so plain users pay nothing. See `example/inngest-agent.ts` in the repo for a runnable end-to-end (Inngest dev server + the bridge).
141
+
142
+ ## One surface, every runtime — and Python too
143
+
144
+ - **`guard.protect(fn, describe, { adapter })`** is the same call everywhere. Engine adapters map it onto each engine's native durable wait: `@decidio/sdk/langgraph` (drop-in via `interrupt()`), `@decidio/sdk/inngest` (`waitForEvent`), `@decidio/sdk/temporal` (`condition` + signal), `@decidio/sdk/openai` (RunState approvals). No engine? The core webhook/worker resume is the universal path.
145
+ - **Python twin:** `pip install decidio` exposes the identical `guard.protect` (plus the `@guard.approve` decorator, adapters, and the offline verifier); a conformance suite asserts both languages emit an identical request + receipt.
146
+
147
+ ## CLI
148
+
149
+ `npx @decidio/sdk <cmd>` — `init [agentId]` (sign in, register, mint the agent token, write `.env`) · `doctor` (config + connectivity + token-scope check) · `approvals [approve|reject <id> [reason]]` · `dev [--target URL]` (signal-only relay for a local agent). Commands that administer your workspace (`init`, `approvals`) use a **session** sign-in held in memory for the run; the only credential written to disk is the agent's floor-limited token.
150
+
151
+ ## Errors
152
+
153
+ - `DecidioBlockedError` — policy blocked the action (incl. the universal deny-by-default backstop).
154
+ - `DecidioRejectedError` — a human rejected the routed action.
155
+ - `DecidioSuspendedError` — (resume mode) the action was routed and is now parked for async approval; the process may exit and resume later. Not a failure.
156
+ - `DecidioTimeoutError` — (blocking mode only) no human decided within `pollTimeoutMs` (default 10 min).
157
+
158
+ ## Notes
159
+
160
+ - **Framework-agnostic:** it wraps at the tool-call boundary, so it works with any agent runtime (LangGraph, OpenAI Agents SDK, CrewAI, a raw tool loop) and any action (Salesforce, M365, SAP, a DB write).
161
+ - **Confirmation integrity:** the wrapper runs in your process, so it captures the genuine API response — a materially stronger signal than an agent self-report, but it trusts the host. For high-assurance, Decidio can *independently* read the source back (`source_confirmed`) where it holds a read connection.
162
+ - **Zero-code alternative:** an agent that speaks MCP (e.g. Claude on the web) can reach the same gate via Decidio's `request_approval` MCP tool with no code at all.
163
+ - **API surface:** the agent-gate REST contract this SDK speaks is described in [`openapi.yaml`](./openapi.yaml), shipped in the package.
@@ -0,0 +1,13 @@
1
+ import { type GuardConfig, type ApprovalContext } from "../index.js";
2
+ export interface GateAuth {
3
+ decision: "proceed" | "route" | "block";
4
+ decisionId: string;
5
+ receiptId?: string;
6
+ reason?: string;
7
+ }
8
+ export declare function gateAuthorize(config: GuardConfig, ctx: ApprovalContext): Promise<GateAuth>;
9
+ export declare function gateConfirm(config: GuardConfig, decisionId: string, result: unknown): Promise<void>;
10
+ /** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
11
+ export declare function verdictFrom(payload: any): string;
12
+ /** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
13
+ export declare function assertVerdict(verdict: string, decisionId: string, reason?: string): void;
@@ -0,0 +1,60 @@
1
+ // Shared adapter gate logic (TS) — the thin-adapter rule in code. Every engine adapter
2
+ // reuses these gate calls + verdict handling; only the suspend/resume translation
3
+ // differs. Pure/testable so the offline smoke covers our logic; the engine-primitive
4
+ // call in each adapter is a thin one-liner verified by typecheck against real types.
5
+ import { DecidioBlockedError, DecidioRejectedError, signBoundProof, signConfirm } from "../index.js";
6
+ export async function gateAuthorize(config, ctx) {
7
+ const f = config.fetchImpl ?? fetch;
8
+ // Fall back to the resume controller's URL (matches the core wrapper) so an adapter
9
+ // decision carries a callback URL even when only resume.resumeUrl was set.
10
+ const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
11
+ // Sign the bound identity proof like the core wrapper, so an adapter-path agent can PROVE
12
+ // its identity and auto-approve (not just route). No key → unsigned → server routes.
13
+ const requesterIdentity = (config.agentKey && config.agentDid)
14
+ ? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
15
+ : undefined;
16
+ const res = await f(config.apiUrl.replace(/\/$/, "") + "/agent/authorize", {
17
+ method: "POST",
18
+ headers: { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) },
19
+ body: JSON.stringify({
20
+ requester: config.agentId, action: ctx.action, amount: ctx.amount, scope: ctx.scope,
21
+ sourceSystem: config.sourceSystem, ...(ctx.context ? { context: ctx.context } : {}),
22
+ ...(requesterIdentity ? { requesterIdentity } : {}), ...(resumeUrl ? { resumeUrl } : {}),
23
+ }),
24
+ });
25
+ if (!res.ok)
26
+ throw new Error(`decidio /agent/authorize ${res.status}`);
27
+ return res.json();
28
+ }
29
+ export async function gateConfirm(config, decisionId, result) {
30
+ const f = config.fetchImpl ?? fetch;
31
+ try {
32
+ // Sign the confirm too (bound to {action:"confirm", scope:decisionId}) so the adapter path
33
+ // earns application_confirmed; without a key the server caps it at agent_asserted.
34
+ const requesterIdentity = signConfirm(decisionId, config);
35
+ const res = await f(config.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
36
+ method: "POST",
37
+ headers: { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) },
38
+ body: JSON.stringify({ decisionId, capturedResponse: result ?? null, source: "wrapper", ...(requesterIdentity ? { requesterIdentity } : {}) }),
39
+ });
40
+ // fetch only rejects on network error — surface a non-2xx (auth/500) too. Best-effort:
41
+ // log, don't throw (the action already happened).
42
+ if (!res.ok)
43
+ console.warn(`[decidio] confirm returned ${res.status} (action succeeded)`);
44
+ }
45
+ catch (e) {
46
+ console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
47
+ }
48
+ }
49
+ /** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
50
+ export function verdictFrom(payload) {
51
+ return (payload && typeof payload === "object" ? payload.verdict : payload) ?? "";
52
+ }
53
+ /** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
54
+ export function assertVerdict(verdict, decisionId, reason) {
55
+ if (verdict === "approved" || verdict === "auto_approved")
56
+ return;
57
+ if (verdict === "rejected")
58
+ throw new DecidioRejectedError(reason ?? "rejected by approver", decisionId);
59
+ throw new DecidioBlockedError(reason ?? "not approved", decisionId);
60
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from "@langchain/langgraph";
2
+ import { type GuardConfig, type ApprovalContext } from "../index.js";
3
+ /** Gate `run()` behind Decidio inside a LangGraph node. */
4
+ export declare function decidioGate<R>(config: GuardConfig, ctx: ApprovalContext, run: () => Promise<R>): Promise<R>;
5
+ /** Translate Decidio's signed resume webhook into the LangGraph resume Command. The host
6
+ * maps decisionId → thread_id and passes this to graph.invoke/stream. */
7
+ export declare function decidioResumeCommand(body: {
8
+ verdict?: string;
9
+ reason?: string;
10
+ }): Command;
@@ -0,0 +1,28 @@
1
+ // @decidio/sdk/langgraph — durable suspend via LangGraph's native `interrupt()`.
2
+ // `interrupt` finds the running graph itself (no handle needed), so the gate drops into
3
+ // a node: authorize → on route, interrupt() durably pauses (the checkpointer persists;
4
+ // the process can die) → on resume (Command(resume={verdict})) run the agent's own
5
+ // action + confirm. Decidio is the approval system; the host bridges its webhook to the
6
+ // graph resume (decidioResumeCommand). Requires @langchain/langgraph (optional peer dep).
7
+ import { interrupt, Command } from "@langchain/langgraph";
8
+ import { DecidioBlockedError } from "../index.js";
9
+ import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
10
+ /** Gate `run()` behind Decidio inside a LangGraph node. */
11
+ export async function decidioGate(config, ctx, run) {
12
+ const auth = await gateAuthorize(config, ctx);
13
+ if (auth.decision === "block")
14
+ throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
15
+ if (auth.decision === "route") {
16
+ // Durable pause. On resume, interrupt() returns the Command(resume=...) payload.
17
+ const resumed = interrupt({ decidio: { decisionId: auth.decisionId, action: ctx.action, amount: ctx.amount, scope: ctx.scope } });
18
+ assertVerdict(verdictFrom(resumed), auth.decisionId, resumed?.reason);
19
+ }
20
+ const result = await run();
21
+ await gateConfirm(config, auth.decisionId, result);
22
+ return result;
23
+ }
24
+ /** Translate Decidio's signed resume webhook into the LangGraph resume Command. The host
25
+ * maps decisionId → thread_id and passes this to graph.invoke/stream. */
26
+ export function decidioResumeCommand(body) {
27
+ return new Command({ resume: { verdict: body.verdict, reason: body.reason } });
28
+ }
@@ -0,0 +1,26 @@
1
+ import type { GuardConfig, ApprovalContext } from "../index.js";
2
+ type ToolApprovalItem = any;
3
+ interface RunStateLike {
4
+ getInterruptions(): ToolApprovalItem[];
5
+ approve(item: ToolApprovalItem): void;
6
+ reject(item: ToolApprovalItem): void;
7
+ }
8
+ export interface Resolved {
9
+ item: ToolApprovalItem;
10
+ verdict: "approved" | "blocked";
11
+ decisionId: string;
12
+ }
13
+ export interface Pending {
14
+ item: ToolApprovalItem;
15
+ decisionId: string;
16
+ }
17
+ /** Ask Decidio for each pending tool-approval on a RunState.
18
+ * proceed → state.approve(item); block → state.reject(item); route → pending (a human
19
+ * decides via Decidio; the host parks state.toString() + resumes via applyResume). */
20
+ export declare function gateInterruptions(config: GuardConfig, state: RunStateLike, describe: (item: ToolApprovalItem) => ApprovalContext): Promise<{
21
+ resolved: Resolved[];
22
+ pending: Pending[];
23
+ }>;
24
+ /** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
25
+ export declare function applyResume(state: RunStateLike, item: ToolApprovalItem, verdict: string): void;
26
+ export {};
@@ -0,0 +1,45 @@
1
+ // @decidio/sdk/openai — gate OpenAI Agents SDK tool-approval interruptions through
2
+ // Decidio, with durable suspend via the SDK's native RunState serialization.
3
+ //
4
+ // The Agents loop surfaces tool approvals on the RunState (state.getInterruptions());
5
+ // you state.approve(item)/reject(item) then resume with run(agent, state). RunState
6
+ // serializes (state.toString() / RunState.fromString(agent, s)) — that's the durability:
7
+ // park the serialized state agent-side on `route`, resume on Decidio's approval.
8
+ //
9
+ // const state = result.state; // or RunState from the run
10
+ // const { resolved, pending } = await gateInterruptions(config, state, describe);
11
+ // if (pending.length) { await store.put(decisionId, state.toString()); return; } // suspend
12
+ // result = await run(agent, state); // all resolved → resume
13
+ // // on Decidio approval webhook: state = await RunState.fromString(agent, parked);
14
+ // // applyResume(state, item, verdict); result = await run(agent, state);
15
+ //
16
+ // Duck-typed on the state/item (no hard @openai/agents import) so it stays thin.
17
+ import { gateAuthorize } from "./_gate.js";
18
+ /** Ask Decidio for each pending tool-approval on a RunState.
19
+ * proceed → state.approve(item); block → state.reject(item); route → pending (a human
20
+ * decides via Decidio; the host parks state.toString() + resumes via applyResume). */
21
+ export async function gateInterruptions(config, state, describe) {
22
+ const resolved = [];
23
+ const pending = [];
24
+ for (const item of state.getInterruptions()) {
25
+ const auth = await gateAuthorize(config, describe(item));
26
+ if (auth.decision === "proceed") {
27
+ state.approve(item);
28
+ resolved.push({ item, verdict: "approved", decisionId: auth.decisionId });
29
+ }
30
+ else if (auth.decision === "block") {
31
+ state.reject(item);
32
+ resolved.push({ item, verdict: "blocked", decisionId: auth.decisionId });
33
+ }
34
+ else
35
+ pending.push({ item, decisionId: auth.decisionId });
36
+ }
37
+ return { resolved, pending };
38
+ }
39
+ /** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
40
+ export function applyResume(state, item, verdict) {
41
+ if (verdict === "approved" || verdict === "auto_approved")
42
+ state.approve(item);
43
+ else
44
+ state.reject(item);
45
+ }
@@ -0,0 +1,16 @@
1
+ import { type GuardConfig, type ApprovalContext } from "../index.js";
2
+ import { type GateAuth } from "./_gate.js";
3
+ export declare const decidioResumeSignal: import("@temporalio/workflow").SignalDefinition<[string, string, (string | undefined)?]>;
4
+ /** Call inside the workflow to wire the resume signal; returns the verdict store. */
5
+ export declare function installDecidioSignal(): Map<string, {
6
+ verdict: string;
7
+ reason?: string;
8
+ }>;
9
+ export interface DecidioGateOpts {
10
+ authorize?: () => Promise<GateAuth>;
11
+ confirm?: (decisionId: string, result: unknown) => Promise<void>;
12
+ }
13
+ export declare function decidioGate<R>(config: GuardConfig, ctx: ApprovalContext, run: () => Promise<R>, verdicts: Map<string, {
14
+ verdict: string;
15
+ reason?: string;
16
+ }>, opts?: DecidioGateOpts): Promise<R>;
@@ -0,0 +1,34 @@
1
+ // @decidio/sdk/temporal — durable suspend via Temporal's native `condition()` + a
2
+ // Decidio resume signal. Strongest durability (the workflow survives worker restarts for
3
+ // days); the agent's action runs on resume. Requires @temporalio/workflow (optional peer).
4
+ //
5
+ // Inside your workflow:
6
+ // const verdicts = installDecidioSignal(); // wires the resume signal
7
+ // const out = await decidioGate(config, ctx, () => createOpp(o), verdicts, {
8
+ // authorize: () => executeActivity(authorizeActivity, ...), // HTTP belongs in activities
9
+ // confirm: (id, r) => executeActivity(confirmActivity, ...),
10
+ // });
11
+ // Decidio's webhook bridges to: handle.signal(decidioResumeSignal, decisionId, verdict, reason)
12
+ import { condition, defineSignal, setHandler } from "@temporalio/workflow";
13
+ import { DecidioBlockedError } from "../index.js";
14
+ import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
15
+ export const decidioResumeSignal = defineSignal("decidioResume");
16
+ /** Call inside the workflow to wire the resume signal; returns the verdict store. */
17
+ export function installDecidioSignal() {
18
+ const verdicts = new Map();
19
+ setHandler(decidioResumeSignal, (decisionId, verdict, reason) => { verdicts.set(decisionId, { verdict, reason }); });
20
+ return verdicts;
21
+ }
22
+ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
23
+ const auth = await (opts.authorize ?? (() => gateAuthorize(config, ctx)))();
24
+ if (auth.decision === "block")
25
+ throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
26
+ if (auth.decision === "route") {
27
+ await condition(() => verdicts.has(auth.decisionId)); // durable wait on the signal
28
+ const v = verdicts.get(auth.decisionId);
29
+ assertVerdict(verdictFrom(v), auth.decisionId, v.reason);
30
+ }
31
+ const result = await run();
32
+ await (opts.confirm ?? ((id, r) => gateConfirm(config, id, r)))(auth.decisionId, result);
33
+ return result;
34
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};