@decidio/sdk 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +6 -3
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +163 -22
- package/dist/demo-gate.js +12 -3
- package/openapi.yaml +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ Release notes for the client SDK. The Python package `decidio` ships the same ch
|
|
|
4
4
|
version (the two are conformance-locked twins). Each entry names what changed, what a reader must do
|
|
5
5
|
on upgrade, and what proved it.
|
|
6
6
|
|
|
7
|
+
## 0.6.1 — the first-run fork, and the local demo's loose ends
|
|
8
|
+
|
|
9
|
+
Two independent first-run reviews of 0.6.0 (grades B / A− overall) agreed on one thing: the hosted tour and the
|
|
10
|
+
verifier earn trust, and the new local demo had polish debts. This release pays them and adds the one question a
|
|
11
|
+
stranger's first minute was missing (founder ruling 2026-09-06).
|
|
12
|
+
|
|
13
|
+
- **One question, then the thing you picked.** A bare `npx @decidio/sdk quickstart` (or a bare `npx @decidio/sdk`) on
|
|
14
|
+
a terminal with no sign-in asks: do you have a Decidio invite? `n` runs the local demo; `y` signs you in (the emailed
|
|
15
|
+
link) and asks what next: the two-minute tour, or wiring up your own agent (`init`). Every answer prints the exact
|
|
16
|
+
command it stands for, so the questions teach their own flags and you never need them twice. Nothing is stored;
|
|
17
|
+
Enter is not an answer; scripts and CI never see a question (they get today's text, which now also names `--demo`).
|
|
18
|
+
A signed-in `quickstart` runs straight away; the bare `npx @decidio/sdk` for a signed-in user skips the invite
|
|
19
|
+
question and asks only "what next?" - the bare command always leads somewhere, usage is a `--help` away. After
|
|
20
|
+
the sign-in step, the tour does not ask you to sign in again.
|
|
21
|
+
- **Your agent's name has a real default.** `init` with no name used `my-agent`; it now uses this folder's name (or
|
|
22
|
+
the package name), slugified, and the fork explains what the name is for ("Requested by …", the key `.env` is
|
|
23
|
+
written under). Any name still works.
|
|
24
|
+
- **A mark, not a banner:** the title lines carry the app's ✦ beside the name.
|
|
25
|
+
- **Headless `--demo` leaves nothing behind** (review: Medium). The stop at the human moment used to exit inside the
|
|
26
|
+
tour, skipping the wrapper that removes the temporary folder - the mock key, mock token and journal stayed in the
|
|
27
|
+
temp dir, and the message pointed at the hosted tour's remedies (`approvals approve`, `quickstart cleanup`), which
|
|
28
|
+
cannot act on a stand-in that lives only in that process. The stop now returns through the wrapper, and says the
|
|
29
|
+
demo's truth: nothing executed, nothing kept, re-run interactively or with `--auto-approve-demo`.
|
|
30
|
+
- **The demo receipt carries what the terminal claims** (review: Medium). Its canonical record now holds the real
|
|
31
|
+
amount and currency, the reason the decision arrived with (the automated flag names itself there, exactly as it does
|
|
32
|
+
in a hosted record's reason), and how it was decided (terminal, page, or the flag). The party stays tokenized, as
|
|
33
|
+
the real seal keeps it.
|
|
34
|
+
- **`--demo --keep-agent` is refused** (both reviews: Low) - nothing of the demo's can be kept, and the flag used to
|
|
35
|
+
be accepted while the opposite happened. Exit 2, with the reason.
|
|
36
|
+
- **A receipt the demo could not copy home is not a completed tour** (retest: Low): the temporary folder is kept and
|
|
37
|
+
named, and the run exits non-zero, instead of deleting the only copy and exiting 0.
|
|
38
|
+
- **Words that were not true** (review: Low): the demo's Target line no longer says "nothing hosted" while the web
|
|
39
|
+
door uses a hosted relay; the automated line names the demo's mock credential, not "your session credential"; a
|
|
40
|
+
repeat run's ④ frame names the suffixed file that actually lands; the README no longer calls the sandbox "one shared
|
|
41
|
+
lab workspace" in two places while saying the opposite in a third.
|
|
42
|
+
|
|
7
43
|
## 0.6.0 — `quickstart --demo`: the whole tour on your machine, no account
|
|
8
44
|
|
|
9
45
|
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
|
@@ -11,6 +11,9 @@ is the two-minute tour, the sign-in and the token admin around it. Install, wrap
|
|
|
11
11
|
```bash
|
|
12
12
|
npx @decidio/sdk quickstart # against your workspace (you need an invite - request one below)
|
|
13
13
|
npx @decidio/sdk quickstart --demo # no invite yet? the same tour on this machine, against a local stand-in of the API
|
|
14
|
+
# Not sure? The bare command asks: with no sign-in on this computer it asks whether you have an
|
|
15
|
+
# invite (n → the local demo; y → sign in, then the tour or your own agent). Every answer prints
|
|
16
|
+
# the command it stands for, so you never need the questions twice.
|
|
14
17
|
```
|
|
15
18
|
|
|
16
19
|
One command, guided: it registers a fresh agent (key generated locally), gates a demo action
|
|
@@ -137,8 +140,8 @@ the agent's floor-limited token — it cannot approve anything):
|
|
|
137
140
|
|
|
138
141
|
```bash
|
|
139
142
|
npx @decidio/sdk approvals --mine # see it pending (--mine narrows to THIS agent; the
|
|
140
|
-
#
|
|
141
|
-
# command lists every
|
|
143
|
+
# workspace is yours alone unless you invite others,
|
|
144
|
+
# so the bare command lists every pending decision in it)
|
|
142
145
|
npx @decidio/sdk approvals approve <decisionId>
|
|
143
146
|
```
|
|
144
147
|
|
|
@@ -747,7 +750,7 @@ the next reader grades a promise, not a moving target:
|
|
|
747
750
|
|
|
748
751
|
## Using this for real?
|
|
749
752
|
|
|
750
|
-
The hosted sandbox is synthetic
|
|
753
|
+
The hosted sandbox is synthetic data in a workspace of your own. Production workspaces are per-tenant too, with your own
|
|
751
754
|
issuer key and receipts that verify offline against it.
|
|
752
755
|
|
|
753
756
|
[decidioai.com](https://decidioai.com) · [Developer docs](https://decidioai.com/developers/) · [Request access](https://decidioai.com/#early-access)
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
export type ApprovalAnswer = "approve" | "reject" | "again";
|
|
3
3
|
export declare function approvalAnswer(answer: unknown): ApprovalAnswer;
|
|
4
|
+
/** The agent id `init` uses when none is given: this folder's name (or the package name), slugified. "my-agent" told a
|
|
5
|
+
* tester nothing; the folder they are in is the agent they are building (founder ruling 2026-09-06). */
|
|
6
|
+
export declare function defaultAgentId(dir?: string): string;
|
|
4
7
|
/** `decidio-receipt-<id>.json`, with every character outside [A-Za-z0-9_-] escaped as `~` + hex code point, so two
|
|
5
8
|
* distinct decision ids can never share a file (first-run review of 0.4.1: dots were STRIPPED, `a.b` and `ab`
|
|
6
9
|
* collided). `~` is outside the allowed set, so the escape is unambiguous. Exported for the smokes. */
|
package/dist/cli.js
CHANGED
|
@@ -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) {
|
|
@@ -608,12 +609,79 @@ async function acquireSession(purpose) {
|
|
|
608
609
|
if (!process.stdin.isTTY) {
|
|
609
610
|
err(`✘ ${purpose} needs a Decidio workspace sign-in, and this terminal can't prompt for one.`);
|
|
610
611
|
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`);
|
|
612
|
+
err(` or set DECIDIO_SESSION_TOKEN - or, with no account at all: npx @decidio/sdk quickstart --demo. To mint one by hand: request a sign-in link, then`);
|
|
612
613
|
err(` curl -X POST ${API}/demo/login-consume -H "content-type: application/json" -d '{"token":"<login value>"}' # the response's "token"`);
|
|
613
614
|
return null;
|
|
614
615
|
}
|
|
615
616
|
return loginFlow(purpose);
|
|
616
617
|
}
|
|
618
|
+
export function defaultAgentId(dir = process.cwd()) {
|
|
619
|
+
let name = "";
|
|
620
|
+
try {
|
|
621
|
+
const pkg = JSON.parse(readFileSync(joinPath(dir, "package.json"), "utf8"));
|
|
622
|
+
if (typeof pkg?.name === "string")
|
|
623
|
+
name = pkg.name.replace(/^@[^/]+\//, "");
|
|
624
|
+
}
|
|
625
|
+
catch { }
|
|
626
|
+
if (!name)
|
|
627
|
+
name = basename(dir);
|
|
628
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
629
|
+
return slug || "my-agent";
|
|
630
|
+
}
|
|
631
|
+
async function firstRunFork(signedInAs = null) {
|
|
632
|
+
const pick = async (prompt, ok) => {
|
|
633
|
+
let a = "";
|
|
634
|
+
for (let i = 0; i < 3 && !ok.test(a); i++)
|
|
635
|
+
a = (await ask(i === 0 ? " › " : ` Please answer ${prompt}: `)).trim().toLowerCase();
|
|
636
|
+
return ok.test(a) ? a : "";
|
|
637
|
+
};
|
|
638
|
+
log("");
|
|
639
|
+
if (signedInAs !== null) {
|
|
640
|
+
log(` Signed in${signedInAs ? ` as ${signedInAs}` : ""} on this computer.`);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
log(` Do you have a Decidio invite? ${dim("[y/n]")}`);
|
|
644
|
+
log(` ${bold("n")} the local demo on this machine, no account ${dim("npx @decidio/sdk quickstart --demo")}`);
|
|
645
|
+
log(` ${bold("y")} sign in to your workspace ${dim("npx @decidio/sdk login")}`);
|
|
646
|
+
const a = await pick("y or n", /^[yn]$/);
|
|
647
|
+
if (!a) {
|
|
648
|
+
log(` No answer — nothing was done. Run either command above when you are ready.`);
|
|
649
|
+
return { kind: "stop", code: 1 };
|
|
650
|
+
}
|
|
651
|
+
if (a === "n") {
|
|
652
|
+
log(` → running: npx @decidio/sdk quickstart --demo`);
|
|
653
|
+
log("");
|
|
654
|
+
return { kind: "demo" };
|
|
655
|
+
}
|
|
656
|
+
log(` → running: npx @decidio/sdk login`);
|
|
657
|
+
const token = await loginFlow("Signing in");
|
|
658
|
+
if (!token)
|
|
659
|
+
return { kind: "stop", code: 1 };
|
|
660
|
+
}
|
|
661
|
+
log("");
|
|
662
|
+
log(` What next? ${dim("[1/2]")}`);
|
|
663
|
+
log(` ${bold("1")} the two-minute tour against your workspace ${dim("npx @decidio/sdk quickstart")}`);
|
|
664
|
+
log(` ${bold("2")} wire up your own agent now (register, token, .env) ${dim("npx @decidio/sdk init <agentId>")}`);
|
|
665
|
+
const b = await pick("1 or 2", /^[12]$/);
|
|
666
|
+
if (!b) {
|
|
667
|
+
log(` No answer — you are signed in; run either command above when you are ready.`);
|
|
668
|
+
return { kind: "stop", code: 0 };
|
|
669
|
+
}
|
|
670
|
+
if (b === "1") {
|
|
671
|
+
log(` → running: npx @decidio/sdk quickstart ${dim("(signed in - it will not ask again)")}`);
|
|
672
|
+
log("");
|
|
673
|
+
return { kind: "tour" };
|
|
674
|
+
}
|
|
675
|
+
const def = defaultAgentId();
|
|
676
|
+
log(` Your agent needs a name. It is how the workspace shows the request ("Requested by ${def.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}")`);
|
|
677
|
+
log(` and the key .env is written under. Any name works; Enter takes the folder's.`);
|
|
678
|
+
const typed = (await ask(` agent id [${def}]: `)).trim();
|
|
679
|
+
const agentId = typed || def;
|
|
680
|
+
log(` → running: npx @decidio/sdk init ${agentId} ${dim("(next time, skip the questions: run that directly)")}`);
|
|
681
|
+
log("");
|
|
682
|
+
return { kind: "init", agentId };
|
|
683
|
+
}
|
|
684
|
+
const forkApplies = () => !!(process.stdin.isTTY && process.stdout.isTTY) && !process.env.DECIDIO_SESSION_TOKEN && !loadSession();
|
|
617
685
|
async function cmdLogin(emailArg) {
|
|
618
686
|
try {
|
|
619
687
|
assertSafeTransport(API, true);
|
|
@@ -994,7 +1062,10 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
|
994
1062
|
return 0;
|
|
995
1063
|
}
|
|
996
1064
|
export function receiptFileName(id) {
|
|
997
|
-
|
|
1065
|
+
const base = `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, (c) => "~" + c.codePointAt(0).toString(16).padStart(2, "0"))}.json`;
|
|
1066
|
+
if (DEMO_ORIGIN && DEMO_STAMP && existsSync(joinPath(DEMO_ORIGIN, base)))
|
|
1067
|
+
return base.replace(/\.json$/, `.${DEMO_STAMP}.json`);
|
|
1068
|
+
return base;
|
|
998
1069
|
}
|
|
999
1070
|
function writeReceiptFile(name, bytes, say = log) {
|
|
1000
1071
|
if (existsSync(name)) {
|
|
@@ -1155,17 +1226,28 @@ function valuedOpt(args, flag) {
|
|
|
1155
1226
|
const eq = args.find((x) => x.startsWith(flag + "="));
|
|
1156
1227
|
return eq ? eq.slice(flag.length + 1) : undefined;
|
|
1157
1228
|
}
|
|
1229
|
+
let DEMO_ORIGIN = null;
|
|
1230
|
+
let DEMO_STAMP = null;
|
|
1158
1231
|
async function cmdQuickstart(opts = {}) {
|
|
1159
1232
|
if (!opts.demo)
|
|
1160
1233
|
return cmdQuickstartInner(opts);
|
|
1234
|
+
if (opts.keepAgent) {
|
|
1235
|
+
err(`✘ --keep-agent has nothing to keep under --demo: the demo's agent and token live in a temporary folder that is removed at the end (only the receipt stays).`);
|
|
1236
|
+
err(` Drop --keep-agent, or run the hosted tour (npx @decidio/sdk quickstart) where --keep-agent keeps a real agent + token.`);
|
|
1237
|
+
return 2;
|
|
1238
|
+
}
|
|
1161
1239
|
const relayBase = SHELL_ENV.DECIDIO_DEMO_RELAY_URL === undefined ? "https://decidio-api.onrender.com" : (SHELL_ENV.DECIDIO_DEMO_RELAY_URL || null);
|
|
1162
1240
|
const gate = await startDemoGate({ relayBase });
|
|
1163
1241
|
enterDemo(gate.url);
|
|
1164
1242
|
const origin = process.cwd();
|
|
1165
1243
|
const work = mkdtempSync(joinPath(tmpdir(), "decidio-demo-"));
|
|
1244
|
+
DEMO_ORIGIN = origin;
|
|
1245
|
+
DEMO_STAMP = String(Date.now());
|
|
1166
1246
|
process.chdir(work);
|
|
1247
|
+
let code = 1;
|
|
1248
|
+
let copyFailed = false;
|
|
1167
1249
|
try {
|
|
1168
|
-
|
|
1250
|
+
code = await cmdQuickstartInner(opts);
|
|
1169
1251
|
}
|
|
1170
1252
|
finally {
|
|
1171
1253
|
process.chdir(origin);
|
|
@@ -1180,12 +1262,17 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1180
1262
|
log(` receipt kept here: ${target.slice(origin.length + 1)} ${dim("(the demo's agent, token and files lived in a temporary folder, now removed)")}`);
|
|
1181
1263
|
}
|
|
1182
1264
|
catch (e) {
|
|
1183
|
-
|
|
1265
|
+
copyFailed = true;
|
|
1266
|
+
err(` ✘ could not copy the receipt into this folder (${e?.message ?? e}) — the temporary folder is KEPT so the receipt is not lost: ${joinPath(work, f)}`);
|
|
1184
1267
|
}
|
|
1185
1268
|
}
|
|
1186
|
-
|
|
1269
|
+
if (!copyFailed)
|
|
1270
|
+
rmSync(work, { recursive: true, force: true });
|
|
1187
1271
|
await gate.close();
|
|
1272
|
+
DEMO_ORIGIN = null;
|
|
1273
|
+
DEMO_STAMP = null;
|
|
1188
1274
|
}
|
|
1275
|
+
return copyFailed ? (code || 1) : code;
|
|
1189
1276
|
}
|
|
1190
1277
|
async function cmdQuickstartInner(opts = {}) {
|
|
1191
1278
|
const t0 = Date.now();
|
|
@@ -1194,9 +1281,9 @@ async function cmdQuickstartInner(opts = {}) {
|
|
|
1194
1281
|
err(`✘ --phone takes app (default: the QR opens the installed app when there is one) or web (the QR opens the phone web rendering, always).`);
|
|
1195
1282
|
return 2;
|
|
1196
1283
|
}
|
|
1197
|
-
log(bold("
|
|
1284
|
+
log(brand() + bold(" quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
|
|
1198
1285
|
if (DEMO)
|
|
1199
|
-
log(` Target: LOCAL DEMO — an in-process stand-in for the Decidio API on this machine (${API})
|
|
1286
|
+
log(` Target: LOCAL DEMO — an in-process stand-in for the Decidio API on this machine (${API}). Only the web/phone door touches a hosted relay (one word, no account); nothing is kept but the receipt`);
|
|
1200
1287
|
else
|
|
1201
1288
|
log(` Target: ${API}${process.env.DECIDIO_API_URL ? "" : dim(" (the hosted synthetic sandbox — set DECIDIO_API_URL to use your own)")}`);
|
|
1202
1289
|
hostAnnounced = true;
|
|
@@ -1458,26 +1545,52 @@ async function cmdQuickstartInner(opts = {}) {
|
|
|
1458
1545
|
verdict = "again";
|
|
1459
1546
|
if (verdict === "again" && !(earlyError instanceof DecidioTimeoutError)) {
|
|
1460
1547
|
log(`③ No decision was made (no y or n answer), so the tour stops here rather than guess.`);
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1548
|
+
if (DEMO) {
|
|
1549
|
+
log(` Nothing executed and no receipt was written. Nothing is kept: the demo's agent, token and folder go now.`);
|
|
1550
|
+
log(` Re-run and answer y or n (or use the web page / QR).`);
|
|
1551
|
+
}
|
|
1552
|
+
else {
|
|
1553
|
+
log(` The request is still ROUTED and waiting in your queue - approve or reject it there, or re-run the tour.`);
|
|
1554
|
+
log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's key). Once you`);
|
|
1555
|
+
log(` have decided - or if you abandon the tour - remove them: npx @decidio/sdk quickstart cleanup`);
|
|
1556
|
+
}
|
|
1464
1557
|
log("");
|
|
1465
1558
|
sessionReminder();
|
|
1466
|
-
|
|
1559
|
+
currentRl?.close();
|
|
1560
|
+
stdinReadAbandoned = true;
|
|
1561
|
+
return 3;
|
|
1467
1562
|
}
|
|
1468
1563
|
if (earlyError instanceof DecidioTimeoutError) {
|
|
1469
1564
|
log(`③ the tour's blocking call gave up after 5 minutes while waiting for your answer, so your answer is NOT posted:`);
|
|
1470
|
-
|
|
1471
|
-
|
|
1565
|
+
if (DEMO)
|
|
1566
|
+
log(` the demo ends here; nothing is kept (the demo's agent, token and folder go now). Re-run and answer within 5 minutes.`);
|
|
1567
|
+
else {
|
|
1568
|
+
log(` the request is still ROUTED in your queue - decide it there, or re-run the tour and answer within 5 minutes.`);
|
|
1569
|
+
log(` NOTE: this tour's credentials are still live in ${envFile} - remove them: npx @decidio/sdk quickstart cleanup`);
|
|
1570
|
+
}
|
|
1472
1571
|
log("");
|
|
1473
1572
|
sessionReminder();
|
|
1474
|
-
|
|
1573
|
+
currentRl?.close();
|
|
1574
|
+
stdinReadAbandoned = true;
|
|
1575
|
+
return 3;
|
|
1475
1576
|
}
|
|
1476
1577
|
}
|
|
1477
1578
|
else if (opts.autoApproveDemo) {
|
|
1478
|
-
log(
|
|
1579
|
+
log(`③ --auto-approve-demo: approving with ${DEMO ? "the demo's mock credential (no account is involved)" : "your session credential"}. This is AUTOMATED authority`);
|
|
1479
1580
|
log(" (a flag you passed, not a human decision in the moment) — fine for CI, not the real story.");
|
|
1480
1581
|
}
|
|
1582
|
+
else if (DEMO) {
|
|
1583
|
+
log("③ This terminal can't put a human in the loop, and an approval IS a human act — the demo stops here");
|
|
1584
|
+
log(" rather than approve its own request. Nothing executed, no receipt was written, and nothing is kept:");
|
|
1585
|
+
log(" the demo's agent, token and working folder go now (the stand-in API lives only in this process).");
|
|
1586
|
+
log("");
|
|
1587
|
+
log(" Re-run in an interactive terminal and answer y or n there, on the web page or by QR — or, for CI:");
|
|
1588
|
+
log(" npx @decidio/sdk quickstart --demo --auto-approve-demo (the tour approves itself under that explicit label)");
|
|
1589
|
+
log("");
|
|
1590
|
+
log(" (exit 3 = the demo stopped at the human moment; deliberately not reported as a completed tour.)");
|
|
1591
|
+
stdinReadAbandoned = true;
|
|
1592
|
+
return 3;
|
|
1593
|
+
}
|
|
1481
1594
|
else {
|
|
1482
1595
|
log("③ This terminal can't put a human in the loop, and an approval IS a human act — the tour");
|
|
1483
1596
|
log(" stops here rather than approve its own request. The decision is routed and waiting —");
|
|
@@ -1499,7 +1612,8 @@ async function cmdQuickstartInner(opts = {}) {
|
|
|
1499
1612
|
log(` or if you abandon the tour — remove them: npx @decidio/sdk quickstart cleanup`);
|
|
1500
1613
|
log("");
|
|
1501
1614
|
sessionReminder();
|
|
1502
|
-
|
|
1615
|
+
stdinReadAbandoned = true;
|
|
1616
|
+
return 3;
|
|
1503
1617
|
}
|
|
1504
1618
|
const rejected = verdict === "reject";
|
|
1505
1619
|
if (rejected) {
|
|
@@ -1644,7 +1758,7 @@ async function cmdQuickstartInner(opts = {}) {
|
|
|
1644
1758
|
const secs = Math.round((Date.now() - t0) / 1000);
|
|
1645
1759
|
block("✔", `Done in ${secs}s`, [
|
|
1646
1760
|
`${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 ?
|
|
1761
|
+
`${bold("decided")} ${decidedOutside ? "approved in the web app (or on your phone)" : opts.autoApproveDemo ? `approved by --auto-approve-demo (automated — the ${DEMO ? "receipt's reason says so" : "record's reason says so"})` : "approved by you, here"}`,
|
|
1648
1762
|
`${bold("executed")} your function ran once, AFTER approval`,
|
|
1649
1763
|
`${bold("receipt")} VALID — verified offline, pinned to ${DEMO ? "this demo's own key" : "your workspace issuer"} (④ above)`,
|
|
1650
1764
|
dim("gated → routed to a human → approved → executed once → a receipt you can verify offline"),
|
|
@@ -1968,7 +2082,7 @@ function cmdDev(target, port) {
|
|
|
1968
2082
|
}).listen(port, "127.0.0.1");
|
|
1969
2083
|
return 0;
|
|
1970
2084
|
}
|
|
1971
|
-
const USAGE =
|
|
2085
|
+
const USAGE = `✦ Decidio — the SDK command line
|
|
1972
2086
|
|
|
1973
2087
|
The wrapper IS the SDK: guard.protect(fn, describe) ships in this package (import { guard } from "@decidio/sdk").
|
|
1974
2088
|
This command line is the tour, the sign-in, and the token admin around it.
|
|
@@ -1977,6 +2091,10 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
1977
2091
|
|
|
1978
2092
|
npx @decidio/sdk quickstart
|
|
1979
2093
|
|
|
2094
|
+
With no sign-in on this computer it first asks ONE question - do you have an invite? - and, if you do,
|
|
2095
|
+
what next: the tour, or wiring up your own agent. Every answer shows the command it stands for. The bare
|
|
2096
|
+
npx @decidio/sdk on a terminal asks the same (skipping the invite question when you are signed in).
|
|
2097
|
+
|
|
1980
2098
|
It gates a demo action, routes it to YOU for approval, executes on your yes, and hands you a
|
|
1981
2099
|
cryptographically verifiable receipt of the DECISION (execution evidence then accrues on the
|
|
1982
2100
|
hosted record — the receipt seals who authorized what, not what ran afterward). Guided and
|
|
@@ -1995,7 +2113,7 @@ THE TOUR
|
|
|
1995
2113
|
npx @decidio/sdk quickstart --auto-approve-demo headless tour; cleans up after itself
|
|
1996
2114
|
npx @decidio/sdk quickstart --demo the same tour against a LOCAL stand-in of the API — no account needed; the
|
|
1997
2115
|
web/phone door is a hosted demo page; the receipt is the one thing kept
|
|
1998
|
-
(
|
|
2116
|
+
(--keep-agent is refused here: nothing of the demo's can be kept)
|
|
1999
2117
|
|
|
2000
2118
|
SIGN IN (once)
|
|
2001
2119
|
npx @decidio/sdk login [email] sign in ONCE: the CLI emails you a link, you paste it; the sign-in
|
|
@@ -2005,6 +2123,8 @@ SIGN IN (once)
|
|
|
2005
2123
|
|
|
2006
2124
|
YOUR AGENT AND ITS TOKENS
|
|
2007
2125
|
npx @decidio/sdk init [agentId] sign in, register the agent, mint its API token, write .env
|
|
2126
|
+
(agentId defaults to this folder's name; any name works - it is how the
|
|
2127
|
+
workspace shows the request and the key .env is written under)
|
|
2008
2128
|
--unique append a random suffix (collision-proof samples)
|
|
2009
2129
|
--force replace a different agent already in this .env
|
|
2010
2130
|
(its .env is backed up first)
|
|
@@ -2147,6 +2267,16 @@ async function main(argv) {
|
|
|
2147
2267
|
log(VERSION);
|
|
2148
2268
|
return 0;
|
|
2149
2269
|
}
|
|
2270
|
+
if (!cmd && process.stdin.isTTY && process.stdout.isTTY) {
|
|
2271
|
+
log(brand() + " — gate an action, route it to a human, execute on approval, hold the proof. " + dim("(npx @decidio/sdk --help for every command)"));
|
|
2272
|
+
const stored = process.env.DECIDIO_SESSION_TOKEN ? "" : loadSession()?.email ?? null;
|
|
2273
|
+
const pick = await firstRunFork(process.env.DECIDIO_SESSION_TOKEN ? "" : stored);
|
|
2274
|
+
if (pick.kind === "stop")
|
|
2275
|
+
return pick.code;
|
|
2276
|
+
if (pick.kind === "init")
|
|
2277
|
+
return cmdInit(pick.agentId, {});
|
|
2278
|
+
return cmdQuickstart({ demo: pick.kind === "demo" });
|
|
2279
|
+
}
|
|
2150
2280
|
if (!cmd || wantsHelp(cmd)) {
|
|
2151
2281
|
log(USAGE);
|
|
2152
2282
|
return 0;
|
|
@@ -2257,7 +2387,18 @@ async function main(argv) {
|
|
|
2257
2387
|
return 1;
|
|
2258
2388
|
return quickstartCleanup(session, envFile);
|
|
2259
2389
|
}
|
|
2260
|
-
|
|
2390
|
+
const qopts = { demo: argsIn.includes("--demo"), autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent"), explain: argsIn.includes("--explain"), phone: valuedOpt(argsIn, "--phone") };
|
|
2391
|
+
if (!qopts.demo && !qopts.autoApproveDemo && forkApplies()) {
|
|
2392
|
+
log(brand() + bold(" quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
|
|
2393
|
+
const pick = await firstRunFork();
|
|
2394
|
+
if (pick.kind === "stop")
|
|
2395
|
+
return pick.code;
|
|
2396
|
+
if (pick.kind === "init")
|
|
2397
|
+
return cmdInit(pick.agentId, {});
|
|
2398
|
+
if (pick.kind === "demo")
|
|
2399
|
+
qopts.demo = true;
|
|
2400
|
+
}
|
|
2401
|
+
return cmdQuickstart(qopts);
|
|
2261
2402
|
}
|
|
2262
2403
|
case "init": {
|
|
2263
2404
|
const unique = argsIn.includes("--unique");
|
|
@@ -2270,7 +2411,7 @@ async function main(argv) {
|
|
|
2270
2411
|
}
|
|
2271
2412
|
const ddIdx = argsIn.indexOf("--");
|
|
2272
2413
|
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
|
|
2414
|
+
let name = nameArg ?? (process.env.DECIDIO_AGENT_ID || defaultAgentId());
|
|
2274
2415
|
if (unique)
|
|
2275
2416
|
name = `${name}-${randomBytes(2).toString("hex")}`;
|
|
2276
2417
|
return cmdInit(name, { force: argsIn.includes("--force"), expiresDays: expRaw ? Number(expRaw) : undefined });
|
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.1 # tracks the SDK release - checked by packages/conformance/manifests.mjs
|
|
9
9
|
description: >
|
|
10
10
|
Authorize an AI-agent action (proceed | route | block), confirm the captured outcome, and
|
|
11
11
|
receive the signed resume webhook when a routed action is decided. Every outcome seals a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decidio/sdk",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "One-line approval gate for AI-agent actions. Wrap a risky tool call with guard.protect(); Decidio's policy engine decides proceed | route | block, a human approves routed actions, and every outcome is sealed as a verifiable Authority Receipt.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"prepublishOnly": "npm run typecheck && npm run build && node ../publish-boundary/check.mjs --only @decidio/sdk"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@decidio/verify": "0.6.
|
|
87
|
+
"@decidio/verify": "0.6.1"
|
|
88
88
|
},
|
|
89
89
|
"author": "OmniTwin Technologies Inc.",
|
|
90
90
|
"bugs": {
|