@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 +38 -10
- package/dist/cli.js +241 -7
- package/dist/index.d.ts +10 -0
- package/dist/index.js +30 -2
- package/openapi.yaml +1 -1
- package/package.json +4 -1
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).
|
|
10
|
-
|
|
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
|
|
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
|
|
112
|
+
**6. Download the receipt (online), then verify it (offline):**
|
|
87
113
|
|
|
88
114
|
```bash
|
|
89
115
|
npx @decidio/sdk receipt <decisionId>
|
|
90
|
-
npx
|
|
116
|
+
npx @decidio/sdk verify decidio-receipt-<decisionId>.json
|
|
91
117
|
```
|
|
92
118
|
|
|
93
|
-
The
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
|
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 "
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
"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
|
}
|