@decidio/sdk 0.1.4 → 0.1.6
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 +27 -12
- package/dist/adapters/_gate.d.ts +0 -2
- package/dist/adapters/_gate.js +0 -15
- package/dist/adapters/langgraph.d.ts +0 -3
- package/dist/adapters/langgraph.js +0 -10
- package/dist/adapters/openai.d.ts +0 -4
- package/dist/adapters/openai.js +0 -20
- package/dist/adapters/temporal.d.ts +0 -1
- package/dist/adapters/temporal.js +1 -13
- package/dist/cli.js +43 -113
- package/dist/guard.d.ts +0 -14
- package/dist/guard.js +1 -18
- package/dist/index.d.ts +0 -41
- package/dist/index.js +3 -81
- package/dist/inngest.d.ts +0 -20
- package/dist/inngest.js +0 -26
- package/dist/resume.d.ts +5 -61
- package/dist/resume.js +70 -66
- package/dist/signing.d.ts +0 -12
- package/dist/signing.js +0 -25
- package/openapi.yaml +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @decidio/sdk
|
|
2
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
|
|
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 decision, with execution evidence accruing on the record.
|
|
4
4
|
|
|
5
5
|
## The two-minute tour
|
|
6
6
|
|
|
@@ -41,13 +41,12 @@ npm install @decidio/sdk
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
export DECIDIO_API_URL=https://decidio-api.onrender.com # the hosted sandbox
|
|
44
|
-
npx @decidio/sdk init my-agent
|
|
44
|
+
npx @decidio/sdk init --unique my-agent # --unique appends a random suffix: collision-proof
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
`init` signs you into your workspace (email + password, or paste the sign-in link from your
|
|
48
48
|
workspace email), generates the agent's Ed25519 keypair **locally** (only the public `did:key`
|
|
49
|
-
is sent), registers the agent, mints its **floor-limited API token** (
|
|
50
|
-
and nothing else — your workspace session never touches the agent's disk), and writes `.env`.
|
|
49
|
+
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`.
|
|
51
50
|
Every later command reads `.env` from this directory automatically.
|
|
52
51
|
|
|
53
52
|
What `.env` contains after a successful init: `DECIDIO_API_URL`, `DECIDIO_AGENT_ID`,
|
|
@@ -86,9 +85,10 @@ console.log("executed after approval:", result);
|
|
|
86
85
|
node --env-file=.env quickstart.mjs
|
|
87
86
|
```
|
|
88
87
|
|
|
89
|
-
A brand-new agent matches **no** auto-approve rule, so Decidio
|
|
90
|
-
|
|
91
|
-
a person.
|
|
88
|
+
A brand-new agent matches **no** auto-approve rule, so Decidio **routes every request to a
|
|
89
|
+
human by default** — that is the point: nothing executes without either a named policy rule or
|
|
90
|
+
a person. (Routing-by-default is distinct from a policy **block**, which refuses the action
|
|
91
|
+
outright — no human can approve a blocked request.) The script prints the decision id and waits. (To later see a policy `proceed`,
|
|
92
92
|
register a **new** agent in the sandbox's pre-policied lane — an existing agent's lane can't be
|
|
93
93
|
changed: `DECIDIO_SOURCE_SYSTEM=salesforce npx @decidio/sdk init my-sf-agent`. That lane's
|
|
94
94
|
named rule auto-approves amounts under $1M when the request carries the agent's signed
|
|
@@ -192,8 +192,21 @@ also allow-list resume hosts (default-deny). That's operator territory: if you r
|
|
|
192
192
|
Decidio (or we run a pilot with you), the webhook is set up then. Against the hosted sandbox,
|
|
193
193
|
use the worker — a webhook secret your server never learned would (correctly) fail closed.
|
|
194
194
|
|
|
195
|
-
Either way, re-execution is **single-use
|
|
196
|
-
duplicate signal
|
|
195
|
+
Either way, re-execution is **single-use on the happy path**: the parked entry is claimed before
|
|
196
|
+
execution and deleted on success, so a duplicate signal finds nothing to replay. Honest edge — if
|
|
197
|
+
your action succeeds but the process dies *before* the entry is cleared, the claim can be retried
|
|
198
|
+
(robust crash-recovery of stale claims is on the roadmap), so make your downstream action
|
|
199
|
+
idempotent if you need exactly-once under crashes.
|
|
200
|
+
|
|
201
|
+
**Payload integrity.** When a routed action is parked, the SDK signs its arguments (bound to the
|
|
202
|
+
decision id) with your agent key; on resume it **refuses to execute** if the parked payload was
|
|
203
|
+
modified, its signature stripped, or the file swapped into another decision's slot. That defends
|
|
204
|
+
against tampering by anything lacking your agent's key — note a *fully compromised agent host*
|
|
205
|
+
can bypass the guard entirely (it holds both the key and the function), so the durable guarantee
|
|
206
|
+
that closes that gap is server-sealed argument binding, auditable in the receipt itself, which is
|
|
207
|
+
on the roadmap. **Upgrading from ≤0.1.5:** entries parked by an older version carry no signature
|
|
208
|
+
and are refused once your agent has a key — drain pending approvals before upgrading, or delete
|
|
209
|
+
the parked files and re-run those actions.
|
|
197
210
|
|
|
198
211
|
### Already on a durable engine? Use its native wait
|
|
199
212
|
|
|
@@ -201,8 +214,8 @@ If your agent runs on Inngest/Temporal/LangGraph, the engine *is* the durable st
|
|
|
201
214
|
`@decidio/sdk/inngest` maps the gate onto `step.waitForEvent`; `@decidio/sdk/langgraph` is a
|
|
202
215
|
drop-in via `interrupt()`; `@decidio/sdk/temporal` uses `condition` + signal;
|
|
203
216
|
`@decidio/sdk/openai` gates RunState approvals. All optional peer dependencies — the core SDK
|
|
204
|
-
imports none of them.
|
|
205
|
-
runnable end-to-
|
|
217
|
+
imports none of them. Each adapter's shipped `.d.ts` header documents its wiring; `npx
|
|
218
|
+
@decidio/sdk quickstart` is the fastest runnable end-to-end.
|
|
206
219
|
|
|
207
220
|
## One surface, every runtime — and Python too
|
|
208
221
|
|
|
@@ -213,10 +226,12 @@ runnable end-to-ends.
|
|
|
213
226
|
|
|
214
227
|
`npx @decidio/sdk <cmd>`:
|
|
215
228
|
|
|
216
|
-
- `
|
|
229
|
+
- `quickstart` — the guided two-minute tour (gate → approve → execute → proof)
|
|
230
|
+
- `init [agentId] [--unique]` — sign in, register, mint the agent token, write `.env` (+ `.gitignore` entries)
|
|
217
231
|
- `doctor` — config, connectivity, and what your current token can do
|
|
218
232
|
- `approvals` / `approvals approve|reject <id> [reason]` — the human side, from your terminal
|
|
219
233
|
- `receipt <id>` — download a sealed decision's Authority Receipt to a `.json` file
|
|
234
|
+
- `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)
|
|
220
235
|
- `dev [--target URL]` — local relay for the resume webhook (operator setups)
|
|
221
236
|
|
|
222
237
|
Commands that administer your workspace (`init`, `approvals`, `receipt`) use a **session**
|
package/dist/adapters/_gate.d.ts
CHANGED
|
@@ -7,7 +7,5 @@ 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. */
|
|
11
10
|
export declare function verdictFrom(payload: any): string;
|
|
12
|
-
/** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
|
|
13
11
|
export declare function assertVerdict(verdict: string, decisionId: string, reason?: string): void;
|
package/dist/adapters/_gate.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
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
1
|
import { DecidioBlockedError, DecidioRejectedError, signBoundProof, signConfirm } from "../index.js";
|
|
6
2
|
export async function gateAuthorize(config, ctx) {
|
|
7
3
|
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
4
|
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
5
|
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
14
6
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
15
7
|
: undefined;
|
|
@@ -29,10 +21,7 @@ export async function gateAuthorize(config, ctx) {
|
|
|
29
21
|
export async function gateConfirm(config, decisionId, result) {
|
|
30
22
|
const f = config.fetchImpl ?? fetch;
|
|
31
23
|
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
24
|
const requesterIdentity = signConfirm(decisionId, config);
|
|
35
|
-
// Same no-silent-downgrade rule as the inline wrap (guard equivalent-path catch).
|
|
36
25
|
if (!requesterIdentity)
|
|
37
26
|
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.`);
|
|
38
27
|
const res = await f(config.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
|
|
@@ -40,8 +29,6 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
40
29
|
headers: { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) },
|
|
41
30
|
body: JSON.stringify({ decisionId, capturedResponse: result ?? null, source: "wrapper", ...(requesterIdentity ? { requesterIdentity } : {}) }),
|
|
42
31
|
});
|
|
43
|
-
// fetch only rejects on network error — surface a non-2xx (auth/500) too. Best-effort:
|
|
44
|
-
// log, don't throw (the action already happened).
|
|
45
32
|
if (!res.ok)
|
|
46
33
|
console.warn(`[decidio] confirm returned ${res.status} (action succeeded)`);
|
|
47
34
|
}
|
|
@@ -49,11 +36,9 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
49
36
|
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
50
37
|
}
|
|
51
38
|
}
|
|
52
|
-
/** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
|
|
53
39
|
export function verdictFrom(payload) {
|
|
54
40
|
return (payload && typeof payload === "object" ? payload.verdict : payload) ?? "";
|
|
55
41
|
}
|
|
56
|
-
/** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
|
|
57
42
|
export function assertVerdict(verdict, decisionId, reason) {
|
|
58
43
|
if (verdict === "approved" || verdict === "auto_approved")
|
|
59
44
|
return;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { Command } from "@langchain/langgraph";
|
|
2
2
|
import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
3
|
-
/** Gate `run()` behind Decidio inside a LangGraph node. */
|
|
4
3
|
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
4
|
export declare function decidioResumeCommand(body: {
|
|
8
5
|
verdict?: string;
|
|
9
6
|
reason?: string;
|
|
@@ -1,19 +1,11 @@
|
|
|
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
1
|
import { interrupt, Command } from "@langchain/langgraph";
|
|
8
2
|
import { DecidioBlockedError } from "../index.js";
|
|
9
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
|
|
10
|
-
/** Gate `run()` behind Decidio inside a LangGraph node. */
|
|
11
4
|
export async function decidioGate(config, ctx, run) {
|
|
12
5
|
const auth = await gateAuthorize(config, ctx);
|
|
13
6
|
if (auth.decision === "block")
|
|
14
7
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
15
8
|
if (auth.decision === "route") {
|
|
16
|
-
// Durable pause. On resume, interrupt() returns the Command(resume=...) payload.
|
|
17
9
|
const resumed = interrupt({ decidio: { decisionId: auth.decisionId, action: ctx.action, amount: ctx.amount, scope: ctx.scope } });
|
|
18
10
|
assertVerdict(verdictFrom(resumed), auth.decisionId, resumed?.reason);
|
|
19
11
|
}
|
|
@@ -21,8 +13,6 @@ export async function decidioGate(config, ctx, run) {
|
|
|
21
13
|
await gateConfirm(config, auth.decisionId, result);
|
|
22
14
|
return result;
|
|
23
15
|
}
|
|
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
16
|
export function decidioResumeCommand(body) {
|
|
27
17
|
return new Command({ resume: { verdict: body.verdict, reason: body.reason } });
|
|
28
18
|
}
|
|
@@ -14,13 +14,9 @@ export interface Pending {
|
|
|
14
14
|
item: ToolApprovalItem;
|
|
15
15
|
decisionId: string;
|
|
16
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
17
|
export declare function gateInterruptions(config: GuardConfig, state: RunStateLike, describe: (item: ToolApprovalItem) => ApprovalContext): Promise<{
|
|
21
18
|
resolved: Resolved[];
|
|
22
19
|
pending: Pending[];
|
|
23
20
|
}>;
|
|
24
|
-
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
|
|
25
21
|
export declare function applyResume(state: RunStateLike, item: ToolApprovalItem, verdict: string): void;
|
|
26
22
|
export {};
|
package/dist/adapters/openai.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
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
1
|
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
2
|
export async function gateInterruptions(config, state, describe) {
|
|
22
3
|
const resolved = [];
|
|
23
4
|
const pending = [];
|
|
@@ -36,7 +17,6 @@ export async function gateInterruptions(config, state, describe) {
|
|
|
36
17
|
}
|
|
37
18
|
return { resolved, pending };
|
|
38
19
|
}
|
|
39
|
-
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
|
|
40
20
|
export function applyResume(state, item, verdict) {
|
|
41
21
|
if (verdict === "approved" || verdict === "auto_approved")
|
|
42
22
|
state.approve(item);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
2
2
|
import { type GateAuth } from "./_gate.js";
|
|
3
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
4
|
export declare function installDecidioSignal(): Map<string, {
|
|
6
5
|
verdict: string;
|
|
7
6
|
reason?: string;
|
|
@@ -1,19 +1,7 @@
|
|
|
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
1
|
import { condition, defineSignal, setHandler } from "@temporalio/workflow";
|
|
13
2
|
import { DecidioBlockedError } from "../index.js";
|
|
14
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
|
|
15
4
|
export const decidioResumeSignal = defineSignal("decidioResume");
|
|
16
|
-
/** Call inside the workflow to wire the resume signal; returns the verdict store. */
|
|
17
5
|
export function installDecidioSignal() {
|
|
18
6
|
const verdicts = new Map();
|
|
19
7
|
setHandler(decidioResumeSignal, (decisionId, verdict, reason) => { verdicts.set(decisionId, { verdict, reason }); });
|
|
@@ -24,7 +12,7 @@ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
|
24
12
|
if (auth.decision === "block")
|
|
25
13
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
26
14
|
if (auth.decision === "route") {
|
|
27
|
-
await condition(() => verdicts.has(auth.decisionId));
|
|
15
|
+
await condition(() => verdicts.has(auth.decisionId));
|
|
28
16
|
const v = verdicts.get(auth.decisionId);
|
|
29
17
|
assertVerdict(verdictFrom(v), auth.decisionId, v.reason);
|
|
30
18
|
}
|