@decidio/sdk 0.1.3 → 0.1.4

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
@@ -2,12 +2,33 @@
2
2
 
3
3
  One line to put a human-or-policy approval gate in front of any AI-agent action — and get a signed, verifiable Authority Receipt for every outcome.
4
4
 
5
+ ## The two-minute tour
6
+
7
+ ```bash
8
+ npx @decidio/sdk quickstart
9
+ ```
10
+
11
+ One command, guided: it registers a fresh agent (key generated locally), gates a demo action
12
+ with the same one line you'll use on your real function, routes it to a human — **you** — lets
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. Sign in when prompted, or
15
+ export `DECIDIO_SESSION_TOKEN` for headless terminals. No config: with no `DECIDIO_API_URL` set
16
+ it targets the hosted synthetic sandbox.
17
+
5
18
  ## Quickstart — seven steps to your first sealed receipt
6
19
 
7
20
  Under 30 minutes, no help needed. You'll need Node 20.6+, a terminal, and a Decidio workspace
8
21
  (request one at [decidioai.com](https://decidioai.com/#early-access) — the hosted sandbox is
9
- synthetic data, safe to experiment in). Shell commands below are Bash; on PowerShell, `export
10
- NAME=value` becomes `$env:NAME="value"`.
22
+ synthetic data, safe to experiment in).
23
+
24
+ **PowerShell users** — the equivalents for every Bash idiom used below:
25
+
26
+ ```powershell
27
+ mkdir decidio-quickstart; cd decidio-quickstart # instead of mkdir ... && cd ...
28
+ $env:DECIDIO_API_URL = "https://decidio-api.onrender.com" # instead of export NAME=value
29
+ $env:DECIDIO_SOURCE_SYSTEM = "salesforce"; npx @decidio/sdk init my-sf-agent
30
+ # instead of VAR=value npx ... (inline env)
31
+ ```
11
32
 
12
33
  **1. Install**
13
34
 
@@ -34,7 +55,8 @@ What `.env` contains after a successful init: `DECIDIO_API_URL`, `DECIDIO_AGENT_
34
55
  `application_confirmed` evidence later), `DECIDIO_API_TOKEN` (the floor-limited agent token),
35
56
  `DECIDIO_WORKSPACE_ID`, and `DECIDIO_WEBHOOK_SECRET` (operator deployments only). Pick any
36
57
  agent name — if it's already taken in your workspace, `init` stops cleanly and suggests a
37
- fresh one. Headless terminal (no prompts possible)? Export `DECIDIO_SESSION_TOKEN` first:
58
+ fresh one (or pass `--unique` to auto-append a random suffix — collision-proof for shared
59
+ workspaces and CI). Headless terminal (no prompts possible)? Export `DECIDIO_SESSION_TOKEN` first:
38
60
  request a sign-in link, then exchange its `?login=` value at `POST /demo/login-consume` — the
39
61
  response's `token` is your session token (the CLI prints these exact instructions when it
40
62
  can't prompt).
@@ -72,7 +94,11 @@ changed: `DECIDIO_SOURCE_SYSTEM=salesforce npx @decidio/sdk init my-sf-agent`. T
72
94
  named rule auto-approves amounts under $1M when the request carries the agent's signed
73
95
  identity proof, so even this $86k example would proceed there.)
74
96
 
75
- **5. Approve it** — in a second terminal (same directory):
97
+ **5. Approve it** — in a second terminal (same directory; replace `<decisionId>` with the id
98
+ step 4 printed — angle brackets are placeholders, never typed literally). Approvals are a
99
+ *human* act, so this terminal needs your sign-in too: it will prompt, or export
100
+ `DECIDIO_SESSION_TOKEN` here as well for headless shells (the `.env` deliberately holds only
101
+ the agent's floor-limited token — it cannot approve anything):
76
102
 
77
103
  ```bash
78
104
  npx @decidio/sdk approvals # see it pending
@@ -83,17 +109,19 @@ The moment you approve, the first terminal wakes: **your own function executes**
83
109
  holds your credentials or runs your code — and the SDK reports the captured result back, which
