@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 +61 -0
- package/README.md +10 -6
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +247 -53
- package/dist/demo-gate.js +12 -3
- package/openapi.yaml +1 -1
- package/package.json +2 -2
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
|
|
75
|
-
|
|
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
|
-
#
|
|
141
|
-
# command lists every
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
575
|
-
log(dim(` invite-only lab:
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
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(`
|
|
657
|
-
err(`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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})
|
|
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
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
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
|
-
|
|
1614
|
+
currentRl?.close();
|
|
1615
|
+
stdinReadAbandoned = true;
|
|
1616
|
+
return 3;
|
|
1475
1617
|
}
|
|
1476
1618
|
}
|
|
1477
1619
|
else if (opts.autoApproveDemo) {
|
|
1478
|
-
log(
|
|
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
|
-
|
|
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 ?
|
|
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 =
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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",
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
87
|
+
"@decidio/verify": "0.6.2"
|
|
88
88
|
},
|
|
89
89
|
"author": "OmniTwin Technologies Inc.",
|
|
90
90
|
"bugs": {
|