@decidio/sdk 0.5.0 → 0.6.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,75 @@ Release notes for the client SDK. The Python package `decidio` ships the same ch
4
4
  version (the two are conformance-locked twins). Each entry names what changed, what a reader must do
5
5
  on upgrade, and what proved it.
6
6
 
7
+ ## 0.6.1 — the first-run fork, and the local demo's loose ends
8
+
9
+ Two independent first-run reviews of 0.6.0 (grades B / A− overall) agreed on one thing: the hosted tour and the
10
+ verifier earn trust, and the new local demo had polish debts. This release pays them and adds the one question a
11
+ stranger's first minute was missing (founder ruling 2026-09-06).
12
+
13
+ - **One question, then the thing you picked.** A bare `npx @decidio/sdk quickstart` (or a bare `npx @decidio/sdk`) on
14
+ a terminal with no sign-in asks: do you have a Decidio invite? `n` runs the local demo; `y` signs you in (the emailed
15
+ link) and asks what next: the two-minute tour, or wiring up your own agent (`init`). Every answer prints the exact
16
+ command it stands for, so the questions teach their own flags and you never need them twice. Nothing is stored;
17
+ Enter is not an answer; scripts and CI never see a question (they get today's text, which now also names `--demo`).
18
+ A signed-in `quickstart` runs straight away; the bare `npx @decidio/sdk` for a signed-in user skips the invite
19
+ question and asks only "what next?" - the bare command always leads somewhere, usage is a `--help` away. After
20
+ the sign-in step, the tour does not ask you to sign in again.
21
+ - **Your agent's name has a real default.** `init` with no name used `my-agent`; it now uses this folder's name (or
22
+ the package name), slugified, and the fork explains what the name is for ("Requested by …", the key `.env` is
23
+ written under). Any name still works.
24
+ - **A mark, not a banner:** the title lines carry the app's ✦ beside the name.
25
+ - **Headless `--demo` leaves nothing behind** (review: Medium). The stop at the human moment used to exit inside the
26
+ tour, skipping the wrapper that removes the temporary folder - the mock key, mock token and journal stayed in the
27
+ temp dir, and the message pointed at the hosted tour's remedies (`approvals approve`, `quickstart cleanup`), which
28
+ cannot act on a stand-in that lives only in that process. The stop now returns through the wrapper, and says the
29
+ demo's truth: nothing executed, nothing kept, re-run interactively or with `--auto-approve-demo`.
30
+ - **The demo receipt carries what the terminal claims** (review: Medium). Its canonical record now holds the real
31
+ amount and currency, the reason the decision arrived with (the automated flag names itself there, exactly as it does
32
+ in a hosted record's reason), and how it was decided (terminal, page, or the flag). The party stays tokenized, as
33
+ the real seal keeps it.
34
+ - **`--demo --keep-agent` is refused** (both reviews: Low) - nothing of the demo's can be kept, and the flag used to
35
+ be accepted while the opposite happened. Exit 2, with the reason.
36
+ - **A receipt the demo could not copy home is not a completed tour** (retest: Low): the temporary folder is kept and
37
+ named, and the run exits non-zero, instead of deleting the only copy and exiting 0.
38
+ - **Words that were not true** (review: Low): the demo's Target line no longer says "nothing hosted" while the web
39
+ door uses a hosted relay; the automated line names the demo's mock credential, not "your session credential"; a
40
+ repeat run's ④ frame names the suffixed file that actually lands; the README no longer calls the sandbox "one shared
41
+ lab workspace" in two places while saying the opposite in a third.
42
+
43
+ ## 0.6.0 — `quickstart --demo`: the whole tour on your machine, no account
44
+
45
+ An uninvited developer could not run the tour at all: a workspace needs an invite (no open signup, by ruling). A
46
+ hosted sandbox was designed, red-teamed by the privacy council (BLOCK as written; eight design-level highs) and then
47
+ costed against the alternatives: the cheapest, least-maintenance path for a stranger's first two minutes is a LOCAL
48
+ demo with a hosted page for the phone moment. Founder ruling 2026-09-05.
49
+
50
+ - **`npx @decidio/sdk quickstart --demo`** runs the SAME tour code against an in-process stand-in of the Decidio API
51
+ on 127.0.0.1 (`demo-gate.ts`, shipped in this package): register, mint, gate, route, approve or reject, execute
52
+ once, and a REAL Ed25519 receipt signed by a key generated for the run — verifiable offline with the shipped
53
+ verifier, and provably NOT the hosted issuer's (pinning it to the hosted DID fails). Nothing hosted, no account, no
54
+ workspace: the tour runs in a temporary folder and leaves exactly one file behind, the receipt (never over an
55
+ existing one). Its content says `recordSeries: local-demo`.
56
+ - **The web and phone doors still work.** The stand-in asks a hosted RELAY (`POST /demo/relay`) for a code, a link to
57
+ the demo decision page (`app.decidioai.com/demo/<code>`) and a QR; the page shows the product's decision surface for
58
+ the tour's fixed scenario and writes one word — approve or reject — which the stand-in reads while the tour waits.
59
+ The relay holds no account, no record and no personal data; a code lives ten minutes, takes one verdict, and every
60
+ failure answers with one shape. Offline (relay unreachable) the tour is terminal-only and says so.
61
+ - **Honesty is asserted, not hoped:** the frames say LOCAL DEMO; ④ says the issuer is a key this demo generated and
62
+ where real tours pin theirs; the smoke plants the forbidden phrases ("PINNED to your workspace issuer", "hosted
63
+ synthetic sandbox", "your trust anchor") and asserts the directory diff is the receipt alone; a decision made on the
64
+ page reaches the terminal with no keystroke. The sign-in dead end now offers `--demo`.
65
+ - **On a phone the demo IS the phone app's screen.** A phone that opens the demo link (the QR, or the link on a phone)
66
+ lands on the phone build's own decision screen — the same Detail the installed app renders, with Approve and Reject
67
+ where they always are — not the desktop panel squeezed into a phone viewport (founder UAT round 2). It holds no
68
+ session and says LOCAL DEMO in one line; the decided moment says the terminal seals. Host-side only: nothing in this
69
+ package changed for it.
70
+ - **The demo leaves your sign-in alone.** `quickstart --demo` never signs in, so its cleanup no longer ends a stored
71
+ sign-in that happens to be on the computer (it did, in the founder's UAT; the smoke now plants one and asserts it
72
+ survives, silently).
73
+ - TypeScript CLI first — `npx @decidio/sdk quickstart` is where an uninvited developer arrives; the Python twin gains
74
+ `--demo` when asked for (the version bump is in lockstep).
75
+
7
76
  ## 0.5.0 — `verify` pins by default
8
77
 
9
78
  **Breaking, on purpose (founder ruling 2026-09-04, after the prodsec review of 0.4.3).** Every CLI verify door - `npx
package/README.md CHANGED
@@ -9,7 +9,11 @@ is the two-minute tour, the sign-in and the token admin around it. Install, wrap
9
9
  ## The two-minute tour
10
10
 
11
11
  ```bash
12
- npx @decidio/sdk quickstart
12
+ npx @decidio/sdk quickstart # against your workspace (you need an invite - request one below)
13
+ npx @decidio/sdk quickstart --demo # no invite yet? the same tour on this machine, against a local stand-in of the API
14
+ # Not sure? The bare command asks: with no sign-in on this computer it asks whether you have an
15
+ # invite (n → the local demo; y → sign in, then the tour or your own agent). Every answer prints
16
+ # the command it stands for, so you never need the questions twice.
13
17
  ```
14
18
 
15
19
  One command, guided: it registers a fresh agent (key generated locally), gates a demo action
@@ -136,8 +140,8 @@ the agent's floor-limited token — it cannot approve anything):
136
140
 
137
141
  ```bash
138
142
  npx @decidio/sdk approvals --mine # see it pending (--mine narrows to THIS agent; the
139
- # sandbox is one shared lab workspace, so the bare
140
- # command lists every member's pending decisions)
143
+ # workspace is yours alone unless you invite others,
144
+ # so the bare command lists every pending decision in it)
141
145
  npx @decidio/sdk approvals approve <decisionId>
142
146
  ```
143
147
 
@@ -653,6 +657,7 @@ that fits every other confirm error.
653
657
  - `quickstart cleanup` — revoke the tour's token and delete its local credentials. What stays, honestly: sealed records (permanent by design) and the agent's registration row (inert without key or token; server-side deregistration is on the roadmap) Also ends the stored sign-in.
654
658
  - **Update check.** Network commands ask the public npm registry once a day whether a newer version exists; if so, an interactive run asks `Upgrade now? [y/n]` first (`y` upgrades the way you installed - `npm i @decidio/sdk@latest` in the project for a local install, `-g` for a global one, and under `npx` it prints the `@latest` form - then re-runs your command once; `n` exits), a headless run prints the notice and continues (`DECIDIO_REQUIRE_LATEST=1` makes it exit). `DECIDIO_NO_UPDATE_CHECK=1` skips it. Nothing is sent to Decidio; `verify` never checks.
655
659
  - **The token recovery journal** (`.decidio-token-journal`, owner-only, gitignored; ids and timestamps, never a token value; append-only by the CLI's behaviour — not OS-enforced or tamper-evident). One JSON row per event: `minted` (a token was issued, written BEFORE its credential file), `retired how=persisted` (the credential file now holds it - the token is live and that file is its handle; not revoked), `retired how=revoked` (revoked server-side), `probe` (a durability check). `doctor` names rows still outstanding; `quickstart cleanup` sweeps them.
660
+ - `quickstart --demo` — the same tour with no account: an in-process stand-in of the API on 127.0.0.1 (shipped in this package) answers every door, a key generated for the run signs a real receipt (its content says `local-demo`; pinning it to the hosted issuer fails), and the web/phone door is a hosted demo decision page reached through a one-word relay (no account, no record, ten minutes). Your folder gains exactly one file, the receipt. Offline, the tour is terminal-only and says so.
656
661
  - `quickstart --phone web` — the QR at the human step opens the phone web rendering even when the Decidio app is installed (by default it opens the app when there is one).
657
662
  - `login [email]` / `logout` — sign in ONCE (the CLI emails you a link; you paste it) and forget that sign-in on this computer (the session token itself stays valid until it expires (up to 30 days) — there is no server-side revocation yet). The session is stored in your user profile, owner-only, bound to the terminal window that created it, for 30 days; `DECIDIO_SESSION_TOKEN` in the environment outranks it (headless shells).
658
663
  - `dev [--target URL] [--port N]` — local relay for the resume webhook (operator setups)
@@ -745,7 +750,7 @@ the next reader grades a promise, not a moving target:
745
750
 
746
751
  ## Using this for real?
747
752
 
748
- The hosted sandbox is synthetic and shared. Production workspaces are per-tenant, with your own
753
+ The hosted sandbox is synthetic data in a workspace of your own. Production workspaces are per-tenant too, with your own
749
754
  issuer key and receipts that verify offline against it.
750
755
 
751
756
  [decidioai.com](https://decidioai.com) · [Developer docs](https://decidioai.com/developers/) · [Request access](https://decidioai.com/#early-access)
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  export type ApprovalAnswer = "approve" | "reject" | "again";
3
3
  export declare function approvalAnswer(answer: unknown): ApprovalAnswer;
4
+ /** The agent id `init` uses when none is given: this folder's name (or the package name), slugified. "my-agent" told a
5
+ * tester nothing; the folder they are in is the agent they are building (founder ruling 2026-09-06). */
6
+ export declare function defaultAgentId(dir?: string): string;
4
7
  /** `decidio-receipt-<id>.json`, with every character outside [A-Za-z0-9_-] escaped as `~` + hex code point, so two
5
8
  * distinct decision ids can never share a file (first-run review of 0.4.1: dots were STRIPPED, `a.b` and `ab`
6
9
  * collided). `~` is outside the allowed set, so the escape is unambiguous. Exported for the smokes. */
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, existsSync, appendFileSync, chmodSync, rmSync, openSync, writeSync, fsyncSync, closeSync, renameSync, mkdirSync } from "node:fs";
2
+ import { readFileSync, writeFileSync, existsSync, appendFileSync, chmodSync, rmSync, openSync, writeSync, fsyncSync, closeSync, renameSync, mkdirSync, mkdtempSync, readdirSync, copyFileSync } from "node:fs";
3
3
  import { randomBytes } from "node:crypto";
4
- import { homedir } from "node:os";
5
- import { join as joinPath, resolve as resolvePath, sep as pathSep } from "node:path";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { join as joinPath, resolve as resolvePath, sep as pathSep, basename } from "node:path";
6
6
  import { createServer } from "node:http";
7
7
  import { execFileSync, spawnSync } from "node:child_process";
8
8
  import { createInterface } from "node:readline";
@@ -13,6 +13,7 @@ import { generateKeypair, assertSafeTransport, redactUrl } from "./signing.js";
13
13
  import { hardenFile } from "./resume.js";
14
14
  import { withApproval, DecidioRateLimitError, DecidioRejectedError, DecidioTimeoutError, didFromPrivateKey } from "./index.js";
15
15
  import { verifyReceipt, runVerify, pinRequiredMessage } from "@decidio/verify";
16
+ import { startDemoGate } from "./demo-gate.js";
16
17
  const VERSION = (() => {
17
18
  try {
18
19
  return createRequire(import.meta.url)("../package.json").version;
@@ -65,7 +66,13 @@ if (!OFFLINE_CMD)
65
66
  if (process.env[k] === undefined)
66
67
  process.env[k] = v;
67
68
  }
68
- const API = (process.env.DECIDIO_API_URL ?? "https://decidio-api.onrender.com").replace(/\/$/, "");
69
+ let API = (process.env.DECIDIO_API_URL ?? "https://decidio-api.onrender.com").replace(/\/$/, "");
70
+ let DEMO = false;
71
+ function enterDemo(url) { API = url; process.env.DECIDIO_API_URL = url; DEMO = true; }
72
+ const pinWords = () => (DEMO ? "PINNED to this demo's own key" : "PINNED to your workspace issuer");
73
+ const issuerNote = () => (DEMO
74
+ ? "· a key this demo generated for this run — real tours pin the host's published DID at https://decidio-api.onrender.com/api/did"
75
+ : `· your trust anchor — first seen over this signed-in connection; cross-check ${API}/api/did, then verify --issuer <did> <receipt.json>`);
69
76
  const TOKEN = () => process.env.DECIDIO_API_TOKEN;
70
77
  const ENV_PATH = ".env";
71
78
  const log = (...a) => console.log(...a);
@@ -399,6 +406,8 @@ function recordKindOf(credential) {
399
406
  }
400
407
  }
401
408
  function sessionReminder() {
409
+ if (DEMO)
410
+ return;
402
411
  log(` ⚠ agent code launched from THIS terminal inherits your sign-in — run logout first (it forgets the sign-in here; the token itself stays valid until it expires); if you`);
403
412
  log(` exported DECIDIO_SESSION_TOKEN, unset it ${dim("(bash/zsh: unset DECIDIO_SESSION_TOKEN · PowerShell: Remove-Item Env:DECIDIO_SESSION_TOKEN)")}`);
404
413
  }
@@ -407,6 +416,7 @@ const paint = (code, s) => (TTY_OUT ? `\x1b[${code}m${s}\x1b[0m` : s);
407
416
  const bold = (s) => paint("1", s);
408
417
  const dim = (s) => paint("2", s);
409
418
  const link = (url) => (TTY_OUT ? `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\` : url);
419
+ const brand = () => `${paint("38;5;178", "✦")} ${paint("38;5;29", bold("Decidio"))}`;
410
420
  const FRAME_WIDTH = 84;
411
421
  const visibleLength = (s) => s.replace(/\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\/g, "").length;
412
422
  function frameWidth(rows) {
@@ -564,6 +574,7 @@ async function loginFlow(purpose, emailArg) {
564
574
  }
565
575
  log(` ✔ sign-in email requested for ${email} — check your inbox and spam; paste the link (or its login= code; single-use, 15 minutes)`);
566
576
  log(dim(` invite-only lab: a link arrives only for an invited address — not invited yet? request access: decidioai.com/#early-access`));
577
+ log(dim(` or see the whole story on this machine right now, no account: npx @decidio/sdk quickstart --demo`));
567
578
  const pasted = await ask(" link or code: ");
568
579
  if (!pasted) {
569
580
  log(" ✘ nothing pasted — re-run when you have the email.");
@@ -598,12 +609,79 @@ async function acquireSession(purpose) {
598
609
  if (!process.stdin.isTTY) {
599
610
  err(`✘ ${purpose} needs a Decidio workspace sign-in, and this terminal can't prompt for one.`);
600
611
  err(` Fix: run \`npx @decidio/sdk login\` in an interactive terminal first (the sign-in is stored for this terminal),`);
601
- err(` or set DECIDIO_SESSION_TOKEN. To mint one by hand: request a sign-in link, then`);
612
+ err(` or set DECIDIO_SESSION_TOKEN - or, with no account at all: npx @decidio/sdk quickstart --demo. To mint one by hand: request a sign-in link, then`);
602
613
  err(` curl -X POST ${API}/demo/login-consume -H "content-type: application/json" -d '{"token":"<login value>"}' # the response's "token"`);
603
614
  return null;
604
615
  }
605
616
  return loginFlow(purpose);
606
617
  }
618
+ export function defaultAgentId(dir = process.cwd()) {
619
+ let name = "";
620
+ try {
621
+ const pkg = JSON.parse(readFileSync(joinPath(dir, "package.json"), "utf8"));
622
+ if (typeof pkg?.name === "string")
623
+ name = pkg.name.replace(/^@[^/]+\//, "");
624
+ }
625
+ catch { }
626
+ if (!name)
627
+ name = basename(dir);
628
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
629
+ return slug || "my-agent";
630
+ }
631
+ async function firstRunFork(signedInAs = null) {
632
+ const pick = async (prompt, ok) => {
633
+ let a = "";
634
+ for (let i = 0; i < 3 && !ok.test(a); i++)
635
+ a = (await ask(i === 0 ? " › " : ` Please answer ${prompt}: `)).trim().toLowerCase();
636
+ return ok.test(a) ? a : "";
637
+ };
638
+ log("");
639
+ if (signedInAs !== null) {
640
+ log(` Signed in${signedInAs ? ` as ${signedInAs}` : ""} on this computer.`);
641
+ }
642
+ else {
643
+ log(` Do you have a Decidio invite? ${dim("[y/n]")}`);
644
+ log(` ${bold("n")} the local demo on this machine, no account ${dim("npx @decidio/sdk quickstart --demo")}`);
645
+ log(` ${bold("y")} sign in to your workspace ${dim("npx @decidio/sdk login")}`);
646
+ const a = await pick("y or n", /^[yn]$/);
647
+ if (!a) {
648
+ log(` No answer — nothing was done. Run either command above when you are ready.`);
649
+ return { kind: "stop", code: 1 };
650
+ }
651
+ if (a === "n") {
652
+ log(` → running: npx @decidio/sdk quickstart --demo`);
653
+ log("");
654
+ return { kind: "demo" };
655
+ }
656
+ log(` → running: npx @decidio/sdk login`);
657
+ const token = await loginFlow("Signing in");
658
+ if (!token)
659
+ return { kind: "stop", code: 1 };
660
+ }
661
+ log("");
662
+ log(` What next? ${dim("[1/2]")}`);
663
+ log(` ${bold("1")} the two-minute tour against your workspace ${dim("npx @decidio/sdk quickstart")}`);
664
+ log(` ${bold("2")} wire up your own agent now (register, token, .env) ${dim("npx @decidio/sdk init <agentId>")}`);
665
+ const b = await pick("1 or 2", /^[12]$/);
666
+ if (!b) {
667
+ log(` No answer — you are signed in; run either command above when you are ready.`);
668
+ return { kind: "stop", code: 0 };
669
+ }
670
+ if (b === "1") {
671
+ log(` → running: npx @decidio/sdk quickstart ${dim("(signed in - it will not ask again)")}`);
672
+ log("");
673
+ return { kind: "tour" };
674
+ }
675
+ const def = defaultAgentId();
676
+ log(` Your agent needs a name. It is how the workspace shows the request ("Requested by ${def.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}")`);
677
+ log(` and the key .env is written under. Any name works; Enter takes the folder's.`);
678
+ const typed = (await ask(` agent id [${def}]: `)).trim();
679
+ const agentId = typed || def;
680
+ log(` → running: npx @decidio/sdk init ${agentId} ${dim("(next time, skip the questions: run that directly)")}`);
681
+ log("");
682
+ return { kind: "init", agentId };
683
+ }
684
+ const forkApplies = () => !!(process.stdin.isTTY && process.stdout.isTTY) && !process.env.DECIDIO_SESSION_TOKEN && !loadSession();
607
685
  async function cmdLogin(emailArg) {
608
686
  try {
609
687
  assertSafeTransport(API, true);
@@ -984,7 +1062,10 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
984
1062
  return 0;
985
1063
  }
986
1064
  export function receiptFileName(id) {
987
- return `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, (c) => "~" + c.codePointAt(0).toString(16).padStart(2, "0"))}.json`;
1065
+ const base = `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, (c) => "~" + c.codePointAt(0).toString(16).padStart(2, "0"))}.json`;
1066
+ if (DEMO_ORIGIN && DEMO_STAMP && existsSync(joinPath(DEMO_ORIGIN, base)))
1067
+ return base.replace(/\.json$/, `.${DEMO_STAMP}.json`);
1068
+ return base;
988
1069
  }
989
1070
  function writeReceiptFile(name, bytes, say = log) {
990
1071
  if (existsSync(name)) {
@@ -1145,15 +1226,66 @@ function valuedOpt(args, flag) {
1145
1226
  const eq = args.find((x) => x.startsWith(flag + "="));
1146
1227
  return eq ? eq.slice(flag.length + 1) : undefined;
1147
1228
  }
1229
+ let DEMO_ORIGIN = null;
1230
+ let DEMO_STAMP = null;
1148
1231
  async function cmdQuickstart(opts = {}) {
1232
+ if (!opts.demo)
1233
+ return cmdQuickstartInner(opts);
1234
+ if (opts.keepAgent) {
1235
+ err(`✘ --keep-agent has nothing to keep under --demo: the demo's agent and token live in a temporary folder that is removed at the end (only the receipt stays).`);
1236
+ err(` Drop --keep-agent, or run the hosted tour (npx @decidio/sdk quickstart) where --keep-agent keeps a real agent + token.`);
1237
+ return 2;
1238
+ }
1239
+ const relayBase = SHELL_ENV.DECIDIO_DEMO_RELAY_URL === undefined ? "https://decidio-api.onrender.com" : (SHELL_ENV.DECIDIO_DEMO_RELAY_URL || null);
1240
+ const gate = await startDemoGate({ relayBase });
1241
+ enterDemo(gate.url);
1242
+ const origin = process.cwd();
1243
+ const work = mkdtempSync(joinPath(tmpdir(), "decidio-demo-"));
1244
+ DEMO_ORIGIN = origin;
1245
+ DEMO_STAMP = String(Date.now());
1246
+ process.chdir(work);
1247
+ let code = 1;
1248
+ let copyFailed = false;
1249
+ try {
1250
+ code = await cmdQuickstartInner(opts);
1251
+ }
1252
+ finally {
1253
+ process.chdir(origin);
1254
+ for (const f of readdirSync(work)) {
1255
+ if (!/^decidio-receipt-.*\.json$/.test(f))
1256
+ continue;
1257
+ let target = joinPath(origin, f);
1258
+ if (existsSync(target))
1259
+ target = joinPath(origin, f.replace(/\.json$/, `.${Date.now()}.json`));
1260
+ try {
1261
+ copyFileSync(joinPath(work, f), target);
1262
+ log(` receipt kept here: ${target.slice(origin.length + 1)} ${dim("(the demo's agent, token and files lived in a temporary folder, now removed)")}`);
1263
+ }
1264
+ catch (e) {
1265
+ copyFailed = true;
1266
+ err(` ✘ could not copy the receipt into this folder (${e?.message ?? e}) — the temporary folder is KEPT so the receipt is not lost: ${joinPath(work, f)}`);
1267
+ }
1268
+ }
1269
+ if (!copyFailed)
1270
+ rmSync(work, { recursive: true, force: true });
1271
+ await gate.close();
1272
+ DEMO_ORIGIN = null;
1273
+ DEMO_STAMP = null;
1274
+ }
1275
+ return copyFailed ? (code || 1) : code;
1276
+ }
1277
+ async function cmdQuickstartInner(opts = {}) {
1149
1278
  const t0 = Date.now();
1150
1279
  EXPLAIN = !!opts.explain;
1151
1280
  if (opts.phone !== undefined && opts.phone !== "app" && opts.phone !== "web") {
1152
1281
  err(`✘ --phone takes app (default: the QR opens the installed app when there is one) or web (the QR opens the phone web rendering, always).`);
1153
1282
  return 2;
1154
1283
  }
1155
- log(bold("Decidio quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
1156
- log(` Target: ${API}${process.env.DECIDIO_API_URL ? "" : dim(" (the hosted synthetic sandbox — set DECIDIO_API_URL to use your own)")}`);
1284
+ log(brand() + bold(" quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
1285
+ if (DEMO)
1286
+ log(` Target: LOCAL DEMO — an in-process stand-in for the Decidio API on this machine (${API}). Only the web/phone door touches a hosted relay (one word, no account); nothing is kept but the receipt`);
1287
+ else
1288
+ log(` Target: ${API}${process.env.DECIDIO_API_URL ? "" : dim(" (the hosted synthetic sandbox — set DECIDIO_API_URL to use your own)")}`);
1157
1289
  hostAnnounced = true;
1158
1290
  if (EXPLAIN) {
1159
1291
  log(" This tour will: register one synthetic agent · mint a 7-DAY floor-limited token ·");
@@ -1163,12 +1295,16 @@ async function cmdQuickstart(opts = {}) {
1163
1295
  log(" · create one synthetic decision and its sealed record (records are permanent by design) · wait up to");
1164
1296
  log(" 5 minutes for your y (approve) or n (reject) at the human step. Cleanup offer at the end.");
1165
1297
  }
1298
+ else if (DEMO) {
1299
+ log(dim(" Creates here: the receipt only (decidio-receipt-<id>.json). The demo's agent, token and working files live in a temporary"));
1300
+ log(dim(" folder that is removed at the end; the web/phone door is a hosted demo page — nothing signs in, no record exists anywhere else."));
1301
+ }
1166
1302
  else {
1167
1303
  log(dim(" Creates here: a synthetic agent + 7-day token (.env), .decidio-token-journal (revocation ids, no secrets), .gitignore entries incl."));
1168
1304
  log(dim(" .decidio-pending/ (the durable store), the receipt as decidio-receipt-<id>.json, one sealed record (permanent). --explain for the long form."));
1169
1305
  }
1170
1306
  log("");
1171
- const session = await acquireSession("The quickstart");
1307
+ const session = DEMO ? "demo-session" : await acquireSession("The quickstart");
1172
1308
  if (!session)
1173
1309
  return 1;
1174
1310
  const prior = readEnvFile();
@@ -1180,7 +1316,8 @@ async function cmdQuickstart(opts = {}) {
1180
1316
  err(`✘ cannot reach ${API}${reach.status ? ` (HTTP ${reach.status})` : " (no response)"} — check the network (or DECIDIO_API_URL) and re-run. Nothing was written.`);
1181
1317
  return 1;
1182
1318
  }
1183
- ensureGitignore();
1319
+ if (!DEMO)
1320
+ ensureGitignore();
1184
1321
  let agentId = `quickstart-${randomBytes(5).toString("hex")}`;
1185
1322
  if (EXPLAIN)
1186
1323
  log(` Registering agent "${agentId}"…`);
@@ -1304,7 +1441,7 @@ async function cmdQuickstart(opts = {}) {
1304
1441
  block("①", "Register a synthetic agent", [
1305
1442
  `${bold("agent")} ${agentId}`,
1306
1443
  `${bold("key")} generated locally · only the public half was registered`,
1307
- `${bold("token")} 7-day, floor-limited${mint.body.expiresAt ? ` · expires ${String(mint.body.expiresAt).slice(0, 10)}` : ""} · written to ${envFile}`,
1444
+ `${bold("token")} ${DEMO ? "demo token (one hour, this machine only)" : "7-day, floor-limited"}${mint.body.expiresAt ? ` · expires ${String(mint.body.expiresAt).slice(0, 10)}` : ""} · written to ${envFile}`,
1308
1445
  ]);
1309
1446
  if (EXPLAIN) {
1310
1447
  log(` The tour runs exactly this, the BLOCKING primitive (your real agent uses guard.protect instead:`);
@@ -1364,10 +1501,12 @@ async function cmdQuickstart(opts = {}) {
1364
1501
  const plainLink = `${APP_URL}/queue?focus=${encodeURIComponent(decisionId)}`;
1365
1502
  const rows = [
1366
1503
  `${bold("here")} Type y to approve or n to reject, then Enter ${dim("(waits up to 5 minutes)")}`,
1367
- `${bold("web app")} ${link(doorLink ?? plainLink)} ${dim(doorLink ? "(one-time sign-in · 5 minutes)" : "(sign in there if it asks)")}`,
1504
+ DEMO
1505
+ ? (doorLink ? `${bold("web page")} ${link(doorLink)} ${dim("(the demo decision page · nothing signs in)")}` : `${bold("web page")} ${dim("the web and phone doors need the internet — the demo relay was unreachable, so this run is terminal-only")}`)
1506
+ : `${bold("web app")} ${link(doorLink ?? plainLink)} ${dim(doorLink ? "(one-time sign-in · 5 minutes)" : "(sign in there if it asks)")}`,
1368
1507
  ];
1369
1508
  if (qr) {
1370
- rows.push(`${bold("your phone")} scan — the same one-time sign-in${opts.phone === "web" ? ", in the phone browser (--phone web)" : ": the Decidio app if it is installed, the browser if not"}`);
1509
+ rows.push(DEMO ? `${bold("your phone")} scan — the same demo decision page, in your phone's browser` : `${bold("your phone")} scan — the same one-time sign-in${opts.phone === "web" ? ", in the phone browser (--phone web)" : ": the Decidio app if it is installed, the browser if not"}`);
1371
1510
  for (const l of qr.split("\n"))
1372
1511
  if (l.trim().length)
1373
1512
  rows.push(" " + l);
@@ -1406,26 +1545,52 @@ async function cmdQuickstart(opts = {}) {
1406
1545
  verdict = "again";
1407
1546
  if (verdict === "again" && !(earlyError instanceof DecidioTimeoutError)) {
1408
1547
  log(`③ No decision was made (no y or n answer), so the tour stops here rather than guess.`);
1409
- log(` The request is still ROUTED and waiting in your queue - approve or reject it there, or re-run the tour.`);
1410
- log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's key). Once you`);
1411
- log(` have decided - or if you abandon the tour - remove them: npx @decidio/sdk quickstart cleanup`);
1548
+ if (DEMO) {
1549
+ log(` Nothing executed and no receipt was written. Nothing is kept: the demo's agent, token and folder go now.`);
1550
+ log(` Re-run and answer y or n (or use the web page / QR).`);
1551
+ }
1552
+ else {
1553
+ log(` The request is still ROUTED and waiting in your queue - approve or reject it there, or re-run the tour.`);
1554
+ log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's key). Once you`);
1555
+ log(` have decided - or if you abandon the tour - remove them: npx @decidio/sdk quickstart cleanup`);
1556
+ }
1412
1557
  log("");
1413
1558
  sessionReminder();
1414
- process.exit(3);
1559
+ currentRl?.close();
1560
+ stdinReadAbandoned = true;
1561
+ return 3;
1415
1562
  }
1416
1563
  if (earlyError instanceof DecidioTimeoutError) {
1417
1564
  log(`③ the tour's blocking call gave up after 5 minutes while waiting for your answer, so your answer is NOT posted:`);
1418
- log(` the request is still ROUTED in your queue - decide it there, or re-run the tour and answer within 5 minutes.`);
1419
- log(` NOTE: this tour's credentials are still live in ${envFile} - remove them: npx @decidio/sdk quickstart cleanup`);
1565
+ if (DEMO)
1566
+ log(` the demo ends here; nothing is kept (the demo's agent, token and folder go now). Re-run and answer within 5 minutes.`);
1567
+ else {
1568
+ log(` the request is still ROUTED in your queue - decide it there, or re-run the tour and answer within 5 minutes.`);
1569
+ log(` NOTE: this tour's credentials are still live in ${envFile} - remove them: npx @decidio/sdk quickstart cleanup`);
1570
+ }
1420
1571
  log("");
1421
1572
  sessionReminder();
1422
- process.exit(3);
1573
+ currentRl?.close();
1574
+ stdinReadAbandoned = true;
1575
+ return 3;
1423
1576
  }
1424
1577
  }
1425
1578
  else if (opts.autoApproveDemo) {
1426
- log("③ --auto-approve-demo: approving with your session credential. This is AUTOMATED authority");
1579
+ log(`③ --auto-approve-demo: approving with ${DEMO ? "the demo's mock credential (no account is involved)" : "your session credential"}. This is AUTOMATED authority`);
1427
1580
  log(" (a flag you passed, not a human decision in the moment) — fine for CI, not the real story.");
1428
1581
  }
1582
+ else if (DEMO) {
1583
+ log("③ This terminal can't put a human in the loop, and an approval IS a human act — the demo stops here");
1584
+ log(" rather than approve its own request. Nothing executed, no receipt was written, and nothing is kept:");
1585
+ log(" the demo's agent, token and working folder go now (the stand-in API lives only in this process).");
1586
+ log("");
1587
+ log(" Re-run in an interactive terminal and answer y or n there, on the web page or by QR — or, for CI:");
1588
+ log(" npx @decidio/sdk quickstart --demo --auto-approve-demo (the tour approves itself under that explicit label)");
1589
+ log("");
1590
+ log(" (exit 3 = the demo stopped at the human moment; deliberately not reported as a completed tour.)");
1591
+ stdinReadAbandoned = true;
1592
+ return 3;
1593
+ }
1429
1594
  else {
1430
1595
  log("③ This terminal can't put a human in the loop, and an approval IS a human act — the tour");
1431
1596
  log(" stops here rather than approve its own request. The decision is routed and waiting —");
@@ -1447,7 +1612,8 @@ async function cmdQuickstart(opts = {}) {
1447
1612
  log(` or if you abandon the tour — remove them: npx @decidio/sdk quickstart cleanup`);
1448
1613
  log("");
1449
1614
  sessionReminder();
1450
- process.exit(3);
1615
+ stdinReadAbandoned = true;
1616
+ return 3;
1451
1617
  }
1452
1618
  const rejected = verdict === "reject";
1453
1619
  if (rejected) {
@@ -1491,17 +1657,17 @@ async function cmdQuickstart(opts = {}) {
1491
1657
  return failTour(1);
1492
1658
  const issuer = rec.body?.issuer;
1493
1659
  const v = verifyReceipt(rec.body.credential, issuer);
1494
- const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", PINNED to your workspace issuer" : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
1660
+ const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", " + pinWords() : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
1495
1661
  if (!v.valid || !issuer) {
1496
1662
  log(` ${kindLine}`);
1497
1663
  err(` ✘ the denial's receipt did not verify PINNED (${!issuer ? "no issuer DID from the server" : v.reason ?? "no reason given"}) — the tour did not complete.`);
1498
1664
  return failTour(1);
1499
1665
  }
1500
1666
  block("④", "The record", [
1501
- `✔ VALID — ${fname} ${dim("(signature + content binding, offline, PINNED to your workspace issuer)")}`,
1667
+ `✔ VALID — ${fname} ${dim(`(signature + content binding, offline, ${pinWords()})`)}`,
1502
1668
  `${bold("decision")} payInvoice $86,000 · REJECTED · routed to a human by policy (no auto-approve rule for this agent)`,
1503
1669
  `${bold("record")} record kind: ${recordKindOf(rec.body.credential) ?? "?"} ${dim("— the denial is sealed too")}`,
1504
- `${bold("issuer")} ${issuer} ${dim(`· your trust anchor — first seen over this signed-in connection; cross-check ${API}/api/did, then verify --issuer <did> <receipt.json>`)}`,
1670
+ `${bold("issuer")} ${issuer} ${dim(issuerNote())}`,
1505
1671
  ], "bad");
1506
1672
  denialVerified = true;
1507
1673
  }
@@ -1529,7 +1695,8 @@ async function cmdQuickstart(opts = {}) {
1529
1695
  }
1530
1696
  log("");
1531
1697
  sessionReminder();
1532
- log(`${bold("Where next")} your queue ${link(APP_URL)} · README "Production mode: durable async resume" ${dim("· built by OmniTwin — decidioai.com/#early-access")}`);
1698
+ if (!DEMO)
1699
+ log(`${bold("Where next")} your queue ${link(APP_URL)} · README "Production mode: durable async resume" ${dim("· built by OmniTwin — decidioai.com/#early-access")}`);
1533
1700
  }
1534
1701
  else {
1535
1702
  const ap = decidedOutside ? { status: 200, body: { alreadyResolved: true } } : await api(`/api/decisions/${encodeURIComponent(decisionId)}/approve`, { method: "POST", body: JSON.stringify({ reason: opts.autoApproveDemo && !interactive ? "quickstart --auto-approve-demo (automated)" : "quickstart" }) }, session);
@@ -1565,7 +1732,7 @@ async function cmdQuickstart(opts = {}) {
1565
1732
  return failTour(1);
1566
1733
  const issuer = rec.body?.issuer;
1567
1734
  const v = verifyReceipt(rec.body.credential, issuer);
1568
- const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", PINNED to your workspace issuer" : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
1735
+ const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", " + pinWords() : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
1569
1736
  if (!v.valid || !issuer)
1570
1737
  log(` ${kindLine}`);
1571
1738
  if (!v.valid) {
@@ -1583,33 +1750,44 @@ async function cmdQuickstart(opts = {}) {
1583
1750
  const detail = await api(`/api/records/${encodeURIComponent(decisionId)}`, {}, session);
1584
1751
  const tier = detail.body?.record?.confirmation?.status;
1585
1752
  block("④", "The receipt", [
1586
- `✔ VALID — ${fname} ${dim("(signature + content binding, offline, PINNED to your workspace issuer)")}`,
1753
+ `✔ VALID — ${fname} ${dim(`(signature + content binding, offline, ${pinWords()})`)}`,
1587
1754
  `${bold("decision")} payInvoice $86,000 · APPROVED · routed to a human by policy (no auto-approve rule for this agent)`,
1588
1755
  `${bold("record")} record kind: ${recordKindOf(rec.body.credential) ?? "?"}${tier ? ` · evidence: ${tier}` : ""} ${dim("— the receipt seals the DECISION; execution evidence lives on the record")}`,
1589
- `${bold("issuer")} ${issuer} ${dim(`· your trust anchor — first seen over this signed-in connection; cross-check ${API}/api/did, then verify --issuer <did> <receipt.json>`)}`,
1756
+ `${bold("issuer")} ${issuer} ${dim(issuerNote())}`,
1590
1757
  ], "good");
1591
1758
  const secs = Math.round((Date.now() - t0) / 1000);
1592
1759
  block("✔", `Done in ${secs}s`, [
1593
1760
  `${bold("gated")} payInvoice $86,000 — ROUTED to a human as ${decisionId}`,
1594
- `${bold("decided")} ${decidedOutside ? "approved in the web app (or on your phone)" : opts.autoApproveDemo ? "approved by --auto-approve-demo (automated, and labeled so on the record)" : "approved by you, here"}`,
1761
+ `${bold("decided")} ${decidedOutside ? "approved in the web app (or on your phone)" : opts.autoApproveDemo ? `approved by --auto-approve-demo (automated — the ${DEMO ? "receipt's reason says so" : "record's reason says so"})` : "approved by you, here"}`,
1595
1762
  `${bold("executed")} your function ran once, AFTER approval`,
1596
- `${bold("receipt")} VALID — verified offline, pinned to your workspace issuer (④ above)`,
1763
+ `${bold("receipt")} VALID — verified offline, pinned to ${DEMO ? "this demo's own key" : "your workspace issuer"} (④ above)`,
1597
1764
  dim("gated → routed to a human → approved → executed once → a receipt you can verify offline"),
1598
1765
  ], "good");
1599
1766
  log("");
1600
- log(bold("Take it to your real agent — the .env here already works:"));
1601
- log(` import { guard } from "@decidio/sdk";`);
1602
- log(` const doIt = guard.protect(doItRaw, (a) => ({ action: "yourAction", amount: a.amount, scope: "YourScope" }));${dim(" // durable by default")}`);
1603
- log("");
1604
- if (EXPLAIN) {
1605
- log(` The README's seven-step flow is this same blocking flow in your own code; if you keep this agent, its .env`);
1606
- log(` already works here, so skip that flow's init step. Production is DURABLE mode (default) + one worker.`);
1767
+ if (DEMO) {
1768
+ log(bold("That was the mechanism — on this machine, against a stand-in. The product runs it against your workspace:"));
1769
+ log(` request access: decidioai.com/#early-access · have an invite? npx @decidio/sdk login, then npx @decidio/sdk quickstart`);
1770
+ log("");
1771
+ }
1772
+ else {
1773
+ log(bold("Take it to your real agent — the .env here already works:"));
1774
+ log(` import { guard } from "@decidio/sdk";`);
1775
+ log(` const doIt = guard.protect(doItRaw, (a) => ({ action: "yourAction", amount: a.amount, scope: "YourScope" }));${dim(" // durable by default")}`);
1776
+ log("");
1777
+ if (EXPLAIN) {
1778
+ log(` The README's seven-step flow is this same blocking flow in your own code; if you keep this agent, its .env`);
1779
+ log(` already works here, so skip that flow's init step. Production is DURABLE mode (default) + one worker.`);
1780
+ }
1607
1781
  }
1608
1782
  sessionReminder();
1609
- log(`${bold("Where next")} your queue ${link(APP_URL)} · README "Production mode: durable async resume" ${dim("· built by OmniTwin — decidioai.com/#early-access")}`);
1783
+ if (!DEMO)
1784
+ log(`${bold("Where next")} your queue ${link(APP_URL)} · README "Production mode: durable async resume" ${dim("· built by OmniTwin — decidioai.com/#early-access")}`);
1610
1785
  }
1611
1786
  let doCleanup = false;
1612
- if (interactive) {
1787
+ if (DEMO) {
1788
+ doCleanup = true;
1789
+ }
1790
+ else if (interactive) {
1613
1791
  const answer = await ask(`\nKeep this agent for development? [Enter = keep · c + Enter = clean up now]: `);
1614
1792
  doCleanup = answer.trim().toLowerCase() === "c";
1615
1793
  if (!doCleanup)
@@ -1757,7 +1935,7 @@ async function quickstartCleanup(session, envFile) {
1757
1935
  });
1758
1936
  if (kept.length === 0) {
1759
1937
  rmSync(envFile);
1760
- log(` ✔ ${envFile} deleted (the agent's local key is gone — the registered agent is now inert)`);
1938
+ log(` ✔ ${envFile} deleted (the agent's local key is gone — ${DEMO ? "the temporary folder follows" : "the registered agent is now inert"})`);
1761
1939
  }
1762
1940
  else {
1763
1941
  writeSecretFile(envFile, kept.join("\n") + "\n");
@@ -1773,9 +1951,13 @@ async function quickstartCleanup(session, envFile) {
1773
1951
  log(` npx @decidio/sdk approvals reject ${c.id} "quickstart cleanup"`);
1774
1952
  }
1775
1953
  }
1776
- log(` • What remains, by design: the sealed decision record (records are permanent — that is the product)`);
1777
- log(` and the agent's registration row (inert without key or token; server-side deregistration is on the roadmap).`);
1778
- if (existsSync(JOURNAL_PATH))
1954
+ if (DEMO)
1955
+ log(` • nothing remains: the demo's stand-in agent, token and files go with the temporary folder — only the receipt is kept`);
1956
+ else {
1957
+ log(` • What remains, by design: the sealed decision record (records are permanent — that is the product)`);
1958
+ log(` and the agent's registration row (inert without key or token; server-side deregistration is on the roadmap).`);
1959
+ }
1960
+ if (!DEMO && existsSync(JOURNAL_PATH))
1779
1961
  log(` • Also kept: ${JOURNAL_PATH} — the ids and timestamps of what this directory minted and revoked (no secrets). Gitignored; owner-only where the OS supports it.`);
1780
1962
  if (existsSync(".decidio-pending")) {
1781
1963
  log(` • Also kept: .decidio-pending/ — this agent's own execution ledger: whatever this agent parked (call arguments,`);
@@ -1783,6 +1965,8 @@ async function quickstartCleanup(session, envFile) {
1783
1965
  log(` pending and you do not want the history, remove it:`);
1784
1966
  log(` ${process.platform === "win32" ? "Remove-Item -Recurse .decidio-pending" : "rm -r .decidio-pending"}`);
1785
1967
  }
1968
+ if (DEMO)
1969
+ return 0;
1786
1970
  if (endSession())
1787
1971
  log(` ✔ signed out — the stored sign-in is removed from this computer (the token itself stays valid until it expires; npx @decidio/sdk login to sign in again).`);
1788
1972
  else if (process.env.DECIDIO_SESSION_TOKEN)
@@ -1898,7 +2082,7 @@ function cmdDev(target, port) {
1898
2082
  }).listen(port, "127.0.0.1");
1899
2083
  return 0;
1900
2084
  }
1901
- const USAGE = `decidio — the Decidio SDK command line
2085
+ const USAGE = `✦ Decidio — the SDK command line
1902
2086
 
1903
2087
  The wrapper IS the SDK: guard.protect(fn, describe) ships in this package (import { guard } from "@decidio/sdk").
1904
2088
  This command line is the tour, the sign-in, and the token admin around it.
@@ -1907,6 +2091,10 @@ const USAGE = `decidio — the Decidio SDK command line
1907
2091
 
1908
2092
  npx @decidio/sdk quickstart
1909
2093
 
2094
+ With no sign-in on this computer it first asks ONE question - do you have an invite? - and, if you do,
2095
+ what next: the tour, or wiring up your own agent. Every answer shows the command it stands for. The bare
2096
+ npx @decidio/sdk on a terminal asks the same (skipping the invite question when you are signed in).
2097
+
1910
2098
  It gates a demo action, routes it to YOU for approval, executes on your yes, and hands you a
1911
2099
  cryptographically verifiable receipt of the DECISION (execution evidence then accrues on the
1912
2100
  hosted record — the receipt seals who authorized what, not what ran afterward). Guided and
@@ -1923,7 +2111,9 @@ THE TOUR
1923
2111
  registration row and the permanent sealed record stay)
1924
2112
  (and end the stored sign-in)
1925
2113
  npx @decidio/sdk quickstart --auto-approve-demo headless tour; cleans up after itself
1926
- (add --keep-agent to KEEP the tour agent + token)
2114
+ npx @decidio/sdk quickstart --demo the same tour against a LOCAL stand-in of the API — no account needed; the
2115
+ web/phone door is a hosted demo page; the receipt is the one thing kept
2116
+ (--keep-agent is refused here: nothing of the demo's can be kept)
1927
2117
 
1928
2118
  SIGN IN (once)
1929
2119
  npx @decidio/sdk login [email] sign in ONCE: the CLI emails you a link, you paste it; the sign-in
@@ -1933,6 +2123,8 @@ SIGN IN (once)
1933
2123
 
1934
2124
  YOUR AGENT AND ITS TOKENS
1935
2125
  npx @decidio/sdk init [agentId] sign in, register the agent, mint its API token, write .env
2126
+ (agentId defaults to this folder's name; any name works - it is how the
2127
+ workspace shows the request and the key .env is written under)
1936
2128
  --unique append a random suffix (collision-proof samples)
1937
2129
  --force replace a different agent already in this .env
1938
2130
  (its .env is backed up first)
@@ -2075,6 +2267,16 @@ async function main(argv) {
2075
2267
  log(VERSION);
2076
2268
  return 0;
2077
2269
  }
2270
+ if (!cmd && process.stdin.isTTY && process.stdout.isTTY) {
2271
+ log(brand() + " — gate an action, route it to a human, execute on approval, hold the proof. " + dim("(npx @decidio/sdk --help for every command)"));
2272
+ const stored = process.env.DECIDIO_SESSION_TOKEN ? "" : loadSession()?.email ?? null;
2273
+ const pick = await firstRunFork(process.env.DECIDIO_SESSION_TOKEN ? "" : stored);
2274
+ if (pick.kind === "stop")
2275
+ return pick.code;
2276
+ if (pick.kind === "init")
2277
+ return cmdInit(pick.agentId, {});
2278
+ return cmdQuickstart({ demo: pick.kind === "demo" });
2279
+ }
2078
2280
  if (!cmd || wantsHelp(cmd)) {
2079
2281
  log(USAGE);
2080
2282
  return 0;
@@ -2089,7 +2291,7 @@ async function main(argv) {
2089
2291
  }
2090
2292
  const argsIn = argv.slice(3);
2091
2293
  const KNOWN = {
2092
- quickstart: { flags: ["--explain", "--auto-approve-demo", "--keep-agent"], valued: ["--phone"] },
2294
+ quickstart: { flags: ["--explain", "--auto-approve-demo", "--keep-agent", "--demo"], valued: ["--phone"] },
2093
2295
  login: { flags: [] },
2094
2296
  logout: { flags: [] },
2095
2297
  init: { flags: ["--unique", "--force"], valued: ["--expires-days"] },
@@ -2185,7 +2387,18 @@ async function main(argv) {
2185
2387
  return 1;
2186
2388
  return quickstartCleanup(session, envFile);
2187
2389
  }
2188
- return cmdQuickstart({ autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent"), explain: argsIn.includes("--explain"), phone: valuedOpt(argsIn, "--phone") });
2390
+ const qopts = { demo: argsIn.includes("--demo"), autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent"), explain: argsIn.includes("--explain"), phone: valuedOpt(argsIn, "--phone") };
2391
+ if (!qopts.demo && !qopts.autoApproveDemo && forkApplies()) {
2392
+ log(brand() + bold(" quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
2393
+ const pick = await firstRunFork();
2394
+ if (pick.kind === "stop")
2395
+ return pick.code;
2396
+ if (pick.kind === "init")
2397
+ return cmdInit(pick.agentId, {});
2398
+ if (pick.kind === "demo")
2399
+ qopts.demo = true;
2400
+ }
2401
+ return cmdQuickstart(qopts);
2189
2402
  }
2190
2403
  case "init": {
2191
2404
  const unique = argsIn.includes("--unique");
@@ -2198,7 +2411,7 @@ async function main(argv) {
2198
2411
  }
2199
2412
  const ddIdx = argsIn.indexOf("--");
2200
2413
  const nameArg = ddIdx >= 0 ? argsIn[ddIdx + 1] : argsIn.find((x, i) => !x.startsWith("-") && !(expIdx >= 0 && i === expIdx + 1));
2201
- let name = nameArg ?? process.env.DECIDIO_AGENT_ID ?? "my-agent";
2414
+ let name = nameArg ?? (process.env.DECIDIO_AGENT_ID || defaultAgentId());
2202
2415
  if (unique)
2203
2416
  name = `${name}-${randomBytes(2).toString("hex")}`;
2204
2417
  return cmdInit(name, { force: argsIn.includes("--force"), expiresDays: expRaw ? Number(expRaw) : undefined });
@@ -0,0 +1,16 @@
1
+ export interface DemoGate {
2
+ /** http://127.0.0.1:<port> — point the tour here. */
3
+ url: string;
4
+ /** The did:key of the key generated for THIS run (never the hosted issuer's). */
5
+ issuerDid: string;
6
+ /** Whether the hosted relay answered (the web/phone door exists) — null until session-link was asked. */
7
+ relayReachable: () => boolean | null;
8
+ close(): Promise<void>;
9
+ }
10
+ export interface DemoGateOptions {
11
+ /** Base URL of the hosted relay; null = offline (terminal-only). Default: the hosted sandbox API. */
12
+ relayBase?: string | null;
13
+ /** Milliseconds to wait for the relay on each call. */
14
+ relayTimeoutMs?: number;
15
+ }
16
+ export declare function startDemoGate(opts?: DemoGateOptions): Promise<DemoGate>;
@@ -0,0 +1,170 @@
1
+ import { createServer } from "node:http";
2
+ import { createHash, generateKeyPairSync, randomBytes, sign as edSign } from "node:crypto";
3
+ import { jcsCanonicalize } from "@decidio/verify";
4
+ const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
5
+ function b58(buf) {
6
+ const digits = [0];
7
+ for (const byte of buf) {
8
+ let carry = byte;
9
+ for (let i = 0; i < digits.length; i++) {
10
+ carry += digits[i] << 8;
11
+ digits[i] = carry % 58;
12
+ carry = (carry / 58) | 0;
13
+ }
14
+ while (carry) {
15
+ digits.push(carry % 58);
16
+ carry = (carry / 58) | 0;
17
+ }
18
+ }
19
+ let out = "";
20
+ for (const b of buf) {
21
+ if (b === 0)
22
+ out += B58[0];
23
+ else
24
+ break;
25
+ }
26
+ return out + digits.reverse().map((d) => B58[d]).join("");
27
+ }
28
+ const DECISION_ID = "D-DEMO-1";
29
+ const D_NUMBER = "D·0001";
30
+ export async function startDemoGate(opts = {}) {
31
+ const relayBase = opts.relayBase === undefined ? "https://decidio-api.onrender.com" : opts.relayBase;
32
+ const relayTimeoutMs = opts.relayTimeoutMs ?? 2000;
33
+ const issuerKey = generateKeyPairSync("ed25519");
34
+ const issuerRaw = Buffer.from(issuerKey.publicKey.export({ format: "der", type: "spki" })).subarray(-32);
35
+ const issuerDid = "did:key:z" + b58(Buffer.concat([Buffer.from([0xed, 0x01]), issuerRaw]));
36
+ const sealedAt = new Date().toISOString();
37
+ let verdict = null;
38
+ let decidedBy = "the human at the terminal";
39
+ let lastReason = "quickstart";
40
+ let relayCode = null;
41
+ let relayOk = null;
42
+ const relayFetch = async (path, init = {}) => {
43
+ if (!relayBase)
44
+ return null;
45
+ const ctl = new AbortController();
46
+ const t = setTimeout(() => ctl.abort(), relayTimeoutMs);
47
+ try {
48
+ const r = await fetch(`${relayBase.replace(/\/$/, "")}${path}`, { ...init, signal: ctl.signal, headers: { "content-type": "application/json", ...(init.headers ?? {}) } });
49
+ if (r.status === 204)
50
+ return {};
51
+ if (!r.ok)
52
+ return null;
53
+ return await r.json().catch(() => ({}));
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ finally {
59
+ clearTimeout(t);
60
+ }
61
+ };
62
+ const issueReceipt = (kind) => {
63
+ const canonical = jcsCanonicalize({ accountableParty: "[PERSON_1]", action: "payInvoice", amount: 86000, currency: "USD", decidedBy, reason: lastReason, recordKind: kind, recordSeries: "local-demo" });
64
+ const contentHash = createHash("sha256").update(canonical, "utf8").digest("hex");
65
+ const context = ["https://www.w3.org/ns/credentials/v2"];
66
+ const doc = {
67
+ "@context": context,
68
+ type: ["VerifiableCredential", "DecidioAuthorityRecord"],
69
+ issuer: issuerDid,
70
+ validFrom: sealedAt,
71
+ credentialSubject: { id: `urn:decidio:decision:${DECISION_ID}`, recordId: DECISION_ID, dNumber: D_NUMBER, contentHash, prevHash: null, recordCanonical: canonical },
72
+ };
73
+ const cfg = {
74
+ "@context": context, type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022",
75
+ created: sealedAt, verificationMethod: `${issuerDid}#${issuerDid.slice("did:key:".length)}`, proofPurpose: "assertionMethod",
76
+ };
77
+ const signingInput = Buffer.concat([
78
+ createHash("sha256").update(Buffer.from(jcsCanonicalize(cfg), "utf8")).digest(),
79
+ createHash("sha256").update(Buffer.from(jcsCanonicalize(doc), "utf8")).digest(),
80
+ ]);
81
+ return { ...doc, proof: { ...cfg, proofValue: "z" + b58(edSign(null, signingInput, issuerKey.privateKey)) } };
82
+ };
83
+ const server = createServer((req, res) => {
84
+ let raw = "";
85
+ req.on("data", (c) => (raw += c));
86
+ req.on("end", async () => {
87
+ let body;
88
+ try {
89
+ body = raw ? JSON.parse(raw) : undefined;
90
+ }
91
+ catch {
92
+ body = undefined;
93
+ }
94
+ const path = (req.url ?? "").split("?")[0];
95
+ const send = (o, status = 200) => { res.writeHead(status, { "content-type": "application/json" }); res.end(JSON.stringify(o)); };
96
+ try {
97
+ if (path === "/api/health")
98
+ return send({ ok: true, demo: true });
99
+ if (path === "/api/did")
100
+ return send({ did: issuerDid, method: "did:key", alg: "Ed25519", demo: true });
101
+ if (path === "/api/decisions/cards")
102
+ return send({ cards: [] });
103
+ if (path === "/api/agents/register")
104
+ return send({ agentId: body?.agentId, did: body?.did });
105
+ if (path === "/api/workspace/agent-token" && req.method === "POST")
106
+ return send({ token: "demo-agent-token", workspaceId: "local-demo", jti: `jti-demo-${randomBytes(4).toString("hex")}`, expiresAt: new Date(Date.now() + 3_600_000).toISOString() });
107
+ if (path === "/api/workspace/agent-token" && req.method === "GET")
108
+ return send({ tokens: [] });
109
+ if (/^\/api\/workspace\/agent-token\/[^/]+$/.test(path) && req.method === "DELETE")
110
+ return send({ ok: true, changed: true });
111
+ if (path === "/agent/authorize") {
112
+ verdict = null;
113
+ return send({ decision: "route", decisionId: DECISION_ID, receiptId: "R-DEMO-1" });
114
+ }
115
+ if (path.startsWith("/agent/status/")) {
116
+ if (!verdict && relayCode) {
117
+ const r = await relayFetch(`/demo/relay/${relayCode}`);
118
+ if (r && (r.verdict === "approve" || r.verdict === "reject")) {
119
+ verdict = r.verdict;
120
+ decidedBy = "the demo page (web or phone), through the relay";
121
+ lastReason = "decided on the demo page";
122
+ }
123
+ }
124
+ return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver" } : { state: verdict === "approve" ? "approved" : "pending" });
125
+ }
126
+ if (/^\/api\/decisions\/[^/]+\/(approve|reject)$/.test(path)) {
127
+ const already = verdict !== null;
128
+ verdict = path.endsWith("/reject") ? "reject" : "approve";
129
+ if (!already) {
130
+ lastReason = typeof body?.reason === "string" && body.reason ? body.reason : "quickstart";
131
+ decidedBy = /automated/i.test(lastReason) ? "automated: the --auto-approve-demo flag (not a human in the moment)" : "the human at the terminal";
132
+ }
133
+ if (relayCode && !already)
134
+ void relayFetch(`/demo/relay/${relayCode}`, { method: "PUT", body: JSON.stringify({ verdict }) });
135
+ return send({ ok: true, decisionId: DECISION_ID, sealed: true, ...(already ? { alreadyResolved: true } : {}) });
136
+ }
137
+ if (path === "/agent/confirm")
138
+ return send({ ok: true, confirmation: "application_confirmed", recordId: DECISION_ID });
139
+ if (path === "/demo/session-link") {
140
+ const r = await relayFetch("/demo/relay", { method: "POST", body: JSON.stringify({ decisionId: DECISION_ID }) });
141
+ if (r && typeof r.code === "string" && /^[0-9A-HJKMNP-TV-Z]{13}$/.test(r.code) && typeof r.link === "string" && /^https?:\/\//.test(r.link)) {
142
+ relayCode = r.code;
143
+ relayOk = true;
144
+ return send({ status: "ok", link: r.link, qr: typeof r.qr === "string" ? r.qr : null, expiresAt: r.expiresAt ?? null });
145
+ }
146
+ relayOk = false;
147
+ return send({ status: "unavailable", error: "the demo relay is unreachable — this run is terminal-only" }, 503);
148
+ }
149
+ if (/^\/api\/records\/[^/]+\/verify$/.test(path)) {
150
+ const cred = issueReceipt(verdict === "reject" ? "denial" : "approval");
151
+ return send({ dNumber: D_NUMBER, issuer: issuerDid, alg: "Ed25519", valid: true, signatureValid: true, contentBound: true, matchesRecord: true, payloadMatches: true, hash: cred.credentialSubject.contentHash, prevHash: null, credential: cred });
152
+ }
153
+ if (/^\/api\/records\/[^/]+$/.test(path))
154
+ return send({ record: { confirmation: verdict === "approve" ? { status: "application_confirmed", confirmed: true } : { status: "pending", confirmed: false } } });
155
+ return send({ error: "not part of the local demo" }, 404);
156
+ }
157
+ catch (e) {
158
+ return send({ error: String(e?.message ?? e) }, 500);
159
+ }
160
+ });
161
+ });
162
+ await new Promise((r) => server.listen(0, "127.0.0.1", r));
163
+ const port = server.address().port;
164
+ return {
165
+ url: `http://127.0.0.1:${port}`,
166
+ issuerDid,
167
+ relayReachable: () => relayOk,
168
+ close: () => new Promise((r) => { server.closeAllConnections(); server.close(() => r()); }),
169
+ };
170
+ }
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.5.0 # tracks the SDK release - checked by packages/conformance/manifests.mjs
8
+ version: 0.6.1 # tracks the SDK release - checked by packages/conformance/manifests.mjs
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.5.0",
3
+ "version": "0.6.1",
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",
@@ -84,7 +84,7 @@
84
84
  "prepublishOnly": "npm run typecheck && npm run build && node ../publish-boundary/check.mjs --only @decidio/sdk"
85
85
  },
86
86
  "dependencies": {
87
- "@decidio/verify": "0.5.0"
87
+ "@decidio/verify": "0.6.1"
88
88
  },
89
89
  "author": "OmniTwin Technologies Inc.",
90
90
  "bugs": {