@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 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 outcome.
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** (it can request approvals
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's deny-by-default **routes every
90
- request to a human** — that is the point: nothing executes without either a named policy rule or
91
- a person. The script prints the decision id and waits. (To later see a policy `proceed`,
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** — the parked entry is claimed on completion, so a
196
- duplicate signal can't double-write.
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. See each adapter's header for its wiring, and `example/` in the repo for
205
- runnable end-to-ends.
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
- - `init [agentId]` — sign in, register, mint the agent token, write `.env` (+ `.gitignore` entries)
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**
@@ -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;
@@ -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 {};
@@ -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)); // durable wait on the signal
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
  }