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