@decidio/sdk 0.1.5 → 0.1.7

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/dist/guard.js CHANGED
@@ -1,7 +1,3 @@
1
- // The Guard facade — `guard.protect(fn, describe)`, the one agnostic line (TS mirror
2
- // of decidio-guard Python). A thin layer over the verified primitives: durable mode
3
- // builds a resume controller and suspends; blocking mode polls (opt-in, loud). The
4
- // agent executes its own action; Decidio gates + records + signals — never executes.
5
1
  import { withApproval } from "./index.js";
6
2
  import { createDecidioResume, FilePendingStore } from "./resume.js";
7
3
  export class Guard {
@@ -26,13 +22,9 @@ export class Guard {
26
22
  appUrl: process.env.DECIDIO_APP_URL,
27
23
  });
28
24
  }
29
- /** Mount on a node/Express/Fastify route: `app.post("/decidio/resume", guard.resumeHandler())`. */
25
+ register(action, fn) { this.ensureResume().register(action, fn); }
30
26
  resumeHandler() { return this.ensureResume().webhookHandler(); }
31
- /** Mount on a Next.js/Hono/CF/Bun route: `export const POST = (req) => guard.resumeFetchHandler()(req)`. */
32
27
  resumeFetchHandler() { return this.ensureResume().fetchHandler(); }
33
- /** Durable resume with NOTHING to host: a background watcher polls Decidio outward and finishes
34
- * any parked approval — so a suspended action survives a process restart. One line at startup.
35
- * Production may swap this for the webhook (resumeUrl); the suspend/resume shape is identical. */
36
28
  worker(opts) {
37
29
  return this.ensureResume().worker(opts);
38
30
  }
@@ -42,8 +34,6 @@ export class Guard {
42
34
  this.resume = createDecidioResume({
43
35
  apiUrl: c.apiUrl, apiToken: c.apiToken, webhookSecret: c.webhookSecret,
44
36
  resumeUrl: c.resumeUrl, store: new FilePendingStore(c.pendingDir ?? ".decidio-pending"),
45
- // Carry the signing identity so the durable/cold confirm on resume is signed too (incl. the
46
- // workspace binding — D-B3.6 — so a cold-resume confirm proof is tenant-scoped as well).
47
37
  agentId: c.agentId, agentKey: c.agentKey, agentDid: c.agentDid, workspaceId: c.workspaceId,
48
38
  fetchImpl: c.fetchImpl,
49
39
  });
@@ -52,9 +42,7 @@ export class Guard {
52
42
  }
53
43
  protect(fn, describe, opts) {
54
44
  if (opts?.adapter && opts.adapter !== "generic") {
55
- // Engine adapters are bound explicitly via @decidio/sdk/adapters (they need the
56
- // engine handle at call time). Surfaced as a clear error until wired in this build.
57
- throw new Error(`[decidio] adapter "${opts.adapter}" must be bound via @decidio/sdk/adapters/${opts.adapter} (it needs the engine handle).`);
45
+ throw new Error(`[decidio] adapter "${opts.adapter}" must be bound via the @decidio/sdk/${opts.adapter} subpath (it needs the engine handle). See that adapter's exports.`);
58
46
  }
59
47
  const c = this.config;
60
48
  const base = {
@@ -64,13 +52,12 @@ export class Guard {
64
52
  if ((opts?.mode ?? "durable") === "blocking") {
65
53
  console.warn("[decidio] WARNING: blocking wait — only safe for short, watched approvals. " +
66
54
  "Omit mode:\"blocking\" for durable async resume on real (minutes-to-days) approvals.");
67
- // No resumeUrl on the blocking path — the server must not fire a webhook to an
68
- // endpoint a blocking agent isn't serving; the poll is the only resume channel.
69
55
  return withApproval(fn, describe, base);
70
56
  }
71
- // durable: refuse-don't-downgrade — always wire a resume transport, never silently block.
72
- return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume: this.ensureResume() });
57
+ const resume = this.ensureResume();
58
+ if (opts?.action)
59
+ resume.register(opts.action, fn);
60
+ return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume });
73
61
  }
74
62
  }
75
- /** Preconfigured singleton — `import { guard } from "@decidio/sdk"`. */
76
63
  export const guard = Guard.fromEnv();
