@decidio/sdk 0.6.1 → 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,31 @@ 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
+
7
32
  ## 0.6.1 — the first-run fork, and the local demo's loose ends
8
33
 
9
34
  Two independent first-run reviews of 0.6.0 (grades B / A− overall) agreed on one thing: the hosted tour and the
package/README.md CHANGED
@@ -9,6 +9,7 @@ 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
14
15
  # Not sure? The bare command asks: with no sign-in on this computer it asks whether you have an
@@ -74,8 +75,8 @@ npx @decidio/sdk init --unique my-agent # --unique appends a random suffix: co
74
75
  Every network command talks to the hosted sandbox by default (`verify` is offline) and announces its target host on
75
76
  stderr each run — set `DECIDIO_API_URL` (env or `.env`) to point at another Decidio instead.
76
77
 
77
- `init` signs you into your workspace (email + password, or paste the sign-in link from your
78
- 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`
79
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`.
80
81
  Every later command that talks to Decidio reads `.env` from this directory automatically (`verify` is
81
82
  offline and never does). Re-running init for an
@@ -91,7 +92,7 @@ agent name — if it's already taken in your workspace, `init` stops cleanly and
91
92
  fresh one (or pass `--unique` to auto-append a random suffix — collision-proof for shared
92
93
  workspaces and CI). Interactive terminals sign in once (`npx @decidio/sdk login` — stored owner-only in your
93
94
  profile, bound to this terminal window, 30 days). Headless terminal (no prompts possible)? Export `DECIDIO_SESSION_TOKEN` first:
94
- 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
95
96
  response's `token` is your session token (the CLI prints these exact instructions when it
96
97
  can't prompt). That session token is your workspace login, not the agent's credential: unset it
97
98
  before launching agent code from the same shell, because child processes inherit exported
package/dist/cli.js CHANGED
@@ -467,24 +467,42 @@ export function approvalAnswer(answer) {
467
467
  return "reject";
468
468
  return "again";
469
469
  }
470
+ let sharedRl = null;
471
+ let sharedRlOpen = false;
472
+ function closeSharedRl() { if (sharedRlOpen) {
473
+ sharedRlOpen = false;
474
+ sharedRl?.close();
475
+ } }
470
476
  function ask(question, mute = false) {
471
477
  if (process.stdin.readableEnded || process.stdin.destroyed)
472
478
  return Promise.resolve("");
473
479
  return new Promise((resolve) => {
474
- 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;
475
486
  currentRl = rl;
487
+ let restore = null;
476
488
  if (mute) {
477
489
  const anyRl = rl;
478
490
  const write = anyRl._writeToOutput?.bind(rl);
479
491
  anyRl._writeToOutput = (s) => { if (s.includes(question))
480
492
  write?.(s); };
481
- }
482
- rl.once("close", () => { if (currentRl === rl)
483
- currentRl = null; resolve(""); });
484
- rl.question(question, (answer) => { resolve(answer.trim()); rl.close(); if (mute)
485
- 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()); });
486
501
  });
487
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
+ }
488
506
  function terminalId() {
489
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"]) {
490
508
  const v = process.env[k];
@@ -567,29 +585,37 @@ async function loginFlow(purpose, emailArg) {
567
585
  }
568
586
  log(` ✘ password sign-in failed (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}. No problem — the emailed link works for every account:`);
569
587
  }
570
- 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 }) }, "");
571
589
  if (!(req.status >= 200 && req.status < 300)) {
572
590
  log(` ✘ could not request a sign-in email (${req.status})${req.body?.error ? ` — ${req.body.error}` : ""}. Check the address and re-run.`);
573
591
  return null;
574
592
  }
575
- log(` ✔ sign-in email requested for ${email} — check your inbox and spam; paste the link (or its login= code; single-use, 15 minutes)`);
576
- 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`));
577
595
  log(dim(` or see the whole story on this machine right now, no account: npx @decidio/sdk quickstart --demo`));
578
- const pasted = await ask(" link or code: ");
579
- if (!pasted) {
580
- log(" ✘ nothing pasted — re-run when you have the email.");
581
- return null;
582
- }
583
- const m = pasted.match(/[?&]login=([^&\s]+)/);
584
- const linkToken = m ? decodeURIComponent(m[1]) : pasted;
585
- const r = await api("/demo/login-consume", { method: "POST", body: JSON.stringify({ token: linkToken }) }, "");
586
- if (r.status >= 200 && r.status < 300 && r.body?.token) {
587
- const s = saveSession(String(r.body.token), email);
588
- 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)")}`);
589
- 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)`);
590
617
  }
591
- log(` ✘ that link didn't work (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}.`);
592
- 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.`);
593
619
  return null;
594
620
  }
595
621
  async function acquireSession(purpose) {
@@ -659,12 +685,13 @@ async function firstRunFork(signedInAs = null) {
659
685
  return { kind: "stop", code: 1 };
660
686
  }
661
687
  log("");
662
- log(` What next? ${dim("[1/2]")}`);
688
+ log(` What next? ${dim("[1/2/3]")}`);
663
689
  log(` ${bold("1")} the two-minute tour against your workspace ${dim("npx @decidio/sdk quickstart")}`);
664
690
  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]$/);
691
+ log(` ${bold("3")} sign out of this computer ${dim("npx @decidio/sdk logout")}`);
692
+ const b = await pick("1, 2 or 3", /^[123]$/);
666
693
  if (!b) {
667
- log(` No answer — you are signed in; run either command above when you are ready.`);
694
+ log(` No answer — you are signed in; run any command above when you are ready.`);
668
695
  return { kind: "stop", code: 0 };
669
696
  }
670
697
  if (b === "1") {
@@ -672,6 +699,21 @@ async function firstRunFork(signedInAs = null) {
672
699
  log("");
673
700
  return { kind: "tour" };
674
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
+ }
675
717
  const def = defaultAgentId();
676
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(" ")}")`);
677
719
  log(` and the key .env is written under. Any name works; Enter takes the folder's.`);