84
110
  becomes the record's execution evidence.
85
111
 
86
- **6. Download the receipt and verify it offline:**
112
+ **6. Download the receipt (online), then verify it (offline):**
87
113
 
88
114
  ```bash
89
115
  npx @decidio/sdk receipt <decisionId>
90
- npx --yes @decidio/verify decidio-receipt-<decisionId>.json
116
+ npx @decidio/sdk verify decidio-receipt-<decisionId>.json
91
117
  ```
92
118
 
93
- The receipt is a W3C Verifiable Credential: the signature and content binding verify with no
94
- Decidio account and no network (after the one-time `npx` package download) — the evidence is
95
- yours, not ours. To also prove *who* issued it, pin your workspace's issuer:
96
- `npx @decidio/verify --issuer <did:key:...> receipt.json` (the DID is shown in your app's
119
+ The download is an authenticated fetch from your workspace; the *verification* is fully
120
+ offline — the verifier ships inside this package (no second download, no registry probe, same
121
+ implementation as the standalone `@decidio/verify`). The receipt is a W3C Verifiable
122
+ Credential: signature and content binding verify with no Decidio account and no network — the
123
+ evidence is yours, not ours. To also prove *who* issued it, pin your workspace's issuer:
124
+ `npx @decidio/sdk verify --issuer <did:key:...> receipt.json` (the DID is shown in your app's
97
125
  record verify panel). One receipt proves itself; chain order across receipts is verified when
98
126
  you pass several files at once.
99
127
 
package/dist/cli.js CHANGED
@@ -24,6 +24,8 @@ import { createServer } from "node:http";
24
24
  import { createInterface } from "node:readline";
25
25
  import { createRequire } from "node:module";
26
26
  import { generateKeypair } from "./signing.js";
27
+ import { withApproval } from "./index.js";
28
+ import { verifyReceipt, verifyChain } from "@decidio/verify";
27
29
  const VERSION = (() => {
28
30
  try {
29
31
  return createRequire(import.meta.url)("../package.json").version;
@@ -55,7 +57,10 @@ for (const [k, v] of Object.entries(readEnvFile())) {
55
57
  if (process.env[k] === undefined)
56
58
  process.env[k] = v;
57
59
  }
58
- const API = (process.env.DECIDIO_API_URL ?? "http://localhost:4000").replace(/\/$/, "");
60
+ // `quickstart` is the zero-config guided tour: with no DECIDIO_API_URL anywhere it targets the
61
+ // hosted sandbox (and says so); every other command keeps the localhost dev default.
62
+ const IS_QUICKSTART = process.argv[2] === "quickstart";
63
+ const API = (process.env.DECIDIO_API_URL ?? (IS_QUICKSTART ? "https://decidio-api.onrender.com" : "http://localhost:4000")).replace(/\/$/, "");
59
64
  // Token is read AT CALL TIME, not captured at import: init mints a fresh agent token mid-run and
60
65
  // its closing doctor must probe with THAT, not the pre-init env (round-3 Codex catch — the stale
61
66
  // capture made init's own doctor print "token: REJECTED" seconds after a successful mint).
@@ -433,7 +438,7 @@ async function cmdReceipt(id) {
433
438
  }
434
439
  const name = `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
435
440
  writeFileSync(name, JSON.stringify(r.body.credential, null, 2) + "\n");
436
- log(`✔ wrote ${name}`);
441
+ log(`✔ wrote ${name} (downloaded from your workspace — the download is online; VERIFYING it is offline)`);
437
442
  // The credential seals at DECISION time (immutable), so execution evidence that arrived later
438
443
  // lives on the record — surface the CURRENT tier here so the reader doesn't need a second
439
444
  // lookup to see it (Codex catch).
@@ -441,12 +446,219 @@ async function cmdReceipt(id) {
441
446
  const conf = detail.body?.record?.confirmation;
442
447
  if (conf?.status)
443
448
  log(` Sealed credential: the authority decision (its confirmation field reads "pending" forever — immutable). Current record evidence tier: ${conf.status}${conf.confirmed ? " (confirmed)" : ""}.`);
444
- log(` Verify it offline — no Decidio account or network needed:`);
445
- log(` npx @decidio/verify ${name}`);
449
+ // Emit EXACTLY the deterministic command we document (round-4 catch: the README and this hint
450
+ // disagreed, and bare npx re-probes the registry) — the verifier ships INSIDE this package.
451
+ log(` Verify it offline — no Decidio account or network needed (the verifier is built in):`);
452
+ log(` npx @decidio/sdk verify ${name}`);
446
453
  log(` To also prove WHO issued it, pin your workspace's issuer DID (shown in your app's record verify panel):`);
447
- log(` npx @decidio/verify --issuer <did:key:...> ${name}`);
454
+ log(` npx @decidio/sdk verify --issuer <did:key:...> ${name}`);
448
455
  return 0;
449
456
  }
457
+ // ---- verify (embedded) ----------------------------------------------------------------------
458
+ /** Duplicate-key-refusing parse — the SAME semantics as the decidio-verify CLI (RFC 8785 §3.1:
459
+ * duplicate keys make different parsers disagree on which value is real; refuse instead).
460
+ * Conformance-locked: the cross-language runner asserts this door refuses the same vectors for
461
+ * the same reasons as decidio-verify and python -m decidio.verify. */
462
+ function parseReceiptStrict(text, file) {
463
+ const value = JSON.parse(text);
464
+ const stack = [];
465
+ let i = 0;
466
+ const readString = () => {
467
+ let out = "";
468
+ i++;
469
+ while (i < text.length) {
470
+ const ch = text[i];
471
+ if (ch === "\\") {
472
+ out += text[i] + text[i + 1];
473
+ i += 2;
474
+ continue;
475
+ }
476
+ if (ch === '"') {
477
+ i++;
478
+ return out;
479
+ }
480
+ out += ch;
481
+ i++;
482
+ }
483
+ return out;
484
+ };
485
+ while (i < text.length) {
486
+ const ch = text[i];
487
+ if (ch === '"') {
488
+ const s = readString();
489
+ let j = i;
490
+ while (j < text.length && /\s/.test(text[j]))
491
+ j++;
492
+ if (text[j] === ":" && stack.length > 0) {
493
+ const key = JSON.parse(`"${s}"`);
494
+ const top = stack[stack.length - 1];
495
+ if (top.has(key))
496
+ throw new Error(`${file}: duplicate JSON key ${JSON.stringify(key)} — RFC 8785 requires duplicate-free input, and different parsers disagree on which value is real`);
497
+ top.add(key);
498
+ }
499
+ continue;
500
+ }
501
+ if (ch === "{")
502
+ stack.push(new Set());
503
+ else if (ch === "}")
504
+ stack.pop();
505
+ i++;
506
+ }
507
+ return value;
508
+ }
509
+ function cmdVerify(argsIn) {
510
+ const issIdx = argsIn.indexOf("--issuer");
511
+ const expectedIssuer = issIdx >= 0 ? argsIn[issIdx + 1] : undefined;
512
+ const files = argsIn.filter((a, i) => !a.startsWith("--") && !(issIdx >= 0 && i === issIdx + 1));
513
+ if (!files.length) {
514
+ log("usage: npx @decidio/sdk verify [--issuer <did:key>] <receipt.json> [more.json ...]");
515
+ log(" Verifies Authority Receipts OFFLINE — signature, content binding, and (for several files) chain order.");
516
+ return 2;
517
+ }
518
+ let vcs;
519
+ try {
520
+ vcs = files.map((f) => parseReceiptStrict(readFileSync(f, "utf8"), f));
521
+ }
522
+ catch (e) {
523
+ log(`✘ INVALID ${String(e?.message ?? e)}`);
524
+ return 1;
525
+ }
526
+ let allOk = true;
527
+ vcs.forEach((vc, i) => {
528
+ const res = verifyReceipt(vc, expectedIssuer);
529
+ allOk = allOk && res.valid;
530
+ log(`${res.valid ? "✔ VALID " : "✘ INVALID"} ${files[i]}`);
531
+ log(` record: ${res.recordId ?? "?"} issuer: ${res.issuer}`);
532
+ log(` content-bound: ${res.contentBound}${res.reason ? ` reason: ${res.reason}` : ""}`);
533
+ });
534
+ if (vcs.length > 1) {
535
+ const c = verifyChain(vcs, expectedIssuer);
536
+ allOk = allOk && c.valid;
537
+ log(`\nchain: ${c.valid ? "✔ intact" : "✘ broken"} (${c.length}/${vcs.length})${c.reason ? ` — ${c.reason}` : ""}`);
538
+ }
539
+ return allOk ? 0 : 1;
540
+ }
541
+ // ---- quickstart (the guided two-minute tour) ------------------------------------------------
542
+ async function cmdQuickstart() {
543
+ const t0 = Date.now();
544
+ log("Decidio quickstart — gate an action, route it to a human, execute on approval, hold the proof.");
545
+ log(` Target: ${API}${process.env.DECIDIO_API_URL ? "" : " (the hosted synthetic sandbox — set DECIDIO_API_URL to use your own)"}`);
546
+ log("");
547
+ const session = await acquireSession("The quickstart");
548
+ if (!session)
549
+ return 1;
550
+ // 1. A collision-proof agent, registered fresh every run (round-4 ask: idempotent sample
551
+ // registration). Key generated locally; floor-limited token minted; .env written so the
552
+ // take-away snippet works in this directory afterward.
553
+ const agentId = `quickstart-${randomBytes(2).toString("hex")}`;
554
+ log(`① Registering agent "${agentId}" (fresh name each run — no collisions)…`);
555
+ const { did, privateKeyBase64 } = generateKeypair();
556
+ const reg = await api("/api/agents/register", { method: "POST", body: JSON.stringify({ agentId, did }) }, session);
557
+ if (!(reg.status >= 200 && reg.status < 300)) {
558
+ log(`✘ registration failed (${reg.status}): ${JSON.stringify(reg.body)}`);
559
+ return 1;
560
+ }
561
+ const mint = await api("/api/workspace/agent-token", { method: "POST", body: JSON.stringify({ agentId, label: "quickstart" }) }, session);
562
+ if (!(mint.status >= 200 && mint.status < 300) || !mint.body?.token) {
563
+ log(`✘ token mint failed (${mint.status}): ${JSON.stringify(mint.body)}`);
564
+ return 1;
565
+ }
566
+ const envOut = {
567
+ DECIDIO_API_URL: API, DECIDIO_AGENT_ID: agentId, DECIDIO_AGENT_DID: reg.body?.did ?? did,
568
+ DECIDIO_AGENT_KEY: privateKeyBase64, DECIDIO_API_TOKEN: mint.body.token,
569
+ DECIDIO_WORKSPACE_ID: mint.body.workspaceId ?? "", DECIDIO_WEBHOOK_SECRET: readEnvFile().DECIDIO_WEBHOOK_SECRET || randomBytes(32).toString("hex"),
570
+ };
571
+ // NEVER clobber a real agent's key (guard catch: keys never leave this machine — overwriting
572
+ // .env's DECIDIO_AGENT_KEY orphans that agent irrecoverably). A directory that already holds
573
+ // one gets a TOUR-SCOPED env file instead.
574
+ let envFile = ENV_PATH;
575
+ if (readEnvFile().DECIDIO_AGENT_KEY) {
576
+ envFile = ".env.quickstart";
577
+ writeFileSync(envFile, Object.entries(envOut).map(([k, v]) => `${k}=${v}`).join("\n") + "\n");
578
+ log(` ✔ this directory already has an agent in .env — tour credentials went to ${envFile} (your agent is untouched)`);
579
+ }
580
+ else {
581
+ upsertEnv(envOut);
582
+ }
583
+ ensureGitignore();
584
+ log(` ✔ key generated locally (public half registered) · floor-limited token minted · ${envFile} written`);
585
+ log("");
586
+ // 2. Gate a demo action — the same one line you'd wrap your real function with.
587
+ log(`② Gating a demo action with ONE line — guard.protect(payInvoice, …):`);
588
+ log("");
589
+ log(` const payInvoice = guard.protect(payInvoiceRaw,`);
590
+ log(` (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }));`);
591
+ log("");
592
+ let decisionId = "";
593
+ const cfg = {
594
+ apiUrl: API, agentId, apiToken: mint.body.token, agentKey: privateKeyBase64,
595
+ agentDid: reg.body?.did ?? did, workspaceId: mint.body.workspaceId,
596
+ pollIntervalMs: 1500, pollTimeoutMs: 300_000,
597
+ onPending: (p) => { decisionId = p.decisionId; },
598
+ };
599
+ const payInvoice = withApproval(async (inv) => ({ paid: true, id: inv.id }), (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }), cfg);
600
+ const running = payInvoice({ id: "INV-2026-001", amount: 86_000 });
601
+ running.catch(() => { });
602
+ const waitStart = Date.now();
603
+ while (!decisionId && Date.now() - waitStart < 30_000)
604
+ await new Promise((r) => setTimeout(r, 250));
605
+ if (!decisionId) {
606
+ log("✘ the gate did not route within 30s — check connectivity (npx @decidio/sdk doctor) and re-run.");
607
+ return 1;
608
+ }
609
+ log(` ✔ $86,000 payInvoice ROUTED to a human (decision ${decisionId}) — this agent has no auto-approve rule,`);
610
+ log(` so deny-by-default sends everything to a person. Nothing executes until someone decides.`);
611
+ log("");
612
+ // 3. The human moment — approve right here, or from the Decidio app.
613
+ if (process.stdin.isTTY) {
614
+ await ask(`③ YOU are the approver. Press Enter to approve it here (or approve it in your Decidio queue first, then Enter): `);
615
+ }
616
+ else {
617
+ log("③ Headless terminal — auto-approving with your session so the tour can continue.");
618
+ }
619
+ const ap = await api(`/api/decisions/${encodeURIComponent(decisionId)}/approve`, { method: "POST", body: JSON.stringify({ reason: "quickstart" }) }, session);
620
+ if (!(ap.status >= 200 && ap.status < 300)) {
621
+ log(`✘ approve failed (${ap.status}): ${JSON.stringify(ap.body)}`);
622
+ return 1;
623
+ }
624
+ log(` ✔ approved${ap.body?.alreadyResolved ? " (it was already approved from your app — even better)" : ""}`);
625
+ // 4. The blocking wrapper wakes and runs the agent's own function.
626
+ let result;
627
+ try {
628
+ result = await running;
629
+ }
630
+ catch (e) {
631
+ log(`✘ the gated call did not complete: ${e?.message ?? e}`);
632
+ return 1;
633
+ }
634
+ log(` ✔ your function executed AFTER approval: ${JSON.stringify(result)}`);
635
+ log("");
636
+ // 5. The proof: download the receipt and verify it offline, all in-process.
637
+ log("④ Downloading the sealed Authority Receipt and verifying it OFFLINE…");
638
+ const rec = await api(`/api/records/${encodeURIComponent(decisionId)}/verify`, {}, session);
639
+ if (!(rec.status >= 200 && rec.status < 300) || !rec.body?.credential) {
640
+ log(`✘ receipt fetch failed (${rec.status})`);
641
+ return 1;
642
+ }
643
+ const fname = `decidio-receipt-${decisionId.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
644
+ writeFileSync(fname, JSON.stringify(rec.body.credential, null, 2) + "\n");
645
+ const v = verifyReceipt(rec.body.credential, undefined);
646
+ log(` ${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding verified with NO network, no Decidio account)`);
647
+ const detail = await api(`/api/records/${encodeURIComponent(decisionId)}`, {}, session);
648
+ const tier = detail.body?.record?.confirmation?.status;
649
+ if (tier)
650
+ log(` ✔ execution evidence on the record: ${tier} — the wrapper's report was cryptographically bound to this agent`);
651
+ log("");
652
+ const secs = Math.round((Date.now() - t0) / 1000);
653
+ log(`Done in ${secs}s: gated → routed to a human → approved → executed once → sealed, verifiable proof in your hands.`);
654
+ log("");
655
+ log("Take it to your real agent — the .env here already works:");
656
+ log(` import { guard } from "@decidio/sdk";`);
657
+ log(` const doIt = guard.protect(doItRaw, (a) => ({ action: "yourAction", amount: a.amount, scope: "YourScope" }));`);
658
+ log("");
659
+ log("Next: the README's 7-step quickstart (durable mode, workers, adapters) · your queue at your Decidio app.");
660
+ return v.valid ? 0 : 1;
661
+ }
450
662
  function cmdDev(target, port) {
451
663
  // Signal-only relay: receive Decidio's webhook here (a stable URL) and forward it to
452
664
  // your local agent's resume listener. Useful when the agent isn't directly reachable.
@@ -476,15 +688,27 @@ function cmdDev(target, port) {
476
688
  }
477
689
  const USAGE = `decidio — the Decidio SDK command line
478
690
 
691
+ New here? See the whole thing work in about two minutes:
692
+
693
+ npx @decidio/sdk quickstart
694
+
695
+ It gates a demo action, routes it to YOU for approval, executes on your yes, and hands you a
696
+ cryptographically verifiable receipt. (Guided and explicit — this CLI never acts, signs you
697
+ in, or writes files unless you ask it to.)
698
+
699
+ npx @decidio/sdk quickstart the guided two-minute tour: gate → approve → execute → proof
479
700
  npx @decidio/sdk init [agentId] sign in, register the agent, mint its API token, write .env
701
+ (--unique appends a random suffix — collision-proof samples)
480
702
  npx @decidio/sdk doctor check config, connectivity, and what your token can do
481
- npx @decidio/sdk approvals list pending agent approvals
703
+ npx @decidio/sdk approvals [--json] list pending agent approvals
482
704
  npx @decidio/sdk approvals approve <id> [reason]
483
705
  npx @decidio/sdk approvals reject <id> [reason]
484
706
  npx @decidio/sdk receipt <id> download a sealed decision's Authority Receipt (.json)
707
+ npx @decidio/sdk verify [--issuer <did>] <receipt.json> [more...] verify receipts OFFLINE
485
708
  npx @decidio/sdk dev [--target URL] [--port N] local relay for the resume webhook
486
709
  npx @decidio/sdk --version print the CLI version
487
710
 
711
+ Exit codes: 0 success · 1 failure or honest no-op (e.g. replay refused) · 2 usage error.
488
712
  Docs: the README that shipped with this package (quickstart, durable resume, adapters).`;
489
713
  async function main(argv) {
490
714
  const [cmd, a, b, ...rest] = argv.slice(2);
@@ -502,10 +726,20 @@ async function main(argv) {
502
726
  return 0;
503
727
  }
504
728
  switch (cmd) {
505
- case "init": return cmdInit((a && !a.startsWith("-") ? a : undefined) ?? process.env.DECIDIO_AGENT_ID ?? "my-agent");
729
+ case "quickstart": return cmdQuickstart();
730
+ case "init": {
731
+ const argsIn = argv.slice(3);
732
+ const unique = argsIn.includes("--unique");
733
+ const nameArg = argsIn.find((x) => !x.startsWith("-"));
734
+ let name = nameArg ?? process.env.DECIDIO_AGENT_ID ?? "my-agent";
735
+ if (unique)
736
+ name = `${name}-${randomBytes(2).toString("hex")}`;
737
+ return cmdInit(name);
738
+ }
506
739
  case "doctor": return cmdDoctor();
507
740
  case "approvals": return cmdApprovals(a, b, rest.join(" ") || undefined);
508
741
  case "receipt": return cmdReceipt(a);
742
+ case "verify": return cmdVerify(argv.slice(3));
509
743
  case "dev": {
510
744
  const args = argv.slice(3);
511
745
  const flag = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
package/dist/index.d.ts CHANGED
@@ -75,6 +75,16 @@ export declare class DecidioTimeoutError extends Error {
75
75
  decisionId: string;
76
76
  constructor(decisionId: string);
77
77
  }
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
+ export declare class DecidioNetworkError extends Error {
83
+ endpoint: string;
84
+ op: string;
85
+ code: string | undefined;
86
+ constructor(endpoint: string, op: string, code: string | undefined, cause: unknown);
87
+ }
78
88
  /**
79
89
  * Wrap an async action behind the Decidio gate. `describe` extracts the approval
80
90
  * context (action, amount, scope) from the call arguments. Returns a function with
package/dist/index.js CHANGED
@@ -61,9 +61,37 @@ export class DecidioTimeoutError extends Error {
61
61
  }
62
62
  }
63
63
  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
+ export class DecidioNetworkError extends Error {
69
+ endpoint;
70
+ op;
71
+ code;
72
+ constructor(endpoint, op, code, cause) {
73
+ super(`[decidio] could not reach ${endpoint} during ${op}${code ? ` (${code})` : ""}. ` +
74
+ `Nothing was executed locally and no verdict was received — if a request reached Decidio it is ` +
75
+ `visible in your queue. Check the network/URL (DECIDIO_API_URL) and retry; retrying is safe ` +
76
+ `(a retry creates a new request — it can never double-execute).`);
77
+ this.endpoint = endpoint;
78
+ this.op = op;
79
+ this.code = code;
80
+ this.name = "DecidioNetworkError";
81
+ this.cause = cause;
82
+ }
83
+ }
84
+ async function reach(f, url, op, init) {
85
+ try {
86
+ return await f(url, init);
87
+ }
88
+ catch (e) {
89
+ throw new DecidioNetworkError(url, op, e?.cause?.code ?? e?.code, e);
90
+ }
91
+ }
64
92
  async function post(cfg, path, body) {
65
93
  const f = cfg.fetchImpl ?? fetch;
66
- const res = await f(cfg.apiUrl.replace(/\/$/, "") + path, {
94
+ const res = await reach(f, cfg.apiUrl.replace(/\/$/, "") + path, `POST ${path}`, {
67
95
  method: "POST",
68
96
  headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
69
97
  body: JSON.stringify(body),
@@ -74,7 +102,7 @@ async function post(cfg, path, body) {
74
102
  }
75
103
  async function get(cfg, path) {
76
104
  const f = cfg.fetchImpl ?? fetch;
77
- const res = await f(cfg.apiUrl.replace(/\/$/, "") + path, {
105
+ const res = await reach(f, cfg.apiUrl.replace(/\/$/, "") + path, `GET ${path}`, {
78
106
  headers: { ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
79
107
  });
80
108
  if (!res.ok)
package/openapi.yaml CHANGED
@@ -5,7 +5,7 @@
5
5
  openapi: 3.1.0
6
6
  info:
7
7
  title: Decidio Agent Gate
8
- version: 0.1.3
8
+ version: 0.1.4
9
9
  description: >
10
10
  Authorize an AI-agent action (proceed | route | block), confirm the captured outcome, and
11
11
  receive the signed resume webhook when a routed action is decided. Every outcome seals a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decidio/sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "One-line approval gate for AI-agent actions. Wrap a risky tool call with guard.protect(); Decidio's policy engine decides proceed | route | block, a human approves routed actions, and every outcome is sealed as a verifiable Authority Receipt.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -79,5 +79,8 @@
79
79
  "typecheck": "tsc --noEmit",
80
80
  "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
81
81
  "prepublishOnly": "npm run typecheck && npm run build"
82
+ },
83
+ "dependencies": {
84
+ "@decidio/verify": "^0.1.3"
82
85
  }
83
86
  }