@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 +202 -0
- package/README.md +163 -0
- package/dist/adapters/_gate.d.ts +13 -0
- package/dist/adapters/_gate.js +60 -0
- package/dist/adapters/langgraph.d.ts +10 -0
- package/dist/adapters/langgraph.js +28 -0
- package/dist/adapters/openai.d.ts +26 -0
- package/dist/adapters/openai.js +45 -0
- package/dist/adapters/temporal.d.ts +16 -0
- package/dist/adapters/temporal.js +34 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +314 -0
- package/dist/guard.d.ts +52 -0
- package/dist/guard.js +76 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +197 -0
- package/dist/inngest.d.ts +61 -0
- package/dist/inngest.js +114 -0
- package/dist/resume.d.ts +152 -0
- package/dist/resume.js +301 -0
- package/dist/signing.d.ts +48 -0
- package/dist/signing.js +83 -0
- package/openapi.yaml +171 -0
- package/package.json +83 -0
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