@@ -721,9 +763,8 @@ async function cmdInit(agentId, opts = {}) {
721
763
  err(`✘ this directory's .env already holds the key for a DIFFERENT agent ("${existingEnv.DECIDIO_AGENT_ID}").`);
722
764
  err(` Registering "${agentId}" here would overwrite that key — and a private key that is`);
723
765
  err(` overwritten is gone for good (it never left this machine). Nothing was written.`);
724
- err(` Pick one:`);
725
- err(` 1. New directory (cleanest): mkdir ${agentId} && cd ${agentId} && npx @decidio/sdk init ${agentId}`);
726
- 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`);
727
768
  return 1;
728
769
  }
729
770
  const { did: localDid, privateKeyBase64: localKey } = generateKeypair();
@@ -2116,7 +2157,7 @@ THE TOUR
2116
2157
  (--keep-agent is refused here: nothing of the demo's can be kept)
2117
2158
 
2118
2159
  SIGN IN (once)
2119
- 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
2120
2161
  is kept on this computer 30 days, or until this terminal closes, logout, or cleanup
2121
2162
  npx @decidio/sdk logout forget the stored sign-in on this computer (the token itself stays valid
2122
2163
  until it expires — there is no server-side revocation yet)
@@ -2157,7 +2198,8 @@ NOTES
2157
2198
  Every NETWORK command talks to the hosted sandbox (https://decidio-api.onrender.com) unless
2158
2199
  DECIDIO_API_URL says otherwise; the target host is announced on stderr each run. "verify" is
2159
2200
  fully offline — it touches no network and reads no .env.
2160
- 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,
2161
2203
  an interactive run asks "Upgrade now? [y/n]" first (y upgrades the way you installed - global, or in the
2162
2204
  project for a local install; under npx it prints the @latest form - and re-runs your command; n exits); a headless
2163
2205
  run prints the notice and continues (DECIDIO_REQUIRE_LATEST=1 makes it exit). DECIDIO_NO_UPDATE_CHECK=1 skips
@@ -2212,7 +2254,8 @@ async function latestPublished() {
2212
2254
  }
2213
2255
  }
2214
2256
  async function updateGate(cmd, argv) {
2215
- 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("-"))
2216
2259
  return null;
2217
2260
  const latest = await latestPublished();
2218
2261
  if (!latest || !semverNewer(latest, VERSION))
@@ -2228,7 +2271,9 @@ async function updateGate(cmd, argv) {
2228
2271
  const cwd = norm(process.cwd());
2229
2272
  return cwd === norm(root) || cwd.startsWith(norm(root) + pathSep) ? root : null;
2230
2273
  })();
2231
- 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`;
2232
2277
  const notice = `${latest} is available (you have ${VERSION}).`;
2233
2278
  if (!(process.stdin.isTTY && process.stdout.isTTY)) {
2234
2279
  err(`⚠ ${notice} Upgrade: ${upgradeCmd}`);
@@ -2252,7 +2297,7 @@ async function updateGate(cmd, argv) {
2252
2297
  return null;
2253
2298
  }
2254
2299
  log(` upgrading: ${upgradeCmd}${localRoot ? ` (in ${localRoot})` : ""}`);
2255
- 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 } : {}) });
2256
2301
  if (up.status !== 0) {
2257
2302
  err(`✘ the upgrade did not complete (exit ${up.status ?? "?"}) — run it by hand: ${upgradeCmd}`);
2258
2303
  return 1;
@@ -2268,11 +2313,16 @@ async function main(argv) {
2268
2313
  return 0;
2269
2314
  }
2270
2315
  if (!cmd && process.stdin.isTTY && process.stdout.isTTY) {
2316
+ const gatedBare = await updateGate(cmd, argv);
2317
+ if (gatedBare !== null)
2318
+ return gatedBare;
2271
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)"));
2272
2320
  const stored = process.env.DECIDIO_SESSION_TOKEN ? "" : loadSession()?.email ?? null;
2273
2321
  const pick = await firstRunFork(process.env.DECIDIO_SESSION_TOKEN ? "" : stored);
2274
2322
  if (pick.kind === "stop")
2275
2323
  return pick.code;
2324
+ if (pick.kind === "logout")
2325
+ return cmdLogout();
2276
2326
  if (pick.kind === "init")
2277
2327
  return cmdInit(pick.agentId, {});
2278
2328
  return cmdQuickstart({ demo: pick.kind === "demo" });
@@ -2393,6 +2443,8 @@ async function main(argv) {
2393
2443
  const pick = await firstRunFork();
2394
2444
  if (pick.kind === "stop")
2395
2445
  return pick.code;
2446
+ if (pick.kind === "logout")
2447
+ return cmdLogout();
2396
2448
  if (pick.kind === "init")
2397
2449
  return cmdInit(pick.agentId, {});
2398
2450
  if (pick.kind === "demo")
@@ -2497,6 +2549,7 @@ const invokedAsProgram = (() => {
2497
2549
  })();
2498
2550
  if (invokedAsProgram)
2499
2551
  main(process.argv).then(async (code) => {
2552
+ closeSharedRl();
2500
2553
  if (code !== 0)
2501
2554
  process.exitCode = code;
2502
2555
  if (stdinReadAbandoned) {
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.1 # 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.1",
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.1"
87
+ "@decidio/verify": "0.6.2"
88
88
  },
89
89
  "author": "OmniTwin Technologies Inc.",
90
90
  "bugs": {