package/dist/index.d.ts CHANGED
@@ -6,49 +6,22 @@ export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
6
6
  export { generateKeypair, signBoundProof, signConfirm, canonicalize } from "./signing.js";
7
7
  export type { RequesterIdentity } from "./signing.js";
8
8
  export interface GuardConfig {
9
- /** Base URL of the Decidio gate, e.g. "https://api.decidio.io". */
10
9
  apiUrl: string;
11
- /** The agent identity registered with Decidio (onboarding wizard), e.g. "salesforce/SalesforceBot". */
12
10
  agentId: string;
13
- /** Bearer token for the REST floor (a trusted, already-authenticated internal
14
- * caller). Lives ONLY in the agent host's environment — never in client code. */
15
11
  apiToken?: string;
16
- /** Source system label sealed onto the receipt (e.g. "salesforce"). */
17
12
  sourceSystem?: string;
18
- /** The agent's PRIVATE signing key (PKCS8 base64), generated locally by `init` and held
19
- * ONLY in the agent host's env as DECIDIO_AGENT_KEY. When set (with `agentDid`), the SDK
20
- * signs a request-bound identity proof on each authorize so the floor PROVES the requester
21
- * instead of trusting the label — required for a registered agent to auto-approve. */
22
13
  agentKey?: string;
23
- /** The agent's PUBLIC did:key (the one registered with Decidio). Pairs with `agentKey`. */
24
14
  agentDid?: string;
25
- /** The workspace this agent acts in (DECIDIO_WORKSPACE_ID, from the owner's agent-token mint).
26
- * Bound into the signed proof (D-B3.6) so a captured proof can't be replayed in another tenant.
27
- * For a tester's per-workspace agent token this is the tester's workspace id. */
28
15
  workspaceId?: string;
29
- /** Poll cadence + ceiling while a routed action awaits human approval. Used ONLY
30
- * by the blocking transport (no `resume` configured). */
31
16
  pollIntervalMs?: number;
32
17
  pollTimeoutMs?: number;
33
- /** Optional async notifier so a long human wait surfaces in your logs/UI. */
34
18
  onPending?: (info: {
35
19
  decisionId: string;
36
20
  receiptId: string;
37
21
  url: string | null;
38
22
  }) => void;
39
- /** Base URL of the Decidio app (DECIDIO_APP_URL). When set, a routed action can say WHERE to
40
- * approve it — `<appUrl>/d/<decisionId>` opens that decision directly, on web or on the phone
41
- * build. This link used to be built only inside our own example, so a developer who pasted the
42
- * env block set DECIDIO_APP_URL and nothing ever read it. */
43
23
  appUrl?: string;
44
- /** Durable async resume. When set, a routed action SUSPENDS (parks its args in the
45
- * store, throws DecidioSuspendedError) instead of blocking — the process may exit
46
- * and resume later via the controller's webhook handler or poll worker. This is
47
- * the production transport for real (minutes-to-days) human approvals. */
48
24
  resume?: ResumeController;
49
- /** Callback URL Decidio POSTs the verdict to on approval (webhook transport).
50
- * Sent on /agent/authorize so the decision carries it. Falls back to
51
- * resume.resumeUrl. Omit when using the poll worker. */
52
25
  resumeUrl?: string;
53
26
  fetchImpl?: typeof fetch;
54
27
  }
@@ -56,9 +29,6 @@ export interface ApprovalContext {
56
29
  action: string;
57
30
  amount: number;
58
31
  scope?: string;
59
- /** Optional human-facing DETAILS of the action (e.g. { Account, "Lead Source", Stage }) shown to
60
- * the approver as "what you're approving". Display-only — the policy decides on `amount`/`scope`,
61
- * never these. Residual-safe tokenized server-side before persistence. */
62
32
  context?: Record<string, string | number>;
63
33
  }
