@decidio/sdk 0.1.7 → 0.1.9
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/README.md +59 -20
- package/dist/adapters/_gate.d.ts +2 -0
- package/dist/adapters/_gate.js +25 -2
- package/dist/adapters/langgraph.d.ts +13 -0
- package/dist/adapters/langgraph.js +14 -0
- package/dist/adapters/openai.d.ts +24 -0
- package/dist/adapters/openai.js +4 -0
- package/dist/adapters/temporal.d.ts +16 -0
- package/dist/adapters/temporal.js +17 -1
- package/dist/cli.js +728 -213
- package/dist/guard.d.ts +29 -0
- package/dist/guard.js +25 -0
- package/dist/index.d.ts +54 -2
- package/dist/index.js +123 -7
- package/dist/inngest.d.ts +20 -0
- package/dist/inngest.js +26 -0
- package/dist/resume.d.ts +89 -0
- package/dist/resume.js +157 -18
- package/dist/signing.d.ts +20 -0
- package/dist/signing.js +73 -0
- package/openapi.yaml +1 -1
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -11,22 +11,32 @@ npx @decidio/sdk quickstart
|
|
|
11
11
|
One command, guided: it registers a fresh agent (key generated locally), gates a demo action
|
|
12
12
|
with the same one line you'll use on your real function, routes it to a human — **you** — lets
|
|
13
13
|
you approve right there (or from your Decidio app), executes on approval, then downloads the
|
|
14
|
-
sealed Authority Receipt and verifies it offline in front of you
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
sealed Authority Receipt and verifies it offline in front of you, pinned to your workspace's
|
|
15
|
+
issuer. It mints a **7-day** tour token (`init` mints 90-day tokens — different jobs, different
|
|
16
|
+
lifetimes) and ends by offering to clean itself up (`quickstart cleanup` works later too).
|
|
17
|
+
Sign in when prompted. No config: with no `DECIDIO_API_URL` set it targets the hosted synthetic
|
|
18
|
+
sandbox. Headless, the tour **stops at the routed decision** — approving is a human act, so it
|
|
19
|
+
prints the approve command and exits rather than approve its own request (CI can pass
|
|
20
|
+
`--auto-approve-demo`, which labels that authority as automated).
|
|
17
21
|
|
|
18
22
|
## Quickstart — seven steps to your first sealed receipt
|
|
19
23
|
|
|
20
24
|
Under 30 minutes, no help needed. You'll need Node 20.6+, a terminal, and a Decidio workspace
|
|
21
25
|
(request one at [decidioai.com](https://decidioai.com/#early-access) — the hosted sandbox is
|
|
22
|
-
synthetic data, safe to experiment in
|
|
26
|
+
synthetic data, safe to experiment in — and it is ONE SHARED workspace: workspace-wide views
|
|
27
|
+
like `tokens list` or `approvals` show every lab member's agents and decisions there, which is
|
|
28
|
+
the shared-lab boundary working as designed; production workspaces are per-tenant). Lab
|
|
29
|
+
workspaces have a **governed-action allowance**
|
|
30
|
+
(currently 30 actions/hour); hitting it raises a typed `DecidioRateLimitError` naming the limit
|
|
31
|
+
and reset time — nothing is routed or executed — and `tokens list` shows your remaining
|
|
32
|
+
allowance any time.
|
|
23
33
|
|
|
24
34
|
**PowerShell users** — the equivalents for every Bash idiom used below:
|
|
25
35
|
|
|
26
36
|
```powershell
|
|
27
37
|
mkdir decidio-quickstart; cd decidio-quickstart # instead of mkdir ... && cd ...
|
|
28
38
|
$env:DECIDIO_API_URL = "https://decidio-api.onrender.com" # instead of export NAME=value
|
|
29
|
-
$env:
|
|
39
|
+
$env:DECIDIO_AGENT_ID = "my-agent"; npx @decidio/sdk init
|
|
30
40
|
# instead of VAR=value npx ... (inline env)
|
|
31
41
|
```
|
|
32
42
|
|
|
@@ -37,17 +47,21 @@ mkdir decidio-quickstart && cd decidio-quickstart
|
|
|
37
47
|
npm install @decidio/sdk
|
|
38
48
|
```
|
|
39
49
|
|
|
40
|
-
**2.
|
|
50
|
+
**2. Register your agent**
|
|
41
51
|
|
|
42
52
|
```bash
|
|
43
|
-
export DECIDIO_API_URL=https://decidio-api.onrender.com # the hosted sandbox
|
|
44
53
|
npx @decidio/sdk init --unique my-agent # --unique appends a random suffix: collision-proof
|
|
45
54
|
```
|
|
46
55
|
|
|
56
|
+
Every CLI command talks to the hosted sandbox by default and announces its target host on
|
|
57
|
+
stderr each run — set `DECIDIO_API_URL` (env or `.env`) to point at another Decidio instead.
|
|
58
|
+
|
|
47
59
|
`init` signs you into your workspace (email + password, or paste the sign-in link from your
|
|
48
60
|
workspace email), generates the agent's Ed25519 keypair **locally** (only the public `did:key`
|
|
49
61
|
is sent), registers the agent, mints its **floor-limited API token** (limited to the agent runtime — request authorization, poll status, report signed confirmation; it cannot administer the workspace or approve anything — and your workspace session never touches the agent's disk), and writes `.env`.
|
|
50
|
-
Every later command reads `.env` from this directory automatically.
|
|
62
|
+
Every later command reads `.env` from this directory automatically. Re-running init for an
|
|
63
|
+
agent that already has a token REPLACES it — the server keeps one active token per agent, so
|
|
64
|
+
the previous one is revoked and init says so.
|
|
51
65
|
|
|
52
66
|
What `.env` contains after a successful init: `DECIDIO_API_URL`, `DECIDIO_AGENT_ID`,
|
|
53
67
|
`DECIDIO_AGENT_DID` + `DECIDIO_AGENT_KEY` (the local keypair — the key is what earns
|
|
@@ -89,11 +103,9 @@ node --env-file=.env quickstart.mjs
|
|
|
89
103
|
A brand-new agent matches **no** auto-approve rule, so Decidio **routes every request to a
|
|
90
104
|
human by default** — that is the point: nothing executes without either a named policy rule or
|
|
91
105
|
a person. (Routing-by-default is distinct from a policy **block**, which refuses the action
|
|
92
|
-
outright — no human can approve a blocked request.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
named rule auto-approves amounts under $1M when the request carries the agent's signed
|
|
96
|
-
identity proof, so even this $86k example would proceed there.)
|
|
106
|
+
outright — no human can approve a blocked request. Policy `proceed` rules — named auto-approve
|
|
107
|
+
for requests carrying the agent's signed identity proof — are configured per workspace by its
|
|
108
|
+
owner.) The script prints the decision id and waits.
|
|
97
109
|
|
|
98
110
|
**5. Approve it** — in a second terminal (same directory; replace `<decisionId>` with the id
|
|
99
111
|
step 4 printed — angle brackets are placeholders, never typed literally). Approvals are a
|
|
@@ -126,7 +138,10 @@ evidence is yours, not ours. To also prove *who* issued it, pin your workspace's
|
|
|
126
138
|
record verify panel — or simply read it off the `receipt` command's output, which prints your
|
|
127
139
|
workspace's issuer DID from the authenticated response so you never have to hunt for it).
|
|
128
140
|
|
|
129
|
-
One receipt proves itself. Pass several files and their **ordering** is verified too
|
|
141
|
+
One receipt proves itself. Pass several files and their **ordering** is verified too — in
|
|
142
|
+
**seal order** (each receipt's `prevHash` links to its predecessor's `contentHash`), which is
|
|
143
|
+
the order decisions were *sealed*, not the order of their displayed decision numbers (two
|
|
144
|
+
decisions created together can seal in either order). The first
|
|
130
145
|
file is the anchor and each subsequent receipt must follow it, so verifying any consecutive
|
|
131
146
|
*segment* works — you are told whether the segment starts at your workspace's genesis or
|
|
132
147
|
mid-history, and a set spanning two different issuers is refused outright.
|
|
@@ -137,11 +152,18 @@ why the credential's own `confirmation.status` field reads `"pending"` forever.
|
|
|
137
152
|
evidence that arrives *after* the seal (your function's captured result) lives on the
|
|
138
153
|
**record**, not inside the credential bytes: the `receipt` command prints the record's current
|
|
139
154
|
tier next to the file, and with the signing key `init` wrote you'll see `application_confirmed`
|
|
140
|
-
— the wrapper's report
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
— the wrapper's report carried a signed identity proof bound to your agent, this decision, and
|
|
156
|
+
a freshness window. Precisely: the proof authenticates WHO is confirming WHAT decision — it
|
|
157
|
+
does not yet sign the captured response bytes themselves (that binding is on the receipt-v2
|
|
158
|
+
roadmap), so treat the tier as proven reporter identity, not a cryptographic hash of the
|
|
159
|
+
result. (No signing key configured? The server caps the evidence at the weaker
|
|
160
|
+
`agent_asserted`, and the SDK warns at confirm time.) Immutable seal + accruing evidence is the design, not an omission — corrections and
|
|
143
161
|
later facts supersede, they never rewrite.
|
|
144
162
|
|
|
163
|
+
One sharing note: a receipt is **not a secret, but it is not metadata-free** — it names the
|
|
164
|
+
action, amount limits, policy, agent and issuer identifiers, and the decision reason. Share it
|
|
165
|
+
with auditors and counterparties deliberately, the way you would any business record.
|
|
166
|
+
|
|
145
167
|
**7. Prove a replay is rejected:**
|
|
146
168
|
|
|
147
169
|
```bash
|
|
@@ -151,7 +173,9 @@ npx @decidio/sdk approvals approve <the same decisionId>
|
|
|
151
173
|
Decisions are single-use: the CLI reports "already resolved — nothing changed" and exits
|
|
152
174
|
non-zero. The server answers replays idempotently — a second approve can never re-execute the
|
|
153
175
|
action. (The durable resume path has the same property: parked actions are claimed exactly
|
|
154
|
-
once, and its webhook handler fails closed without a verified signature
|
|
176
|
+
once, and its webhook handler fails closed without a verified signature — unless you
|
|
177
|
+
explicitly set `allowUnsigned: true`, which removes webhook authenticity and makes the
|
|
178
|
+
controller say so loudly at startup; never use it on an internet-reachable endpoint.)
|
|
155
179
|
|
|
156
180
|
That's the whole contract: gate → human decision → your execution → owned, verifiable evidence.
|
|
157
181
|
|
|
@@ -219,6 +243,13 @@ also allow-list resume hosts (default-deny). That's operator territory: if you r
|
|
|
219
243
|
Decidio (or we run a pilot with you), the webhook is set up then. Against the hosted sandbox,
|
|
220
244
|
use the worker — a webhook secret your server never learned would (correctly) fail closed.
|
|
221
245
|
|
|
246
|
+
**The supported argument data model is plain JSON**: `null`, booleans, strings, finite
|
|
247
|
+
numbers, arrays, and plain objects. Anything that cannot survive JSON with its meaning intact
|
|
248
|
+
— `NaN`/`Infinity`, `undefined`, `BigInt`, `Map`/`Set`/`Date`, class instances, functions,
|
|
249
|
+
circular structures — is **refused before the decision is routed or the payload signed**
|
|
250
|
+
(typed error naming the offending path), because a value that silently drifts in serialization
|
|
251
|
+
would mean a human approves one thing and your function executes another.
|
|
252
|
+
|
|
222
253
|
Either way, re-execution is **single-use on the happy path**: the parked entry is claimed before
|
|
223
254
|
execution and deleted on success, so a duplicate signal finds nothing to replay. Honest edge — if
|
|
224
255
|
your action succeeds but the process dies *before* the entry is cleared, the claim can be retried
|
|
@@ -264,9 +295,16 @@ imports none of them. Each adapter's shipped `.d.ts` header documents its wiring
|
|
|
264
295
|
- `doctor` — config, connectivity, and what your current token can do
|
|
265
296
|
- `approvals [--mine] [--json]` / `approvals approve|reject <id> [reason]` — the human side, from your terminal (the list is workspace-wide; `--mine` narrows it to this directory's agent)
|
|
266
297
|
- `receipt <id>` — download a sealed decision's Authority Receipt to a `.json` file
|
|
267
|
-
- `verify [--issuer <did>] <file>...` — verify receipts offline (the canonical verifier; the standalone `@decidio/verify` package is the same implementation, for auditors who want zero Decidio dependency)
|
|
298
|
+
- `verify [--issuer <did>] <file>...` — verify receipts offline (the canonical verifier; the standalone `@decidio/verify` package is the same implementation, for auditors who want zero Decidio dependency — the sdk's embedded copy is pinned to the previous release, so message wording may trail it one version; verdicts and semantics are conformance-locked identical). Unknown flags are refused, never ignored. Fully offline AND fully decoupled: it touches no network and reads no `.env` — a receipt verifies identically from an empty directory.
|
|
299
|
+
- `tokens list [--mine]` / `tokens revoke <jti>` — see and kill agent tokens (values are never re-shown; the jti is the revocation handle). Each row carries its status — `active`, `expired`, or `REVOKED <date>` — so the list confirms the revocations it performs; re-revoking is an honest no-op (exit 1). The list is workspace-wide (a shared workspace shows every member's agents); `--mine` narrows to this directory's agent
|
|
300
|
+
- `quickstart cleanup` — revoke the tour's token and delete its local credentials. What stays, honestly: sealed records (permanent by design) and the agent's registration row (inert without key or token; server-side deregistration is on the roadmap)
|
|
268
301
|
- `dev [--target URL]` — local relay for the resume webhook (operator setups)
|
|
269
302
|
|
|
303
|
+
The CLI's stream contract: **stdout carries results only** (safe to pipe — `--json` output is
|
|
304
|
+
pure JSON); errors, usage shown on *error* paths, and the per-run target-host announcement go
|
|
305
|
+
to **stderr**. Asked-for help (an explicit `--help`, or invoking the bare CLI) is itself a
|
|
306
|
+
result and prints on stdout. Exit codes: 0 success · 1 failure or honest no-op · 2 usage error.
|
|
307
|
+
|
|
270
308
|
Commands that administer your workspace (`init`, `approvals`, `receipt`) use a **session**
|
|
271
309
|
sign-in held in memory for that run. What lands on disk in `.env`: the agent's floor-limited
|
|
272
310
|
API token, its locally-generated private signing key, and a webhook secret (used only in
|
|
@@ -280,7 +318,8 @@ manager for production.
|
|
|
280
318
|
- `DecidioSuspendedError` — (durable mode) the action was routed and is parked for async approval; the process may exit and resume later. Not a failure.
|
|
281
319
|
- `DecidioTimeoutError` — (blocking mode only) no human decided within `pollTimeoutMs` (default 10 min); the decision stays open in Decidio, nothing executed locally.
|
|
282
320
|
|
|
283
|
-
Every error carries the `decisionId`, so a suspended or
|
|
321
|
+
Every error raised after a decision exists carries the `decisionId`, so a suspended or
|
|
322
|
+
timed-out action is always findable in
|
|
284
323
|
your queue.
|
|
285
324
|
|
|
286
325
|
## Notes
|
package/dist/adapters/_gate.d.ts
CHANGED
|
@@ -7,5 +7,7 @@ export interface GateAuth {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function gateAuthorize(config: GuardConfig, ctx: ApprovalContext): Promise<GateAuth>;
|
|
9
9
|
export declare function gateConfirm(config: GuardConfig, decisionId: string, result: unknown): Promise<void>;
|
|
10
|
+
/** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
|
|
10
11
|
export declare function verdictFrom(payload: any): string;
|
|
12
|
+
/** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
|
|
11
13
|
export declare function assertVerdict(verdict: string, decisionId: string, reason?: string): void;
|
package/dist/adapters/_gate.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
|
|
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, assertJsonSafe, assertConfirmRecorded } from "../index.js";
|
|
2
6
|
export async function gateAuthorize(config, ctx) {
|
|
7
|
+
// SAME JSON-safety gate as the core wrapper (guard equivalent-path catch: the adapter door
|
|
8
|
+
// skipped it, and the server's amount coercion turns a drifted NaN→null into a $0 request —
|
|
9
|
+
// a human would approve an amount that meant something else entirely).
|
|
10
|
+
assertJsonSafe(ctx, "describe(...)");
|
|
3
11
|
const f = config.fetchImpl ?? fetch;
|
|
12
|
+
// Fall back to the resume controller's URL (matches the core wrapper) so an adapter
|
|
13
|
+
// decision carries a callback URL even when only resume.resumeUrl was set.
|
|
4
14
|
const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
|
|
15
|
+
// Sign the bound identity proof like the core wrapper, so an adapter-path agent can PROVE
|
|
16
|
+
// its identity and auto-approve (not just route). No key → unsigned → server routes.
|
|
5
17
|
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
6
18
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
7
19
|
: undefined;
|
|
@@ -21,7 +33,10 @@ export async function gateAuthorize(config, ctx) {
|
|
|
21
33
|
export async function gateConfirm(config, decisionId, result) {
|
|
22
34
|
const f = config.fetchImpl ?? fetch;
|
|
23
35
|
try {
|
|
36
|
+
// Sign the confirm too (bound to {action:"confirm", scope:decisionId}) so the adapter path
|
|
37
|
+
// earns application_confirmed; without a key the server caps it at agent_asserted.
|
|
24
38
|
const requesterIdentity = signConfirm(decisionId, config);
|
|
39
|
+
// Same no-silent-downgrade rule as the inline wrap (guard equivalent-path catch).
|
|
25
40
|
if (!requesterIdentity)
|
|
26
41
|
console.warn(`[decidio] execution evidence for ${decisionId} recorded as agent_asserted — no signing key configured, so the report can't be proven to come from this agent. Set DECIDIO_AGENT_KEY/DID to earn application_confirmed.`);
|
|
27
42
|
const res = await f(config.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
|
|
@@ -29,16 +44,24 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
29
44
|
headers: { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) },
|
|
30
45
|
body: JSON.stringify({ decisionId, capturedResponse: result ?? null, source: "wrapper", ...(requesterIdentity ? { requesterIdentity } : {}) }),
|
|
31
46
|
});
|
|
32
|
-
|
|
47
|
+
// fetch only rejects on network error — surface a non-2xx (auth/500) too. Best-effort:
|
|
48
|
+
// log, don't throw (the action already happened).
|
|
49
|
+
if (!res.ok) {
|
|
33
50
|
console.warn(`[decidio] confirm returned ${res.status} (action succeeded)`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// The ONE confirm-body rule, on this door too (guard equivalent-path catch).
|
|
54
|
+
assertConfirmRecorded(await res.json().catch(() => null), decisionId, !!requesterIdentity);
|
|
34
55
|
}
|
|
35
56
|
catch (e) {
|
|
36
57
|
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
37
58
|
}
|
|
38
59
|
}
|
|
60
|
+
/** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
|
|
39
61
|
export function verdictFrom(payload) {
|
|
40
62
|
return (payload && typeof payload === "object" ? payload.verdict : payload) ?? "";
|
|
41
63
|
}
|
|
64
|
+
/** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
|
|
42
65
|
export function assertVerdict(verdict, decisionId, reason) {
|
|
43
66
|
if (verdict === "approved" || verdict === "auto_approved")
|
|
44
67
|
return;
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { Command } from "@langchain/langgraph";
|
|
2
2
|
import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
3
|
+
/**
|
|
4
|
+
* @decidio/sdk/langgraph — durable suspend via LangGraph's native `interrupt()`.
|
|
5
|
+
* `interrupt` finds the running graph itself (no handle needed), so the gate drops into
|
|
6
|
+
* a node: authorize → on route, interrupt() durably pauses (the checkpointer persists;
|
|
7
|
+
* the process can die) → on resume (Command(resume={verdict})) run the agent's own
|
|
8
|
+
* action + confirm. Decidio is the approval system; the host bridges its webhook to the
|
|
9
|
+
* graph resume (decidioResumeCommand). Requires @langchain/langgraph (optional peer dep).
|
|
10
|
+
*
|
|
11
|
+
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
12
|
+
*/
|
|
13
|
+
/** Gate `run()` behind Decidio inside a LangGraph node. */
|
|
3
14
|
export declare function decidioGate<R>(config: GuardConfig, ctx: ApprovalContext, run: () => Promise<R>): Promise<R>;
|
|
15
|
+
/** Translate Decidio's signed resume webhook into the LangGraph resume Command. The host
|
|
16
|
+
* maps decisionId → thread_id and passes this to graph.invoke/stream. */
|
|
4
17
|
export declare function decidioResumeCommand(body: {
|
|
5
18
|
verdict?: string;
|
|
6
19
|
reason?: string;
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { interrupt, Command } from "@langchain/langgraph";
|
|
2
2
|
import { DecidioBlockedError } from "../index.js";
|
|
3
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
|
|
4
|
+
/**
|
|
5
|
+
* @decidio/sdk/langgraph — durable suspend via LangGraph's native `interrupt()`.
|
|
6
|
+
* `interrupt` finds the running graph itself (no handle needed), so the gate drops into
|
|
7
|
+
* a node: authorize → on route, interrupt() durably pauses (the checkpointer persists;
|
|
8
|
+
* the process can die) → on resume (Command(resume={verdict})) run the agent's own
|
|
9
|
+
* action + confirm. Decidio is the approval system; the host bridges its webhook to the
|
|
10
|
+
* graph resume (decidioResumeCommand). Requires @langchain/langgraph (optional peer dep).
|
|
11
|
+
*
|
|
12
|
+
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
13
|
+
*/
|
|
14
|
+
/** Gate `run()` behind Decidio inside a LangGraph node. */
|
|
4
15
|
export async function decidioGate(config, ctx, run) {
|
|
5
16
|
const auth = await gateAuthorize(config, ctx);
|
|
6
17
|
if (auth.decision === "block")
|
|
7
18
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
8
19
|
if (auth.decision === "route") {
|
|
20
|
+
// Durable pause. On resume, interrupt() returns the Command(resume=...) payload.
|
|
9
21
|
const resumed = interrupt({ decidio: { decisionId: auth.decisionId, action: ctx.action, amount: ctx.amount, scope: ctx.scope } });
|
|
10
22
|
assertVerdict(verdictFrom(resumed), auth.decisionId, resumed?.reason);
|
|
11
23
|
}
|
|
@@ -13,6 +25,8 @@ export async function decidioGate(config, ctx, run) {
|
|
|
13
25
|
await gateConfirm(config, auth.decisionId, result);
|
|
14
26
|
return result;
|
|
15
27
|
}
|
|
28
|
+
/** Translate Decidio's signed resume webhook into the LangGraph resume Command. The host
|
|
29
|
+
* maps decisionId → thread_id and passes this to graph.invoke/stream. */
|
|
16
30
|
export function decidioResumeCommand(body) {
|
|
17
31
|
return new Command({ resume: { verdict: body.verdict, reason: body.reason } });
|
|
18
32
|
}
|
|
@@ -5,6 +5,26 @@ interface RunStateLike {
|
|
|
5
5
|
approve(item: ToolApprovalItem): void;
|
|
6
6
|
reject(item: ToolApprovalItem): void;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* @decidio/sdk/openai — gate OpenAI Agents SDK tool-approval interruptions through
|
|
10
|
+
* Decidio, with durable suspend via the SDK's native RunState serialization.
|
|
11
|
+
*
|
|
12
|
+
* The Agents loop surfaces tool approvals on the RunState (state.getInterruptions());
|
|
13
|
+
* you state.approve(item)/reject(item) then resume with run(agent, state). RunState
|
|
14
|
+
* serializes (state.toString() / RunState.fromString(agent, s)) — that's the durability:
|
|
15
|
+
* park the serialized state agent-side on `route`, resume on Decidio's approval.
|
|
16
|
+
*
|
|
17
|
+
* const state = result.state; // or RunState from the run
|
|
18
|
+
* const { resolved, pending } = await gateInterruptions(config, state, describe);
|
|
19
|
+
* if (pending.length) { await store.put(decisionId, state.toString()); return; } // suspend
|
|
20
|
+
* result = await run(agent, state); // all resolved → resume
|
|
21
|
+
* // on Decidio approval webhook: state = await RunState.fromString(agent, parked);
|
|
22
|
+
* // applyResume(state, item, verdict); result = await run(agent, state);
|
|
23
|
+
*
|
|
24
|
+
* Duck-typed on the state/item (no hard @openai/agents import) so it stays thin.
|
|
25
|
+
*
|
|
26
|
+
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
27
|
+
*/
|
|
8
28
|
export interface Resolved {
|
|
9
29
|
item: ToolApprovalItem;
|
|
10
30
|
verdict: "approved" | "blocked";
|
|
@@ -14,9 +34,13 @@ export interface Pending {
|
|
|
14
34
|
item: ToolApprovalItem;
|
|
15
35
|
decisionId: string;
|
|
16
36
|
}
|
|
37
|
+
/** Ask Decidio for each pending tool-approval on a RunState.
|
|
38
|
+
* proceed → state.approve(item); block → state.reject(item); route → pending (a human
|
|
39
|
+
* decides via Decidio; the host parks state.toString() + resumes via applyResume). */
|
|
17
40
|
export declare function gateInterruptions(config: GuardConfig, state: RunStateLike, describe: (item: ToolApprovalItem) => ApprovalContext): Promise<{
|
|
18
41
|
resolved: Resolved[];
|
|
19
42
|
pending: Pending[];
|
|
20
43
|
}>;
|
|
44
|
+
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
|
|
21
45
|
export declare function applyResume(state: RunStateLike, item: ToolApprovalItem, verdict: string): void;
|
|
22
46
|
export {};
|
package/dist/adapters/openai.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { gateAuthorize } from "./_gate.js";
|
|
2
|
+
/** Ask Decidio for each pending tool-approval on a RunState.
|
|
3
|
+
* proceed → state.approve(item); block → state.reject(item); route → pending (a human
|
|
4
|
+
* decides via Decidio; the host parks state.toString() + resumes via applyResume). */
|
|
2
5
|
export async function gateInterruptions(config, state, describe) {
|
|
3
6
|
const resolved = [];
|
|
4
7
|
const pending = [];
|
|
@@ -17,6 +20,7 @@ export async function gateInterruptions(config, state, describe) {
|
|
|
17
20
|
}
|
|
18
21
|
return { resolved, pending };
|
|
19
22
|
}
|
|
23
|
+
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
|
|
20
24
|
export function applyResume(state, item, verdict) {
|
|
21
25
|
if (verdict === "approved" || verdict === "auto_approved")
|
|
22
26
|
state.approve(item);
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
2
2
|
import { type GateAuth } from "./_gate.js";
|
|
3
|
+
/**
|
|
4
|
+
* @decidio/sdk/temporal — durable suspend via Temporal's native `condition()` + a
|
|
5
|
+
* Decidio resume signal. Strongest durability (the workflow survives worker restarts for
|
|
6
|
+
* days); the agent's action runs on resume. Requires @temporalio/workflow (optional peer).
|
|
7
|
+
*
|
|
8
|
+
* Inside your workflow:
|
|
9
|
+
* const verdicts = installDecidioSignal(); // wires the resume signal
|
|
10
|
+
* const out = await decidioGate(config, ctx, () => createOpp(o), verdicts, {
|
|
11
|
+
* authorize: () => executeActivity(authorizeActivity, ...), // HTTP belongs in activities
|
|
12
|
+
* confirm: (id, r) => executeActivity(confirmActivity, ...),
|
|
13
|
+
* });
|
|
14
|
+
* Decidio's webhook bridges to: handle.signal(decidioResumeSignal, decisionId, verdict, reason)
|
|
15
|
+
*
|
|
16
|
+
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
17
|
+
*/
|
|
3
18
|
export declare const decidioResumeSignal: import("@temporalio/workflow").SignalDefinition<[string, string, (string | undefined)?]>;
|
|
19
|
+
/** Call inside the workflow to wire the resume signal; returns the verdict store. */
|
|
4
20
|
export declare function installDecidioSignal(): Map<string, {
|
|
5
21
|
verdict: string;
|
|
6
22
|
reason?: string;
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { condition, defineSignal, setHandler } from "@temporalio/workflow";
|
|
2
2
|
import { DecidioBlockedError } from "../index.js";
|
|
3
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
|
|
4
|
+
/**
|
|
5
|
+
* @decidio/sdk/temporal — durable suspend via Temporal's native `condition()` + a
|
|
6
|
+
* Decidio resume signal. Strongest durability (the workflow survives worker restarts for
|
|
7
|
+
* days); the agent's action runs on resume. Requires @temporalio/workflow (optional peer).
|
|
8
|
+
*
|
|
9
|
+
* Inside your workflow:
|
|
10
|
+
* const verdicts = installDecidioSignal(); // wires the resume signal
|
|
11
|
+
* const out = await decidioGate(config, ctx, () => createOpp(o), verdicts, {
|
|
12
|
+
* authorize: () => executeActivity(authorizeActivity, ...), // HTTP belongs in activities
|
|
13
|
+
* confirm: (id, r) => executeActivity(confirmActivity, ...),
|
|
14
|
+
* });
|
|
15
|
+
* Decidio's webhook bridges to: handle.signal(decidioResumeSignal, decisionId, verdict, reason)
|
|
16
|
+
*
|
|
17
|
+
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
18
|
+
*/
|
|
4
19
|
export const decidioResumeSignal = defineSignal("decidioResume");
|
|
20
|
+
/** Call inside the workflow to wire the resume signal; returns the verdict store. */
|
|
5
21
|
export function installDecidioSignal() {
|
|
6
22
|
const verdicts = new Map();
|
|
7
23
|
setHandler(decidioResumeSignal, (decisionId, verdict, reason) => { verdicts.set(decisionId, { verdict, reason }); });
|
|
@@ -12,7 +28,7 @@ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
|
12
28
|
if (auth.decision === "block")
|
|
13
29
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
14
30
|
if (auth.decision === "route") {
|
|
15
|
-
await condition(() => verdicts.has(auth.decisionId));
|
|
31
|
+
await condition(() => verdicts.has(auth.decisionId)); // durable wait on the signal
|
|
16
32
|
const v = verdicts.get(auth.decisionId);
|
|
17
33
|
assertVerdict(verdictFrom(v), auth.decisionId, v.reason);
|
|
18
34
|
}
|