@decidio/sdk 0.6.0 → 0.6.2

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,67 @@ 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.2 — the fork after its first real run
8
+
9
+ Founder UAT of 0.6.1 on a Windows console, same day.
10
+
11
+ - **Prompts pause.** On the founder's cmd.exe the agent-id prompt "took the folder's" name without waiting. The
12
+ terminal driver could not reproduce it, so this is defence plus instrumentation: one readline now lives for the whole
13
+ command (a console's `\r\n` can no longer split across two questions - the previous per-question interfaces could
14
+ hand the second byte to the next prompt as Enter), and `DECIDIO_DEBUG_INPUT=1` prints the raw bytes stdin delivers, so
15
+ a recurrence names its cause instead of a theory.
16
+ - **"What next?" has a way out:** `3 sign out of this computer (npx @decidio/sdk logout)`.
17
+ - **Option 2 in a folder that already holds an agent** says so before asking for a name (with cleanup / a new folder /
18
+ `--force` as the ways out), instead of `init` refusing afterwards with text that read like a prompt.
19
+ - **The bare command is update-gated** on a terminal, so a cached old npx copy announces the newer version instead of
20
+ quietly showing an old first screen; `npx @decidio/sdk` is enough, `@latest` is not needed.
21
+ - **Sign in by CODE, not by paste.** When the command line asks for a sign-in, the email it triggers carries an
22
+ 8-character code and nothing to click; you type the code into the terminal that asked (dashes, case and an O for a 0
23
+ are forgiven). Three misses and the code is dead. The web app's sign-in screen keeps its link email unchanged; two
24
+ flows, each with exactly one action. Why this shape and not "click the link and the terminal continues": that design
25
+ moves a session to a DIFFERENT device than the one that clicked, which is a new attack class the privacy council had
26
+ to harden (docs/Decidio_CLI_Signin_Wait_Spec.md, retired); a code travels from the inbox to the device you are
27
+ sitting at, and an attacker who triggers the email gains nothing. A pasted link is still accepted, quietly.
28
+ - **`npx decidio`.** A new npm package named `decidio` holds one file: it resolves the `@decidio/sdk` it pins (same
29
+ version, released in lockstep) and runs that CLI. `npx decidio …` is `npx @decidio/sdk …` with fewer keystrokes; the
30
+ update gate names whichever package you came through. Not something to import - the SDK stays `@decidio/sdk`.
31
+
32
+ ## 0.6.1 — the first-run fork, and the local demo's loose ends
33
+
34
+ Two independent first-run reviews of 0.6.0 (grades B / A− overall) agreed on one thing: the hosted tour and the
35
+ verifier earn trust, and the new local demo had polish debts. This release pays them and adds the one question a
36
+ stranger's first minute was missing (founder ruling 2026-09-06).
37
+
38
+ - **One question, then the thing you picked.** A bare `npx @decidio/sdk quickstart` (or a bare `npx @decidio/sdk`) on
39
+ a terminal with no sign-in asks: do you have a Decidio invite? `n` runs the local demo; `y` signs you in (the emailed
40
+ link) and asks what next: the two-minute tour, or wiring up your own agent (`init`). Every answer prints the exact
41
+ command it stands for, so the questions teach their own flags and you never need them twice. Nothing is stored;
42
+ Enter is not an answer; scripts and CI never see a question (they get today's text, which now also names `--demo`).
43
+ A signed-in `quickstart` runs straight away; the bare `npx @decidio/sdk` for a signed-in user skips the invite
44
+ question and asks only "what next?" - the bare command always leads somewhere, usage is a `--help` away. After
45
+ the sign-in step, the tour does not ask you to sign in again.
46
+ - **Your agent's name has a real default.** `init` with no name used `my-agent`; it now uses this folder's name (or
47
+ the package name), slugified, and the fork explains what the name is for ("Requested by …", the key `.env` is
48
+ written under). Any name still works.
49
+ - **A mark, not a banner:** the title lines carry the app's ✦ beside the name.
50
+ - **Headless `--demo` leaves nothing behind** (review: Medium). The stop at the human moment used to exit inside the
51
+ tour, skipping the wrapper that removes the temporary folder - the mock key, mock token and journal stayed in the
52
+ temp dir, and the message pointed at the hosted tour's remedies (`approvals approve`, `quickstart cleanup`), which
53
+ cannot act on a stand-in that lives only in that process. The stop now returns through the wrapper, and says the
54
+ demo's truth: nothing executed, nothing kept, re-run interactively or with `--auto-approve-demo`.
55
+ - **The demo receipt carries what the terminal claims** (review: Medium). Its canonical record now holds the real
56
+ amount and currency, the reason the decision arrived with (the automated flag names itself there, exactly as it does
57
+ in a hosted record's reason), and how it was decided (terminal, page, or the flag). The party stays tokenized, as
58
+ the real seal keeps it.
59
+ - **`--demo --keep-agent` is refused** (both reviews: Low) - nothing of the demo's can be kept, and the flag used to
60
+ be accepted while the opposite happened. Exit 2, with the reason.
61
+ - **A receipt the demo could not copy home is not a completed tour** (retest: Low): the temporary folder is kept and
62
+ named, and the run exits non-zero, instead of deleting the only copy and exiting 0.
63
+ - **Words that were not true** (review: Low): the demo's Target line no longer says "nothing hosted" while the web
64
+ door uses a hosted relay; the automated line names the demo's mock credential, not "your session credential"; a
65
+ repeat run's ④ frame names the suffixed file that actually lands; the README no longer calls the sandbox "one shared
66
+ lab workspace" in two places while saying the opposite in a third.
67
+
7
68
  ## 0.6.0 — `quickstart --demo`: the whole tour on your machine, no account
8
69
 
9
70
  An uninvited developer could not run the tour at all: a workspace needs an invite (no open signup, by ruling). A
package/README.md CHANGED
@@ -9,8 +9,12 @@ 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 # the short form: asks one question, then does the thing you picked
12
13
  npx @decidio/sdk quickstart # against your workspace (you need an invite - request one below)
13
14
  npx @decidio/sdk quickstart --demo # no invite yet? the same tour on this machine, against a local stand-in of the API
15
+ # Not sure? The bare command asks: with no sign-in on this computer it asks whether you have an
16
+ # invite (n → the local demo; y → sign in, then the tour or your own agent). Every answer prints
17
+ # the command it stands for, so you never need the questions twice.
14
18
  ```
15
19
 
16
20
  One command, guided: it registers a fresh agent (key generated locally), gates a demo action
@@ -71,8 +75,8 @@ npx @decidio/sdk init --unique my-agent # --unique appends a random suffix: co
71
75
  Every network command talks to the hosted sandbox by default (`verify` is offline) and announces its target host on
72
76
  stderr each run — set `DECIDIO_API_URL` (env or `.env`) to point at another Decidio instead.
73
77
 
74
- `init` signs you into your workspace (email + password, or paste the sign-in link from your
75
- workspace email), generates the agent's Ed25519 keypair **locally** (only the public `did:key`
78
+ `init` signs you into your workspace (email + password, or the 8-character code the CLI emails you —
79
+ type it where you asked), generates the agent's Ed25519 keypair **locally** (only the public `did:key`
76
80
  is sent), registers the agent, mints its **floor-limited API token** (limited to the agent runtime — request authorization, poll status, report signed confirmation; it cannot administer the workspace or approve anything — and your workspace session never touches the agent's disk), and writes `.env`.
77
81
  Every later command that talks to Decidio reads `.env` from this directory automatically (`verify` is
78
82
  offline and never does). Re-running init for an
@@ -88,7 +92,7 @@ agent name — if it's already taken in your workspace, `init` stops cleanly and
88
92
  fresh one (or pass `--unique` to auto-append a random suffix — collision-proof for shared
89
93
  workspaces and CI). Interactive terminals sign in once (`npx @decidio/sdk login` — stored owner-only in your
90
94
  profile, bound to this terminal window, 30 days). Headless terminal (no prompts possible)? Export `DECIDIO_SESSION_TOKEN` first:
91
- request a sign-in link, then exchange its `?login=` value at `POST /demo/login-consume` — the
95
+ request a sign-in link from the web app's sign-in screen, then exchange its `?login=` value at `POST /demo/login-consume` — the
92
96
  response's `token` is your session token (the CLI prints these exact instructions when it
93
97
  can't prompt). That session token is your workspace login, not the agent's credential: unset it
94
98
  before launching agent code from the same shell, because child processes inherit exported
@@ -137,8 +141,8 @@ the agent's floor-limited token — it cannot approve anything):
137
141
 
138
142
  ```bash
139
143
  npx @decidio/sdk approvals --mine # see it pending (--mine narrows to THIS agent; the
140
- # sandbox is one shared lab workspace, so the bare
141
- # command lists every member's pending decisions)
144
+ # workspace is yours alone unless you invite others,
145
+ # so the bare command lists every pending decision in it)
142
146
  npx @decidio/sdk approvals approve <decisionId>
143
147
  ```
144
148
 
@@ -747,7 +751,7 @@ the next reader grades a promise, not a moving target:
747
751
 
748
752
  ## Using this for real?
749
753
 
750
- The hosted sandbox is synthetic and shared. Production workspaces are per-tenant, with your own
754
+ The hosted sandbox is synthetic data in a workspace of your own. Production workspaces are per-tenant too, with your own
751
755
  issuer key and receipts that verify offline against it.
752
756
 
753
757
  [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
@@ -2,7 +2,7 @@
2
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
4
  import { homedir, tmpdir } from "node:os";
5
- import { join as joinPath, resolve as resolvePath, sep as pathSep } from "node:path";
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";
@@ -416,6 +416,7 @@ const paint = (code, s) => (TTY_OUT ? `\x1b[${code}m${s}\x1b[0m` : s);
416
416
  const bold = (s) => paint("1", s);
417
417
  const dim = (s) => paint("2", s);
418
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"))}`;
419
420
  const FRAME_WIDTH = 84;
420
421
  const visibleLength = (s) => s.replace(/\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\/g, "").length;
421
422
  function frameWidth(rows) {
@@ -466,24 +467,42 @@ export function approvalAnswer(answer) {
466
467
  return "reject";
467
468
  return "again";
468
469
  }
470
+ let sharedRl = null;
471
+ let sharedRlOpen = false;
472
+ function closeSharedRl() { if (sharedRlOpen) {
473
+ sharedRlOpen = false;
474
+ sharedRl?.close();
475
+ } }
469
476
  function ask(question, mute = false) {
470
477
  if (process.stdin.readableEnded || process.stdin.destroyed)
471
478
  return Promise.resolve("");
472
479
  return new Promise((resolve) => {
473
- const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
480
+ if (!sharedRl || !sharedRlOpen) {
481
+ sharedRl = createInterface({ input: process.stdin, output: process.stderr, terminal: true, crlfDelay: Infinity });
482
+ sharedRlOpen = true;
483
+ sharedRl.once("close", () => { sharedRlOpen = false; });
484
+ }
485
+ const rl = sharedRl;
474
486
  currentRl = rl;
487
+ let restore = null;
475
488
  if (mute) {
476
489
  const anyRl = rl;
477
490
  const write = anyRl._writeToOutput?.bind(rl);
478
491
  anyRl._writeToOutput = (s) => { if (s.includes(question))
479
492
  write?.(s); };
480
- }
481
- rl.once("close", () => { if (currentRl === rl)
482
- currentRl = null; resolve(""); });
483
- rl.question(question, (answer) => { resolve(answer.trim()); rl.close(); if (mute)
484
- process.stderr.write("\n"); });
493
+ restore = () => { anyRl._writeToOutput = write; };
494
+ }
495
+ const onClose = () => { if (currentRl === rl)
496
+ currentRl = null; restore?.(); resolve(""); };
497
+ rl.once("close", onClose);
498
+ rl.question(question, (answer) => { rl.off("close", onClose); if (currentRl === rl)
499
+ currentRl = null; restore?.(); if (mute)
500
+ process.stderr.write("\n"); resolve(answer.trim()); });
485
501
  });
486
502
  }
503
+ if (process.env.DECIDIO_DEBUG_INPUT && process.stdin.isTTY) {
504
+ process.stdin.on("data", (c) => { process.stderr.write(`[stdin ${Buffer.from(c).toString("hex")}]\n`); });
505
+ }
487
506
  function terminalId() {
488
507
  for (const k of ["WT_SESSION", "VSCODE_IPC_HOOK_CLI", "TERM_SESSION_ID", "ITERM_SESSION_ID", "TMUX_PANE", "KONSOLE_DBUS_SESSION", "GNOME_TERMINAL_SCREEN", "WINDOWID"]) {
489
508
  const v = process.env[k];
@@ -566,29 +585,37 @@ async function loginFlow(purpose, emailArg) {
566
585
  }
567
586
  log(` ✘ password sign-in failed (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}. No problem — the emailed link works for every account:`);
568
587
  }
569
- const req = await api("/demo/login-link", { method: "POST", body: JSON.stringify({ email }) }, "");
588
+ const req = await api("/demo/login-link", { method: "POST", body: JSON.stringify({ email, code: true }) }, "");
570
589
  if (!(req.status >= 200 && req.status < 300)) {
571
590
  log(` ✘ could not request a sign-in email (${req.status})${req.body?.error ? ` — ${req.body.error}` : ""}. Check the address and re-run.`);
572
591
  return null;
573
592
  }
574
- log(` ✔ sign-in email requested for ${email} — check your inbox and spam; paste the link (or its login= code; single-use, 15 minutes)`);
575
- log(dim(` invite-only lab: a link arrives only for an invited address — not invited yet? request access: decidioai.com/#early-access`));
593
+ log(` ✔ sign-in email sent to ${email} — it holds an 8-character code (single-use, 15 minutes). Check spam too.`);
594
+ log(dim(` invite-only lab: an email arrives only for an invited address — not invited yet? request access: decidioai.com/#early-access`));
576
595
  log(dim(` or see the whole story on this machine right now, no account: npx @decidio/sdk quickstart --demo`));
577
- const pasted = await ask(" link or code: ");
578
- if (!pasted) {
579
- log(" ✘ nothing pasted — re-run when you have the email.");
580
- return null;
581
- }
582
- const m = pasted.match(/[?&]login=([^&\s]+)/);
583
- const linkToken = m ? decodeURIComponent(m[1]) : pasted;
584
- const r = await api("/demo/login-consume", { method: "POST", body: JSON.stringify({ token: linkToken }) }, "");
585
- if (r.status >= 200 && r.status < 300 && r.body?.token) {
586
- const s = saveSession(String(r.body.token), email);
587
- log(` ✔ signed in as ${email} until ${s.expiresAt.slice(0, 10)}${s.terminal ? "" : dim(" (no terminal id here: the sign-in ends on logout or cleanup, not on close)")}`);
588
- return s.token;
596
+ for (let attempt = 1; attempt <= 3; attempt++) {
597
+ const typed = await ask(" code from the email: ");
598
+ if (!typed) {
599
+ log(" ✘ nothing typed — re-run when you have the email.");
600
+ return null;
601
+ }
602
+ const m = typed.match(/[?&]login=([^&\s]+)/);
603
+ const isLink = !!m || /^[0-9a-f]{32,}$/i.test(typed);
604
+ const body = isLink ? { token: m ? decodeURIComponent(m[1]) : typed } : { token: "c." + typed.toUpperCase().replace(/[^0-9A-Z]/g, "").replace(/O/g, "0").replace(/[IL]/g, "1").slice(0, 8), email };
605
+ const r = await api("/demo/login-consume", { method: "POST", body: JSON.stringify(body) }, "");
606
+ if (r.status >= 200 && r.status < 300 && r.body?.token) {
607
+ const s = saveSession(String(r.body.token), email);
608
+ log(` ✔ signed in as ${email} until ${s.expiresAt.slice(0, 10)}${s.terminal ? "" : dim(" (no terminal id here: the sign-in ends on logout or cleanup, not on close)")}`);
609
+ return s.token;
610
+ }
611
+ if (isLink) {
612
+ log(` ✘ that link didn't work (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}. Sign-in links are single-use and expire in 15 minutes — request a fresh one and re-run.`);
613
+ return null;
614
+ }
615
+ if (attempt < 3)
616
+ log(` ✘ that code didn't work — check the email and try again (${3 - attempt} ${3 - attempt === 1 ? "try" : "tries"} left)`);
589
617
  }
590
- log(` ✘ that link didn't work (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}.`);
591
- log(` Sign-in links are single-use and expire in 15 minutes — request a fresh one and re-run.`);
618
+ log(` ✘ three misses — that code is now dead. Re-run to get a fresh email.`);
592
619
  return null;
593
620
  }
594
621
  async function acquireSession(purpose) {
@@ -608,12 +635,95 @@ async function acquireSession(purpose) {
608
635
  if (!process.stdin.isTTY) {
609
636
  err(`✘ ${purpose} needs a Decidio workspace sign-in, and this terminal can't prompt for one.`);
610
637
  err(` Fix: run \`npx @decidio/sdk login\` in an interactive terminal first (the sign-in is stored for this terminal),`);
611
- err(` or set DECIDIO_SESSION_TOKEN. To mint one by hand: request a sign-in link, then`);
638
+ 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`);
612
639
  err(` curl -X POST ${API}/demo/login-consume -H "content-type: application/json" -d '{"token":"<login value>"}' # the response's "token"`);
613
640
  return null;
614
641
  }
615
642
  return loginFlow(purpose);
616
643
  }
644
+ export function defaultAgentId(dir = process.cwd()) {
645
+ let name = "";
646
+ try {
647
+ const pkg = JSON.parse(readFileSync(joinPath(dir, "package.json"), "utf8"));
648
+ if (typeof pkg?.name === "string")
649
+ name = pkg.name.replace(/^@[^/]+\//, "");
650
+ }
651
+ catch { }
652
+ if (!name)
653
+ name = basename(dir);
654
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
655
+ return slug || "my-agent";
656
+ }
657
+ async function firstRunFork(signedInAs = null) {
658
+ const pick = async (prompt, ok) => {
659
+ let a = "";
660
+ for (let i = 0; i < 3 && !ok.test(a); i++)
661
+ a = (await ask(i === 0 ? " › " : ` Please answer ${prompt}: `)).trim().toLowerCase();
662
+ return ok.test(a) ? a : "";
663
+ };
664
+ log("");
665
+ if (signedInAs !== null) {
666
+ log(` Signed in${signedInAs ? ` as ${signedInAs}` : ""} on this computer.`);
667
+ }
668
+ else {
669
+ log(` Do you have a Decidio invite? ${dim("[y/n]")}`);
670
+ log(` ${bold("n")} the local demo on this machine, no account ${dim("npx @decidio/sdk quickstart --demo")}`);
671
+ log(` ${bold("y")} sign in to your workspace ${dim("npx @decidio/sdk login")}`);
672
+ const a = await pick("y or n", /^[yn]$/);
673
+ if (!a) {
674
+ log(` No answer — nothing was done. Run either command above when you are ready.`);
675
+ return { kind: "stop", code: 1 };
676
+ }
677
+ if (a === "n") {
678
+ log(` → running: npx @decidio/sdk quickstart --demo`);
679
+ log("");
680
+ return { kind: "demo" };
681
+ }
682
+ log(` → running: npx @decidio/sdk login`);
683
+ const token = await loginFlow("Signing in");
684
+ if (!token)
685
+ return { kind: "stop", code: 1 };
686
+ }
687
+ log("");
688
+ log(` What next? ${dim("[1/2/3]")}`);
689
+ log(` ${bold("1")} the two-minute tour against your workspace ${dim("npx @decidio/sdk quickstart")}`);
690
+ log(` ${bold("2")} wire up your own agent now (register, token, .env) ${dim("npx @decidio/sdk init <agentId>")}`);
691
+ log(` ${bold("3")} sign out of this computer ${dim("npx @decidio/sdk logout")}`);
692
+ const b = await pick("1, 2 or 3", /^[123]$/);
693
+ if (!b) {
694
+ log(` No answer — you are signed in; run any command above when you are ready.`);
695
+ return { kind: "stop", code: 0 };
696
+ }
697
+ if (b === "1") {
698
+ log(` → running: npx @decidio/sdk quickstart ${dim("(signed in - it will not ask again)")}`);
699
+ log("");
700
+ return { kind: "tour" };
701
+ }
702
+ if (b === "3") {
703
+ log(` → running: npx @decidio/sdk logout`);
704
+ log("");
705
+ return { kind: "logout" };
706
+ }
707
+ const here = readEnvFile();
708
+ if (["DECIDIO_AGENT_KEY", "DECIDIO_API_TOKEN"].some((k) => (here[k] ?? "").trim()) && here.DECIDIO_AGENT_ID) {
709
+ const tour = /^quickstart-/.test(here.DECIDIO_AGENT_ID);
710
+ log(` This folder's .env already holds an agent (${tour ? "the tour's, " : ""}"${here.DECIDIO_AGENT_ID}"). A new agent here would overwrite its key.`);
711
+ log(` Either: start in a new folder ${dim("mkdir <name> && cd <name> && npx @decidio/sdk")}`);
712
+ if (tour)
713
+ log(` or: clear the tour's agent first ${dim("npx @decidio/sdk quickstart cleanup")} then run this again`);
714
+ log(` or: replace it, backing .env up ${dim("npx @decidio/sdk init <name> --force")}`);
715
+ return { kind: "stop", code: 1 };
716
+ }
717
+ const def = defaultAgentId();
718
+ 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(" ")}")`);
719
+ log(` and the key .env is written under. Any name works; Enter takes the folder's.`);
720
+ const typed = (await ask(` agent id [${def}]: `)).trim();
721
+ const agentId = typed || def;
722
+ log(` → running: npx @decidio/sdk init ${agentId} ${dim("(next time, skip the questions: run that directly)")}`);
723
+ log("");
724
+ return { kind: "init", agentId };
725
+ }
726
+ const forkApplies = () => !!(process.stdin.isTTY && process.stdout.isTTY) && !process.env.DECIDIO_SESSION_TOKEN && !loadSession();
617
727
  async function cmdLogin(emailArg) {
618
728
  try {
619
729
  assertSafeTransport(API, true);
@@ -653,9 +763,8 @@ async function cmdInit(agentId, opts = {}) {
653
763
  err(`✘ this directory's .env already holds the key for a DIFFERENT agent ("${existingEnv.DECIDIO_AGENT_ID}").`);
654
764
  err(` Registering "${agentId}" here would overwrite that key — and a private key that is`);
655
765
  err(` overwritten is gone for good (it never left this machine). Nothing was written.`);
656
- err(` Pick one:`);
657
- err(` 1. New directory (cleanest): mkdir ${agentId} && cd ${agentId} && npx @decidio/sdk init ${agentId}`);
658
- err(` 2. Replace the old agent here, backing its .env up first: npx @decidio/sdk init ${agentId} --force`);
766
+ err(` Either: a new directory (cleanest): mkdir ${agentId} && cd ${agentId} && npx @decidio/sdk init ${agentId}`);
767
+ err(` or: replace the old agent here, backing its .env up first: npx @decidio/sdk init ${agentId} --force`);
659
768
  return 1;
660
769
  }
661
770
  const { did: localDid, privateKeyBase64: localKey } = generateKeypair();
@@ -994,7 +1103,10 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
994
1103
  return 0;
995
1104
  }
996
1105
  export function receiptFileName(id) {
997
- return `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, (c) => "~" + c.codePointAt(0).toString(16).padStart(2, "0"))}.json`;
1106
+ const base = `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, (c) => "~" + c.codePointAt(0).toString(16).padStart(2, "0"))}.json`;
1107
+ if (DEMO_ORIGIN && DEMO_STAMP && existsSync(joinPath(DEMO_ORIGIN, base)))
1108
+ return base.replace(/\.json$/, `.${DEMO_STAMP}.json`);
1109
+ return base;
998
1110
  }
999
1111
  function writeReceiptFile(name, bytes, say = log) {
1000
1112
  if (existsSync(name)) {
@@ -1155,17 +1267,28 @@ function valuedOpt(args, flag) {
1155
1267
  const eq = args.find((x) => x.startsWith(flag + "="));
1156
1268
  return eq ? eq.slice(flag.length + 1) : undefined;
1157
1269
  }
1270
+ let DEMO_ORIGIN = null;
1271
+ let DEMO_STAMP = null;
1158
1272
  async function cmdQuickstart(opts = {}) {
1159
1273
  if (!opts.demo)
1160
1274
  return cmdQuickstartInner(opts);
1275
+ if (opts.keepAgent) {
1276
+ 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).`);
1277
+ err(` Drop --keep-agent, or run the hosted tour (npx @decidio/sdk quickstart) where --keep-agent keeps a real agent + token.`);
1278
+ return 2;
1279
+ }
1161
1280
  const relayBase = SHELL_ENV.DECIDIO_DEMO_RELAY_URL === undefined ? "https://decidio-api.onrender.com" : (SHELL_ENV.DECIDIO_DEMO_RELAY_URL || null);
1162
1281
  const gate = await startDemoGate({ relayBase });
1163
1282
  enterDemo(gate.url);
1164
1283
  const origin = process.cwd();
1165
1284
  const work = mkdtempSync(joinPath(tmpdir(), "decidio-demo-"));
1285
+ DEMO_ORIGIN = origin;
1286
+ DEMO_STAMP = String(Date.now());
1166
1287
  process.chdir(work);
1288
+ let code = 1;
1289
+ let copyFailed = false;
1167
1290
  try {
1168
- return await cmdQuickstartInner(opts);
1291
+ code = await cmdQuickstartInner(opts);
1169
1292
  }
1170
1293
  finally {
1171
1294
  process.chdir(origin);
@@ -1180,12 +1303,17 @@ async function cmdQuickstart(opts = {}) {
1180
1303
  log(` receipt kept here: ${target.slice(origin.length + 1)} ${dim("(the demo's agent, token and files lived in a temporary folder, now removed)")}`);
1181
1304
  }
1182
1305
  catch (e) {
1183
- err(` ⚠ could not copy the receipt into this folder (${e?.message ?? e}) — it is at ${joinPath(work, f)}`);
1306
+ copyFailed = true;
1307
+ 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)}`);
1184
1308
  }
1185
1309
  }
1186
- rmSync(work, { recursive: true, force: true });
1310
+ if (!copyFailed)
1311
+ rmSync(work, { recursive: true, force: true });
1187
1312
  await gate.close();
1313
+ DEMO_ORIGIN = null;
1314
+ DEMO_STAMP = null;
1188
1315
  }
1316
+ return copyFailed ? (code || 1) : code;
1189
1317
  }
1190
1318
  async function cmdQuickstartInner(opts = {}) {
1191
1319
  const t0 = Date.now();
@@ -1194,9 +1322,9 @@ async function cmdQuickstartInner(opts = {}) {
1194
1322
  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).`);
1195
1323
  return 2;
1196
1324
  }
1197
- log(bold("Decidio quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
1325
+ log(brand() + bold(" quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
1198
1326
  if (DEMO)
1199
- log(` Target: LOCAL DEMO — an in-process stand-in for the Decidio API on this machine (${API}); nothing hosted, nothing kept`);
1327
+ 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`);
1200
1328
  else
1201
1329
  log(` Target: ${API}${process.env.DECIDIO_API_URL ? "" : dim(" (the hosted synthetic sandbox — set DECIDIO_API_URL to use your own)")}`);
1202
1330
  hostAnnounced = true;
@@ -1458,26 +1586,52 @@ async function cmdQuickstartInner(opts = {}) {
1458
1586
  verdict = "again";
1459
1587
  if (verdict === "again" && !(earlyError instanceof DecidioTimeoutError)) {
1460
1588
  log(`③ No decision was made (no y or n answer), so the tour stops here rather than guess.`);
1461
- log(` The request is still ROUTED and waiting in your queue - approve or reject it there, or re-run the tour.`);
1462
- log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's key). Once you`);
1463
- log(` have decided - or if you abandon the tour - remove them: npx @decidio/sdk quickstart cleanup`);
1589
+ if (DEMO) {
1590
+ log(` Nothing executed and no receipt was written. Nothing is kept: the demo's agent, token and folder go now.`);
1591
+ log(` Re-run and answer y or n (or use the web page / QR).`);
1592
+ }
1593
+ else {
1594
+ log(` The request is still ROUTED and waiting in your queue - approve or reject it there, or re-run the tour.`);
1595
+ log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's key). Once you`);
1596
+ log(` have decided - or if you abandon the tour - remove them: npx @decidio/sdk quickstart cleanup`);
1597
+ }
1464
1598
  log("");
1465
1599
  sessionReminder();
1466
- process.exit(3);
1600
+ currentRl?.close();
1601
+ stdinReadAbandoned = true;
1602
+ return 3;
1467
1603
  }
1468
1604
  if (earlyError instanceof DecidioTimeoutError) {
1469
1605
  log(`③ the tour's blocking call gave up after 5 minutes while waiting for your answer, so your answer is NOT posted:`);
1470
- log(` the request is still ROUTED in your queue - decide it there, or re-run the tour and answer within 5 minutes.`);
1471
- log(` NOTE: this tour's credentials are still live in ${envFile} - remove them: npx @decidio/sdk quickstart cleanup`);
1606
+ if (DEMO)
1607
+ log(` the demo ends here; nothing is kept (the demo's agent, token and folder go now). Re-run and answer within 5 minutes.`);
1608
+ else {
1609
+ log(` the request is still ROUTED in your queue - decide it there, or re-run the tour and answer within 5 minutes.`);
1610
+ log(` NOTE: this tour's credentials are still live in ${envFile} - remove them: npx @decidio/sdk quickstart cleanup`);
1611
+ }
1472
1612
  log("");
1473
1613
  sessionReminder();
1474
- process.exit(3);
1614
+ currentRl?.close();
1615
+ stdinReadAbandoned = true;
1616
+ return 3;
1475
1617
  }
1476
1618
  }
1477
1619
  else if (opts.autoApproveDemo) {
1478
- log("③ --auto-approve-demo: approving with your session credential. This is AUTOMATED authority");
1620
+ log(`③ --auto-approve-demo: approving with ${DEMO ? "the demo's mock credential (no account is involved)" : "your session credential"}. This is AUTOMATED authority`);
1479
1621
  log(" (a flag you passed, not a human decision in the moment) — fine for CI, not the real story.");
1480
1622
  }
1623
+ else if (DEMO) {
1624
+ log("③ This terminal can't put a human in the loop, and an approval IS a human act — the demo stops here");
1625
+ log(" rather than approve its own request. Nothing executed, no receipt was written, and nothing is kept:");
1626
+ log(" the demo's agent, token and working folder go now (the stand-in API lives only in this process).");
1627
+ log("");
1628
+ log(" Re-run in an interactive terminal and answer y or n there, on the web page or by QR — or, for CI:");
1629
+ log(" npx @decidio/sdk quickstart --demo --auto-approve-demo (the tour approves itself under that explicit label)");
1630
+ log("");
1631
+ log(" (exit 3 = the demo stopped at the human moment; deliberately not reported as a completed tour.)");
1632
+ stdinReadAbandoned = true;
1633
+ return 3;
1634
+ }
1481
1635
  else {
1482
1636
  log("③ This terminal can't put a human in the loop, and an approval IS a human act — the tour");
1483
1637
  log(" stops here rather than approve its own request. The decision is routed and waiting —");
@@ -1499,7 +1653,8 @@ async function cmdQuickstartInner(opts = {}) {
1499
1653
  log(` or if you abandon the tour — remove them: npx @decidio/sdk quickstart cleanup`);
1500
1654
  log("");
1501
1655
  sessionReminder();
1502
- process.exit(3);
1656
+ stdinReadAbandoned = true;
1657
+ return 3;
1503
1658
  }
1504
1659
  const rejected = verdict === "reject";
1505
1660
  if (rejected) {
@@ -1644,7 +1799,7 @@ async function cmdQuickstartInner(opts = {}) {
1644
1799
  const secs = Math.round((Date.now() - t0) / 1000);
1645
1800
  block("✔", `Done in ${secs}s`, [
1646
1801
  `${bold("gated")} payInvoice $86,000 — ROUTED to a human as ${decisionId}`,
1647
- `${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"}`,
1802
+ `${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"}`,
1648
1803
  `${bold("executed")} your function ran once, AFTER approval`,
1649
1804
  `${bold("receipt")} VALID — verified offline, pinned to ${DEMO ? "this demo's own key" : "your workspace issuer"} (④ above)`,
1650
1805
  dim("gated → routed to a human → approved → executed once → a receipt you can verify offline"),
@@ -1968,7 +2123,7 @@ function cmdDev(target, port) {
1968
2123
  }).listen(port, "127.0.0.1");
1969
2124
  return 0;
1970
2125
  }
1971
- const USAGE = `decidio — the Decidio SDK command line
2126
+ const USAGE = `✦ Decidio — the SDK command line
1972
2127
 
1973
2128
  The wrapper IS the SDK: guard.protect(fn, describe) ships in this package (import { guard } from "@decidio/sdk").
1974
2129
  This command line is the tour, the sign-in, and the token admin around it.
@@ -1977,6 +2132,10 @@ const USAGE = `decidio — the Decidio SDK command line
1977
2132
 
1978
2133
  npx @decidio/sdk quickstart
1979
2134
 
2135
+ With no sign-in on this computer it first asks ONE question - do you have an invite? - and, if you do,
2136
+ what next: the tour, or wiring up your own agent. Every answer shows the command it stands for. The bare
2137
+ npx @decidio/sdk on a terminal asks the same (skipping the invite question when you are signed in).
2138
+
1980
2139
  It gates a demo action, routes it to YOU for approval, executes on your yes, and hands you a
1981
2140
  cryptographically verifiable receipt of the DECISION (execution evidence then accrues on the
1982
2141
  hosted record — the receipt seals who authorized what, not what ran afterward). Guided and
@@ -1995,16 +2154,18 @@ THE TOUR
1995
2154
  npx @decidio/sdk quickstart --auto-approve-demo headless tour; cleans up after itself
1996
2155
  npx @decidio/sdk quickstart --demo the same tour against a LOCAL stand-in of the API — no account needed; the
1997
2156
  web/phone door is a hosted demo page; the receipt is the one thing kept
1998
- (add --keep-agent to KEEP the tour agent + token)
2157
+ (--keep-agent is refused here: nothing of the demo's can be kept)
1999
2158
 
2000
2159
  SIGN IN (once)
2001
- npx @decidio/sdk login [email] sign in ONCE: the CLI emails you a link, you paste it; the sign-in
2160
+ npx @decidio/sdk login [email] sign in ONCE: the CLI emails you an 8-character code, you type it; the sign-in
2002
2161
  is kept on this computer 30 days, or until this terminal closes, logout, or cleanup
2003
2162
  npx @decidio/sdk logout forget the stored sign-in on this computer (the token itself stays valid
2004
2163
  until it expires — there is no server-side revocation yet)
2005
2164
 
2006
2165
  YOUR AGENT AND ITS TOKENS
2007
2166
  npx @decidio/sdk init [agentId] sign in, register the agent, mint its API token, write .env
2167
+ (agentId defaults to this folder's name; any name works - it is how the
2168
+ workspace shows the request and the key .env is written under)
2008
2169
  --unique append a random suffix (collision-proof samples)
2009
2170
  --force replace a different agent already in this .env
2010
2171
  (its .env is backed up first)
@@ -2037,7 +2198,8 @@ NOTES
2037
2198
  Every NETWORK command talks to the hosted sandbox (https://decidio-api.onrender.com) unless
2038
2199
  DECIDIO_API_URL says otherwise; the target host is announced on stderr each run. "verify" is
2039
2200
  fully offline — it touches no network and reads no .env.
2040
- Update check: network commands ask the public npm registry once a day whether a newer version exists. If so,
2201
+ Update check: network commands (and the bare command on a terminal) ask the public npm registry once a day
2202
+ whether a newer version exists. If so,
2041
2203
  an interactive run asks "Upgrade now? [y/n]" first (y upgrades the way you installed - global, or in the
2042
2204
  project for a local install; under npx it prints the @latest form - and re-runs your command; n exits); a headless
2043
2205
  run prints the notice and continues (DECIDIO_REQUIRE_LATEST=1 makes it exit). DECIDIO_NO_UPDATE_CHECK=1 skips
@@ -2092,7 +2254,8 @@ async function latestPublished() {
2092
2254
  }
2093
2255
  }
2094
2256
  async function updateGate(cmd, argv) {
2095
- if (!cmd || !GATED_CMDS.has(cmd) || SHELL_ENV.DECIDIO_NO_UPDATE_CHECK || SHELL_ENV.DECIDIO_UPDATE_REEXEC || VERSION === "unknown" || VERSION.includes("-"))
2257
+ const gatedHere = cmd ? GATED_CMDS.has(cmd) : !!(process.stdin.isTTY && process.stdout.isTTY);
2258
+ if (!gatedHere || SHELL_ENV.DECIDIO_NO_UPDATE_CHECK || SHELL_ENV.DECIDIO_UPDATE_REEXEC || VERSION === "unknown" || VERSION.includes("-"))
2096
2259
  return null;
2097
2260
  const latest = await latestPublished();
2098
2261
  if (!latest || !semverNewer(latest, VERSION))
@@ -2108,7 +2271,9 @@ async function updateGate(cmd, argv) {
2108
2271
  const cwd = norm(process.cwd());
2109
2272
  return cwd === norm(root) || cwd.startsWith(norm(root) + pathSep) ? root : null;
2110
2273
  })();
2111
- const upgradeCmd = underNpx ? `npx @decidio/sdk@latest ${argv.slice(2).join(" ")}` : localRoot ? "npm i @decidio/sdk@latest" : "npm i -g @decidio/sdk@latest";
2274
+ const viaAlias = /[\\/]decidio$/.test(argv[1] ?? "");
2275
+ const pkgName = viaAlias ? "decidio" : "@decidio/sdk";
2276
+ const upgradeCmd = underNpx ? `npx ${pkgName}@latest ${argv.slice(2).join(" ")}` : localRoot ? `npm i ${pkgName}@latest` : `npm i -g ${pkgName}@latest`;
2112
2277
  const notice = `${latest} is available (you have ${VERSION}).`;
2113
2278
  if (!(process.stdin.isTTY && process.stdout.isTTY)) {
2114
2279
  err(`⚠ ${notice} Upgrade: ${upgradeCmd}`);
@@ -2132,7 +2297,7 @@ async function updateGate(cmd, argv) {
2132
2297
  return null;
2133
2298
  }
2134
2299
  log(` upgrading: ${upgradeCmd}${localRoot ? ` (in ${localRoot})` : ""}`);
2135
- const up = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", localRoot ? ["i", "@decidio/sdk@latest"] : ["i", "-g", "@decidio/sdk@latest"], { stdio: "inherit", shell: process.platform === "win32", env: SHELL_ENV, ...(localRoot ? { cwd: localRoot } : {}) });
2300
+ const up = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", localRoot ? ["i", `${pkgName}@latest`] : ["i", "-g", `${pkgName}@latest`], { stdio: "inherit", shell: process.platform === "win32", env: SHELL_ENV, ...(localRoot ? { cwd: localRoot } : {}) });
2136
2301
  if (up.status !== 0) {
2137
2302
  err(`✘ the upgrade did not complete (exit ${up.status ?? "?"}) — run it by hand: ${upgradeCmd}`);
2138
2303
  return 1;
@@ -2147,6 +2312,21 @@ async function main(argv) {
2147
2312
  log(VERSION);
2148
2313
  return 0;
2149
2314
  }
2315
+ if (!cmd && process.stdin.isTTY && process.stdout.isTTY) {
2316
+ const gatedBare = await updateGate(cmd, argv);
2317
+ if (gatedBare !== null)
2318
+ return gatedBare;
2319
+ log(brand() + " — gate an action, route it to a human, execute on approval, hold the proof. " + dim("(npx @decidio/sdk --help for every command)"));
2320
+ const stored = process.env.DECIDIO_SESSION_TOKEN ? "" : loadSession()?.email ?? null;
2321
+ const pick = await firstRunFork(process.env.DECIDIO_SESSION_TOKEN ? "" : stored);
2322
+ if (pick.kind === "stop")
2323
+ return pick.code;
2324
+ if (pick.kind === "logout")
2325
+ return cmdLogout();
2326
+ if (pick.kind === "init")
2327
+ return cmdInit(pick.agentId, {});
2328
+ return cmdQuickstart({ demo: pick.kind === "demo" });
2329
+ }
2150
2330
  if (!cmd || wantsHelp(cmd)) {
2151
2331
  log(USAGE);
2152
2332
  return 0;
@@ -2257,7 +2437,20 @@ async function main(argv) {
2257
2437
  return 1;
2258
2438
  return quickstartCleanup(session, envFile);
2259
2439
  }
2260
- return cmdQuickstart({ demo: argsIn.includes("--demo"), autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent"), explain: argsIn.includes("--explain"), phone: valuedOpt(argsIn, "--phone") });
2440
+ 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") };
2441
+ if (!qopts.demo && !qopts.autoApproveDemo && forkApplies()) {
2442
+ log(brand() + bold(" quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
2443
+ const pick = await firstRunFork();
2444
+ if (pick.kind === "stop")
2445
+ return pick.code;
2446
+ if (pick.kind === "logout")
2447
+ return cmdLogout();
2448
+ if (pick.kind === "init")
2449
+ return cmdInit(pick.agentId, {});
2450
+ if (pick.kind === "demo")
2451
+ qopts.demo = true;
2452
+ }
2453
+ return cmdQuickstart(qopts);
2261
2454
  }
2262
2455
  case "init": {
2263
2456
  const unique = argsIn.includes("--unique");
@@ -2270,7 +2463,7 @@ async function main(argv) {
2270
2463
  }
2271
2464
  const ddIdx = argsIn.indexOf("--");
2272
2465
  const nameArg = ddIdx >= 0 ? argsIn[ddIdx + 1] : argsIn.find((x, i) => !x.startsWith("-") && !(expIdx >= 0 && i === expIdx + 1));
2273
- let name = nameArg ?? process.env.DECIDIO_AGENT_ID ?? "my-agent";
2466
+ let name = nameArg ?? (process.env.DECIDIO_AGENT_ID || defaultAgentId());
2274
2467
  if (unique)
2275
2468
  name = `${name}-${randomBytes(2).toString("hex")}`;
2276
2469
  return cmdInit(name, { force: argsIn.includes("--force"), expiresDays: expRaw ? Number(expRaw) : undefined });
@@ -2356,6 +2549,7 @@ const invokedAsProgram = (() => {
2356
2549
  })();
2357
2550
  if (invokedAsProgram)
2358
2551
  main(process.argv).then(async (code) => {
2552
+ closeSharedRl();
2359
2553
  if (code !== 0)
2360
2554
  process.exitCode = code;
2361
2555
  if (stdinReadAbandoned) {
package/dist/demo-gate.js CHANGED
@@ -35,6 +35,8 @@ export async function startDemoGate(opts = {}) {
35
35
  const issuerDid = "did:key:z" + b58(Buffer.concat([Buffer.from([0xed, 0x01]), issuerRaw]));
36
36
  const sealedAt = new Date().toISOString();
37
37
  let verdict = null;
38
+ let decidedBy = "the human at the terminal";
39
+ let lastReason = "quickstart";
38
40
  let relayCode = null;
39
41
  let relayOk = null;
40
42
  const relayFetch = async (path, init = {}) => {
@@ -58,7 +60,7 @@ export async function startDemoGate(opts = {}) {
58
60
  }
59
61
  };
60
62
  const issueReceipt = (kind) => {
61
- const canonical = `{"accountableParty":"[PERSON_1]","action":"payInvoice","amount":"[AMOUNT_2]","recordKind":"${kind}","recordSeries":"local-demo"}`;
63
+ const canonical = jcsCanonicalize({ accountableParty: "[PERSON_1]", action: "payInvoice", amount: 86000, currency: "USD", decidedBy, reason: lastReason, recordKind: kind, recordSeries: "local-demo" });
62
64
  const contentHash = createHash("sha256").update(canonical, "utf8").digest("hex");
63
65
  const context = ["https://www.w3.org/ns/credentials/v2"];
64
66
  const doc = {
@@ -113,14 +115,21 @@ export async function startDemoGate(opts = {}) {
113
115
  if (path.startsWith("/agent/status/")) {
114
116
  if (!verdict && relayCode) {
115
117
  const r = await relayFetch(`/demo/relay/${relayCode}`);
116
- if (r && (r.verdict === "approve" || r.verdict === "reject"))
118
+ if (r && (r.verdict === "approve" || r.verdict === "reject")) {
117
119
  verdict = r.verdict;
120
+ decidedBy = "the demo page (web or phone), through the relay";
121
+ lastReason = "decided on the demo page";
122
+ }
118
123
  }
119
124
  return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver" } : { state: verdict === "approve" ? "approved" : "pending" });
120
125
  }
121
126
  if (/^\/api\/decisions\/[^/]+\/(approve|reject)$/.test(path)) {
122
127
  const already = verdict !== null;
123
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
+ }
124
133
  if (relayCode && !already)
125
134
  void relayFetch(`/demo/relay/${relayCode}`, { method: "PUT", body: JSON.stringify({ verdict }) });
126
135
  return send({ ok: true, decisionId: DECISION_ID, sealed: true, ...(already ? { alreadyResolved: true } : {}) });
@@ -156,6 +165,6 @@ export async function startDemoGate(opts = {}) {
156
165
  url: `http://127.0.0.1:${port}`,
157
166
  issuerDid,
158
167
  relayReachable: () => relayOk,
159
- close: () => new Promise((r) => server.close(() => r())),
168
+ close: () => new Promise((r) => { server.closeAllConnections(); server.close(() => r()); }),
160
169
  };
161
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.6.0 # tracks the SDK release - checked by packages/conformance/manifests.mjs
8
+ version: 0.6.2 # 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.6.0",
3
+ "version": "0.6.2",
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.6.0"
87
+ "@decidio/verify": "0.6.2"
88
88
  },
89
89
  "author": "OmniTwin Technologies Inc.",
90
90
  "bugs": {