64
34
  export declare class DecidioBlockedError extends Error {
@@ -75,22 +45,11 @@ export declare class DecidioTimeoutError extends Error {
75
45
  decisionId: string;
76
46
  constructor(decisionId: string);
77
47
  }
78
- /** A transport failure with GUIDANCE instead of a raw undici stack (round-4 Codex catch: the CLI
79
- * wrapped network errors kindly but the runtime leaked `TypeError: fetch failed`). Carries the
80
- * endpoint, the operation, the underlying code, and the safety state a caller cares about most:
81
- * whether anything could have executed. */
82
48
  export declare class DecidioNetworkError extends Error {
83
49
  endpoint: string;
84
50
  op: string;
85
51
  code: string | undefined;
86
52
  constructor(endpoint: string, op: string, code: string | undefined, cause: unknown);
87
53
  }
88
- /**
89
- * Wrap an async action behind the Decidio gate. `describe` extracts the approval
90
- * context (action, amount, scope) from the call arguments. Returns a function with
91
- * the SAME signature as `fn` — drop-in.
92
- */
93
- /** `<appUrl>/d/<decisionId>` — the deep link that opens this decision directly, on the web app or
94
- * the phone build. Null when DECIDIO_APP_URL is not configured, never a half-built URL. */
95
54
  export declare function decisionUrl(appUrl: string | undefined, decisionId: string): string | null;
96
55
  export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig): (...args: A) => Promise<R>;
package/dist/index.js CHANGED
@@ -1,32 +1,3 @@
1
- // @decidio/sdk — the one-line approval gate for AI-agent actions.
2
- //
3
- // import { guard } from "@decidio/sdk";
4
- // const createOpportunity = guard.protect(createOpportunityRaw, (a) => ({
5
- // action: "createOpportunity", amount: a.amount, scope: "Opportunity",
6
- // }));
7
- //
8
- // NOTE: `guard` is the preconfigured SINGLETON (a Guard instance, see guard.ts). It is NOT a
9
- // GuardConfig, so it cannot be passed as withApproval's third argument — this comment used to
10
- // show exactly that, and the error propagated into the product's own copy-paste snippets.
11
- // `withApproval(fn, describe, config)` is the lower-level export and takes a config OBJECT.
12
- //
13
- // On call, the wrapper asks Decidio's gate to authorize the action. Decidio's
14
- // policy engine (Cedar / computeChain) returns proceed | route | block:
15
- // • proceed → the wrapped fn runs immediately (auto-approved under policy)
16
- // • route → the wrapper WAITS for a human to approve in Decidio's queue, then
17
- // runs the fn (or throws if rejected)
18
- // • block → the wrapper throws; the fn never runs
19
- // The wrapper is a LISTENER: it captures the real source-system response the wrapped
20
- // fn returns and reports it to Decidio, which seals an `application_confirmed`
21
- // Authority Receipt (Decidio minimizes + tokenizes before sealing — the immutable
22
- // seal never stores raw payloads). The agent executes its own action; Decidio
23
- // authorizes, records, and (optionally) independently verifies it. Decidio holds NO
24
- // write credentials for your system — only the authority decision and the record.
25
- //
26
- // Framework-agnostic by design: it wraps at the tool-call boundary, so it works with
27
- // any agent runtime (LangGraph, OpenAI Agents SDK, CrewAI, a raw tool loop) and any
28
- // action (Salesforce, M365, SAP, a DB write) — the wrapper doesn't care what the fn
29
- // does, only that it returns the source response.
30
1
  import { DecidioSuspendedError } from "./resume.js";
31
2
  import { signBoundProof, signConfirm } from "./signing.js";
32
3
  export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume } from "./resume.js";
@@ -61,10 +32,6 @@ export class DecidioTimeoutError extends Error {
61
32
  }
62
33
  }
63
34
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
64
- /** A transport failure with GUIDANCE instead of a raw undici stack (round-4 Codex catch: the CLI
65
- * wrapped network errors kindly but the runtime leaked `TypeError: fetch failed`). Carries the
66
- * endpoint, the operation, the underlying code, and the safety state a caller cares about most:
67
- * whether anything could have executed. */
68
35
  export class DecidioNetworkError extends Error {
69
36
  endpoint;
70
37
  op;
@@ -109,13 +76,6 @@ async function get(cfg, path) {
109
76
  throw new Error(`decidio ${path} ${res.status}`);
110
77
  return res.json();
111
78
  }
112
- /**
113
- * Wrap an async action behind the Decidio gate. `describe` extracts the approval
114
- * context (action, amount, scope) from the call arguments. Returns a function with
115
- * the SAME signature as `fn` — drop-in.
116
- */
117
- /** `<appUrl>/d/<decisionId>` — the deep link that opens this decision directly, on the web app or
118
- * the phone build. Null when DECIDIO_APP_URL is not configured, never a half-built URL. */
119
79
  export function decisionUrl(appUrl, decisionId) {
120
80
  if (!appUrl)
121
81
  return null;
@@ -127,10 +87,6 @@ export function withApproval(fn, describe, config) {
127
87
  const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
128
88
  return async (...args) => {
129
89
  const ctx = describe(...args);
130
- // 1. ask the gate (carry the resume URL so a routed decision knows where to call back).
131
- // When a signing key is configured, sign a request-bound identity proof so the floor
132
- // can PROVE this agent rather than trust the `requester` label (registered agents must
133
- // sign to auto-approve). Unsigned still works for generic/non-agent callers.
134
90
  const requesterIdentity = (config.agentKey && config.agentDid)
135
91
  ? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
136
92
  : undefined;
@@ -141,48 +97,28 @@ export function withApproval(fn, describe, config) {
141
97
  ...(requesterIdentity ? { requesterIdentity } : {}),
142
98
  ...(resumeUrl ? { resumeUrl } : {}),
143
99
  });
144
- // Allow-list, not deny-list: an unrecognised verdict means this SDK is older than the
145
- // server, and the safe reading of an answer you do not understand is NO. Without this, the
146
- // day a fourth verdict ships every deployed SDK falls through and executes it.
147
100
  if (auth.decision !== "proceed" && auth.decision !== "route" && auth.decision !== "block")
148
101
  throw new DecidioBlockedError(`unrecognized gate verdict: ${String(auth.decision)}`, auth.decisionId);
149
102
  if (auth.decision === "block")
150
103
  throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
151
- // 2. routed → either SUSPEND for async resume (durable) or BLOCK-poll (interactive)
152
104
  if (auth.decision === "route") {
153
- // ALWAYS hand back the decision id — an agent that suspends and says only "waiting" leaves
154
- // the operator hunting through a queue, and an agent that says NOTHING (the pre-0.1.2 bug:
155
- // the print was gated on appUrl, which init never writes) leaves them staring at a silent
156
- // terminal. The Codex acceptance run failed exactly this claim; the id line is unconditional
157
- // now, with the deep link when appUrl is configured and the CLI command when not.
158
105
  const approvalUrl = decisionUrl(config.appUrl, auth.decisionId);
159
106
  if (config.onPending)
160
107
  config.onPending({ decisionId: auth.decisionId, receiptId: auth.receiptId, url: approvalUrl });
161
108
  else
162
109
  console.log(`[decidio] ${ctx.action} is awaiting approval (decision ${auth.decisionId}) — approve it ${approvalUrl ? `at ${approvalUrl}` : `in your Decidio queue, or: npx @decidio/sdk approvals approve ${auth.decisionId}`}`);
163
- // Durable transport: park the args agent-side, register the raw fn so a (cold)
164
- // resume can replay it, and SUSPEND. The process may exit now; the controller's
165
- // webhook handler or poll worker re-executes THIS fn when a human approves.
166
- // Decidio never executes — it only signals.
167
110
  if (config.resume) {
168
- // Propagate the agent's signing identity to the resume controller so the eventual
169
- // (possibly cold) confirm on resume is SIGNED too — earning application_confirmed on
170
- // the durable path, not just inline. (No-op if the controller already has its own.)
171
111
  config.resume.agentId ??= config.agentId;
172
112
  config.resume.agentKey ??= config.agentKey;
173
113
  config.resume.agentDid ??= config.agentDid;
174
114
  config.resume.register(ctx.action, fn);
175
- await config.resume.store.put({
115
+ await config.resume.park({
176
116
  decisionId: auth.decisionId, receiptId: auth.receiptId,
177
- action: ctx.action, amount: ctx.amount, scope: ctx.scope,
178
- args, createdAt: new Date().toISOString(),
117
+ action: ctx.action, amount: ctx.amount, scope: ctx.scope, args,
179
118
  });
180
119
  throw new DecidioSuspendedError(auth.decisionId, ctx.action);
181
120
  }
182
- // Interactive transport (no resume configured): block until decided. Only
183
- // appropriate for short, watched approvals (demos, synchronous tools).
184
121
  const deadline = Date.now() + pollTimeout;
185
- // eslint-disable-next-line no-constant-condition
186
122
  while (true) {
187
123
  const st = await get(config, `/agent/status/${encodeURIComponent(auth.decisionId)}`);
188
124
  if (st.state === "approved" || st.state === "auto_approved")
@@ -192,39 +128,25 @@ export function withApproval(fn, describe, config) {
192
128
  if (st.state === "blocked")
193
129
  throw new DecidioBlockedError(st.reason ?? "blocked", auth.decisionId);
194
130
  if (Date.now() > deadline) {
195
- // The founder-visible failure of blocking: the wait has a ceiling, and past it the
196
- // work is LOST — the decision stays open in Decidio but nothing is parked to resume.
197
- // Say so, say the limit, and say the durable way out.
198
131
  console.warn(`[decidio] blocking wait hit its ${Math.round(pollTimeout / 1000)}s limit — decision ${auth.decisionId} is still open in Decidio, but this process stopped waiting and nothing was executed. Re-run to request again, or use durable resume (guard.worker()) so an approval is never lost.`);
199
132
  throw new DecidioTimeoutError(auth.decisionId);
200
133
  }
201
134
  await sleep(pollInterval);
202
135
  }
203
136
  }
204
- // 3. authorized inline (proceed, or interactive route just approved) → run the
205
- // REAL action and capture the source response (listener)
206
137
  const result = await fn(...args);
207
- // 4. report the captured outcome → Decidio seals application_confirmed
208
- // (server minimizes + tokenizes the response before sealing). Best-effort:
209
- // a confirmation failure must not undo a successful business action.
210
138
  try {
211
- // Sign the confirmation too (bound to {action:"confirm", scope:decisionId}) so the gate
212
- // can PROVE this is the receipt's original requester before granting the wrapper tier
213
- // (application_confirmed). Without a key the report is capped at agent_asserted server-side.
214
139
  const confirmIdentity = signConfirm(auth.decisionId, config);
215
140
  await post(config, "/agent/confirm", {
216
141
  decisionId: auth.decisionId,
217
142
  capturedResponse: result ?? null,
218
- source: "wrapper", // the SDK captured the genuine response, not an agent self-report
143
+ source: "wrapper",
219
144
  ...(confirmIdentity ? { requesterIdentity: confirmIdentity } : {}),
220
145
  });
221
- // An UNSIGNED confirm is server-capped at the weaker agent_asserted tier. Say so at the
222
- // moment it happens — a silent evidence downgrade was round 3's acceptance FAIL.
223
146
  if (!confirmIdentity)
224
147
  console.warn(`[decidio] execution evidence for ${auth.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 (npx @decidio/sdk init writes them) to earn application_confirmed.`);
225
148
  }
226
149
  catch (e) {
227
- // surface but don't throw — the action already happened
228
150
  console.warn(`[decidio] confirmation post failed (action succeeded): ${e.message}`);
229
151
  }
230
152
  return result;
package/dist/inngest.d.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  import type { RequestListener } from "node:http";
2
2
  import { verifyResumeSignature } from "./resume.js";
3
3
  import type { GuardConfig, ApprovalContext } from "./index.js";
4
- /** The Inngest event Decidio's verdict is translated into. The waiting function
5
- * correlates on `data.decisionId`. */
6
4
  export declare const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
7
- /** The minimal slices of the Inngest step + client APIs we use, typed structurally so
8
- * this file doesn't force an `inngest` type dependency on the core build. */
9
5
  export interface InngestStepLike {
10
6
  run<T>(id: string, fn: () => Promise<T>): Promise<T>;
11
7
  waitForEvent(id: string, opts: {
@@ -29,16 +25,6 @@ export type GateStepOutcome<R> = {
29
25
  decisionId: string;
30
26
  result?: R;
31
27
  };
32
- /**
33
- * Gate an action INSIDE an Inngest function, with durable suspend on routing.
34
- * • proceed → run the action (as a step) + confirm.
35
- * • route → `step.waitForEvent` durably suspends until Decidio's resume event for
36
- * THIS decision arrives; approved → run + confirm; rejected → status.
37
- * • block → return blocked (the action never runs).
38
- * The action runs in YOUR process with YOUR credentials — Decidio only signals.
39
- * `config.resumeUrl` must point at the bridge endpoint (createInngestResumeBridge),
40
- * so Decidio's webhook is translated into the event this step waits on.
41
- */
42
28
  export declare function decidioGateStep<R>(args: {
43
29
  step: InngestStepLike;
44
30
  config: GuardConfig;
@@ -46,12 +32,6 @@ export declare function decidioGateStep<R>(args: {
46
32
  run: () => Promise<R>;
47
33
  timeout?: string;
48
34
  }): Promise<GateStepOutcome<R>>;
49
- /**
50
- * Decidio → Inngest bridge. Mount as an HTTP endpoint and register it as the agent's
51
- * `resumeUrl`. It verifies Decidio's signed resume webhook (same HMAC as the core
52
- * handler) and emits `DECIDIO_RESUME_EVENT` into Inngest, waking the suspended
53
- * `decidioGateStep`. Fails CLOSED without a configured secret (unless allowUnsigned).
54
- */
55
35
  export declare function createInngestResumeBridge(opts: {
56
36
  inngest: InngestClientLike;
57
37
  webhookSecret?: string;
package/dist/inngest.js CHANGED
@@ -1,26 +1,12 @@
1
1
  import { verifyResumeSignature } from "./resume.js";
2
2
  import { signBoundProof, signConfirm } from "./signing.js";
3
- /** The Inngest event Decidio's verdict is translated into. The waiting function
4
- * correlates on `data.decisionId`. */
5
3
  export const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
6
- /**
7
- * Gate an action INSIDE an Inngest function, with durable suspend on routing.
8
- * • proceed → run the action (as a step) + confirm.
9
- * • route → `step.waitForEvent` durably suspends until Decidio's resume event for
10
- * THIS decision arrives; approved → run + confirm; rejected → status.
11
- * • block → return blocked (the action never runs).
12
- * The action runs in YOUR process with YOUR credentials — Decidio only signals.
13
- * `config.resumeUrl` must point at the bridge endpoint (createInngestResumeBridge),
14
- * so Decidio's webhook is translated into the event this step waits on.
15
- */
16
4
  export async function decidioGateStep(args) {
17
5
  const { step, config, ctx } = args;
18
6
  const base = config.apiUrl.replace(/\/$/, "");
19
7
  const f = config.fetchImpl ?? fetch;
20
8
  const authHeaders = { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) };
21
- // 1. authorize (memoized as a step, so a replay doesn't re-ask the gate)
22
9
  const auth = await step.run("decidio-authorize", async () => {
23
- // Sign the bound proof so an Inngest-path agent can prove its identity + auto-approve.
24
10
  const requesterIdentity = (config.agentKey && config.agentDid)
25
11
  ? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
26
12
  : undefined;
@@ -40,9 +26,6 @@ export async function decidioGateStep(args) {
40
26
  if (auth.decision === "block")
41
27
  return { status: "blocked", decisionId: auth.decisionId };
42
28
  if (auth.decision === "route") {
43
- // 2. durably SUSPEND until the resume event for THIS decision arrives. Zero
44
- // compute while waiting; the worker/process may be recycled — Inngest resumes
45
- // from this checkpoint. `if` correlates the awaited event to this decisionId.
46
29
  const ev = await step.waitForEvent("decidio-await-approval", {
47
30
  event: DECIDIO_RESUME_EVENT,
48
31
  timeout: args.timeout ?? "30d",
@@ -56,8 +39,6 @@ export async function decidioGateStep(args) {
56
39
  if (verdict !== "approved" && verdict !== "auto_approved")
57
40
  return { status: "blocked", decisionId: auth.decisionId };
58
41
  }
59
- // 3. authorized → perform the agent's OWN action (a step), then confirm the captured
60
- // response so Decidio seals application_confirmed (never executes it itself).
61
42
  const result = await step.run("agent-action", args.run);
62
43
  await step.run("decidio-confirm", async () => {
63
44
  const requesterIdentity = signConfirm(auth.decisionId, config);
@@ -69,12 +50,6 @@ export async function decidioGateStep(args) {
69
50
  });
70
51
  return { status: "executed", decisionId: auth.decisionId, result };
71
52
  }
72
- /**
73
- * Decidio → Inngest bridge. Mount as an HTTP endpoint and register it as the agent's
74
- * `resumeUrl`. It verifies Decidio's signed resume webhook (same HMAC as the core
75
- * handler) and emits `DECIDIO_RESUME_EVENT` into Inngest, waking the suspended
76
- * `decidioGateStep`. Fails CLOSED without a configured secret (unless allowUnsigned).
77
- */
78
53
  export function createInngestResumeBridge(opts) {
79
54
  const secret = opts.webhookSecret ?? "";
80
55
  const eventName = opts.eventName ?? DECIDIO_RESUME_EVENT;
@@ -110,5 +85,4 @@ export function createInngestResumeBridge(opts) {
110
85
  });
111
86
  };
112
87
  }
113
- // Re-export the signature check so a host can verify the webhook out-of-band if needed.
114
88
  export { verifyResumeSignature };
package/dist/resume.d.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  import type { RequestListener } from "node:http";
2
- /** A routed action parked on the agent side, awaiting human approval. The `args`
3
- * are the original call arguments, replayed verbatim when the action resumes. */
4
2
  export interface PendingAction {
5
3
  decisionId: string;
6
4
  receiptId?: string;
@@ -9,22 +7,17 @@ export interface PendingAction {
9
7
  scope?: string;
10
8
  args: unknown[];
11
9
  createdAt: string;
10
+ argsSig?: {
11
+ kind: "ed25519" | "hmac";
12
+ sig: string;
13
+ };
12
14
  }
13
- /** Where suspended actions live between request and approval. The default is a
14
- * file store; swap in Redis/Postgres/your queue for production. AGENT-SIDE only —
15
- * Decidio never sees this. */
16
15
  export interface PendingStore {
17
16
  put(p: PendingAction): Promise<void>;
18
17
  get(decisionId: string): Promise<PendingAction | null>;
19
18
  delete(decisionId: string): Promise<void>;
20
19
  list(): Promise<PendingAction[]>;
21
- /** Atomically take ownership of a parked action for execution. Returns true if THIS
22
- * caller claimed it, false if it's already claimed/gone — the cross-process single-use
23
- * guarantee (a production store backs this with rename/SETNX/SELECT-FOR-UPDATE). A
24
- * claimed entry is excluded from list(). Optional: if absent, the controller falls
25
- * back to an in-process guard (same-process only). */
26
20
  claim?(decisionId: string): Promise<boolean>;
27
- /** Undo a claim so a failed execution is retryable. */
28
21
  release?(decisionId: string): Promise<void>;
29
22
  }
30
23
  export declare class MemoryPendingStore implements PendingStore {
@@ -37,11 +30,6 @@ export declare class MemoryPendingStore implements PendingStore {
37
30
  claim(id: string): Promise<boolean>;
38
31
  release(id: string): Promise<void>;
39
32
  }
40
- /** File-backed store so a COLD process (the original requester long gone) can still
41
- * resume — the proof that nothing is "kept alive". One JSON file per decision. A claim
42
- * renames `<id>.json` → `<id>.claimed` atomically (same-volume rename), so two processes
43
- * sharing the dir can't both execute. (A stale `.claimed` from a crash mid-execute needs
44
- * TTL recovery — tracked in the backlog.) */
45
33
  export declare class FilePendingStore implements PendingStore {
46
34
  private dir;
47
35
  constructor(dir: string);
@@ -55,10 +43,6 @@ export declare class FilePendingStore implements PendingStore {
55
43
  claim(id: string): Promise<boolean>;
56
44
  release(id: string): Promise<void>;
57
45
  }
58
- /** Thrown by a `withApproval`-wrapped call when the action is routed and configured
59
- * for async resume: the action is now SUSPENDED (parked in the store), not failed.
60
- * The caller can stop here — the process may even exit; the resume transport will
61
- * complete it later. */
62
46
  export declare class DecidioSuspendedError extends Error {
63
47
  decisionId: string;
64
48
  action: string;
@@ -72,58 +56,30 @@ export interface ResumeOutcome {
72
56
  result?: unknown;
73
57
  reason?: string;
74
58
  }
75
- /** Minimal config the controller needs to confirm outcomes + poll status. Shared
76
- * with GuardConfig (apiUrl/apiToken). */
77
59
  export interface ResumeConfig {
78
60
  apiUrl: string;
79
61
  apiToken?: string;
80
- /** Agent identity for SIGNING the confirm call on resume (so the durable path can earn the
81
- * application_confirmed tier, not just the inline path). Default from DECIDIO_AGENT_ID/
82
- * _KEY/_DID env when unset — a cold resume process that has the env signs automatically. */
83
62
  agentId?: string;
84
63
  agentKey?: string;
85
64
  agentDid?: string;
86
- /** The workspace the agent acts in (DECIDIO_WORKSPACE_ID) — bound into the cold-resume confirm
87
- * proof (D-B3.6) so it's tenant-scoped too. Default from env when unset. */
88
65
  workspaceId?: string;
89
- /** Shared secret Decidio signs the resume webhook with (HMAC-SHA256). Defaults to
90
- * the API token if unset. Verified before any re-execution. */
91
66
  webhookSecret?: string;
92
- /** Escape hatch to accept UNSIGNED resume webhooks (no secret). Off by default:
93
- * with no secret and this unset, the webhook handler rejects every call (fail
94
- * closed) so a misconfigured agent can't be driven by forged callbacks. Only set
95
- * true behind a trusted network boundary (e.g. mTLS / private mesh). */
96
67
  allowUnsigned?: boolean;
97
- /** The callback URL Decidio should POST the verdict to (webhook transport). Sent
98
- * on /agent/authorize so the decision carries it; omit when using the poll worker. */
99
68
  resumeUrl?: string;
100
- /** Fired after every terminal resolution (executed / rejected / blocked / no_handler),
101
- * via either transport. Use it to log, close a listener, or exit a CLI. */
102
69
  onResolved?: (o: ResumeOutcome) => void;
103
70
  fetchImpl?: typeof fetch;
104
71
  }
105
- /** Re-execute the agent's own fn on approval, confirm the captured outcome to
106
- * Decidio, and clear the parked entry. Bound to a store + a registry of raw fns. */
107
72
  export interface ResumeController extends ResumeConfig {
108
73
  store: PendingStore;
109
- /** Register a RAW fn under its action name so a (possibly cold) resume can replay
110
- * it. `withApproval` auto-registers on the hot path; a cold worker/service should
111
- * register at startup so it can resume actions it never originated. */
112
74
  register(action: string, fn: (...args: any[]) => Promise<any>): void;
113
- /** Drive a parked decision to completion once a verdict is known. Idempotent:
114
- * a second call after success finds nothing parked → "unknown" (no double-write). */
75
+ park(p: Omit<PendingAction, "createdAt" | "argsSig">): Promise<void>;
115
76
  resolve(input: {
116
77
  decisionId: string;
117
78
  verdict: "approved" | "rejected" | "blocked";
118
79
  reason?: string;
119
80
  }): Promise<ResumeOutcome>;
120
- /** node:http listener for Decidio's signed approval webhook (Express/Fastify/node). */
121
81
  webhookHandler(): RequestListener;
122
- /** Web-Fetch handler (Request → Response) for Next.js route handlers, Hono,
123
- * Cloudflare Workers, Bun, Deno. Same fail-closed signature check. */
124
82
  fetchHandler(): (req: Request) => Promise<Response>;
125
- /** Poll-based resume for hosts with no inbound URL (CLI/laptop). A long-lived
126
- * watcher — this is a durable WORKER, not the request path blocking. */
127
83
  worker(opts?: {
128
84
  intervalMs?: number;
129
85
  timeoutMs?: number;
@@ -133,19 +89,7 @@ export interface ResumeController extends ResumeConfig {
133
89
  };
134
90
  }
135
91
  declare function sign(secret: string, body: string): string;
136
- /** Verify Decidio's `x-decidio-signature` over the raw webhook body (HMAC-SHA256).
137
- * Exported so engine adapters (e.g. the Inngest bridge) reuse the exact check. */
138
92
  export declare function verifyResumeSignature(secret: string, body: string, header: string | undefined): boolean;
139
- /**
140
- * Build the resume controller. Spread it into your GuardConfig as `resume`, then
141
- * either mount `webhookHandler()` (production) or run `worker()` (CLI/laptop).
142
- *
143
- * const resume = createDecidioResume({ apiUrl, apiToken, store: new FilePendingStore(".decidio-pending"), resumeUrl });
144
- * const guard = { apiUrl, agentId, apiToken, resume };
145
- * const createOpportunity = withApproval(createOpportunityRaw, describe, guard);
146
- * // production: http.createServer(resume.webhookHandler()).listen(4100)
147
- * // or CLI: resume.worker()
148
- */
149
93
  export declare function createDecidioResume(opts: ResumeConfig & {
150
94
  store: PendingStore;
151
95
  }): ResumeController;