@decidio/sdk 0.4.4 → 0.6.0
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 +71 -0
- package/README.md +25 -20
- package/dist/cli.js +150 -43
- package/dist/demo-gate.d.ts +16 -0
- package/dist/demo-gate.js +161 -0
- package/dist/resume.d.ts +7 -5
- package/openapi.yaml +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,77 @@ 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.0 — `quickstart --demo`: the whole tour on your machine, no account
|
|
8
|
+
|
|
9
|
+
An uninvited developer could not run the tour at all: a workspace needs an invite (no open signup, by ruling). A
|
|
10
|
+
hosted sandbox was designed, red-teamed by the privacy council (BLOCK as written; eight design-level highs) and then
|
|
11
|
+
costed against the alternatives: the cheapest, least-maintenance path for a stranger's first two minutes is a LOCAL
|
|
12
|
+
demo with a hosted page for the phone moment. Founder ruling 2026-09-05.
|
|
13
|
+
|
|
14
|
+
- **`npx @decidio/sdk quickstart --demo`** runs the SAME tour code against an in-process stand-in of the Decidio API
|
|
15
|
+
on 127.0.0.1 (`demo-gate.ts`, shipped in this package): register, mint, gate, route, approve or reject, execute
|
|
16
|
+
once, and a REAL Ed25519 receipt signed by a key generated for the run — verifiable offline with the shipped
|
|
17
|
+
verifier, and provably NOT the hosted issuer's (pinning it to the hosted DID fails). Nothing hosted, no account, no
|
|
18
|
+
workspace: the tour runs in a temporary folder and leaves exactly one file behind, the receipt (never over an
|
|
19
|
+
existing one). Its content says `recordSeries: local-demo`.
|
|
20
|
+
- **The web and phone doors still work.** The stand-in asks a hosted RELAY (`POST /demo/relay`) for a code, a link to
|
|
21
|
+
the demo decision page (`app.decidioai.com/demo/<code>`) and a QR; the page shows the product's decision surface for
|
|
22
|
+
the tour's fixed scenario and writes one word — approve or reject — which the stand-in reads while the tour waits.
|
|
23
|
+
The relay holds no account, no record and no personal data; a code lives ten minutes, takes one verdict, and every
|
|
24
|
+
failure answers with one shape. Offline (relay unreachable) the tour is terminal-only and says so.
|
|
25
|
+
- **Honesty is asserted, not hoped:** the frames say LOCAL DEMO; ④ says the issuer is a key this demo generated and
|
|
26
|
+
where real tours pin theirs; the smoke plants the forbidden phrases ("PINNED to your workspace issuer", "hosted
|
|
27
|
+
synthetic sandbox", "your trust anchor") and asserts the directory diff is the receipt alone; a decision made on the
|
|
28
|
+
page reaches the terminal with no keystroke. The sign-in dead end now offers `--demo`.
|
|
29
|
+
- **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)
|
|
30
|
+
lands on the phone build's own decision screen — the same Detail the installed app renders, with Approve and Reject
|
|
31
|
+
where they always are — not the desktop panel squeezed into a phone viewport (founder UAT round 2). It holds no
|
|
32
|
+
session and says LOCAL DEMO in one line; the decided moment says the terminal seals. Host-side only: nothing in this
|
|
33
|
+
package changed for it.
|
|
34
|
+
- **The demo leaves your sign-in alone.** `quickstart --demo` never signs in, so its cleanup no longer ends a stored
|
|
35
|
+
sign-in that happens to be on the computer (it did, in the founder's UAT; the smoke now plants one and asserts it
|
|
36
|
+
survives, silently).
|
|
37
|
+
- TypeScript CLI first — `npx @decidio/sdk quickstart` is where an uninvited developer arrives; the Python twin gains
|
|
38
|
+
`--demo` when asked for (the version bump is in lockstep).
|
|
39
|
+
|
|
40
|
+
## 0.5.0 — `verify` pins by default
|
|
41
|
+
|
|
42
|
+
**Breaking, on purpose (founder ruling 2026-09-04, after the prodsec review of 0.4.3).** Every CLI verify door - `npx
|
|
43
|
+
@decidio/sdk verify`, the standalone `decidio-verify`, `python -m decidio verify` and `python -m decidio.verify` -
|
|
44
|
+
now REFUSES to run without `--issuer <did:key>` (exit 2, with the remedy printed). An unpinned VALID only proves the
|
|
45
|
+
file agrees with itself: the reviewer forged a self-signed "wire all funds" receipt and the exit code said yes. Scripts
|
|
46
|
+
trust exit status, not warnings, so the safe check is now the default and the structural check is a named choice:
|
|
47
|
+
`--allow-unpinned` runs it, and its verdict carries the UNPINNED warning as before. `--allow-unpinned` together with
|
|
48
|
+
`--issuer` is refused as ambiguous, like a disagreeing repeated pin.
|
|
49
|
+
|
|
50
|
+
- **What to change:** add `--issuer <your workspace DID>` to any scripted verify (the `receipt` command prints the
|
|
51
|
+
DID; the app's record-verify panel shows it), or `--allow-unpinned` where you genuinely want structure only.
|
|
52
|
+
- **Unchanged:** the LIBRARY (`verifyReceipt(vc)` / `verify_receipt(vc)`) still verifies unpinned when no issuer is
|
|
53
|
+
passed - a caller reads `issuer` in the result and decides; the CLIs are what scripts trust blindly.
|
|
54
|
+
- Vectors in all three suites: the refusal (exit 2, names `--issuer` and `--allow-unpinned`), the named unpinned run
|
|
55
|
+
reaching a verdict, both flags refused together. The usage pages, both READMEs and the `receipt` command's hint
|
|
56
|
+
say the new rule; the live selftest names its one unpinned run.
|
|
57
|
+
- **The sealed payload names two fields for what they are (schemaVersion 2).** `deciders` → `nominatedDeciders` (the
|
|
58
|
+
people nominated IN ADVANCE; the ratifier is who decided, so `nominated deciders: 0, ratified: yes` reads as it
|
|
59
|
+
should) and, inside `authority`, `appliedLimits` → `evaluatedLimits` (what the request was measured against - the
|
|
60
|
+
requested amount, the auto-approve threshold, the hard cap - not limits that "applied"). New records carry the v2
|
|
61
|
+
`payloadSchemaUri`; the canonicalization (`jcs-v1`) is unchanged, so verification is untouched. Both verifiers print
|
|
62
|
+
`nominated deciders: N` and read the v1 names too (every record sealed before 0.5.0 is lab test data). Founder
|
|
63
|
+
ruling 2026-09-04: rename now, while no customer record carries the old names.
|
|
64
|
+
- **From the 0.4.4 first-run audit (B+), the small items** (founder: go): quickstart checks connectivity BEFORE its
|
|
65
|
+
first disk write (a dead host leaves the directory untouched and says "Nothing was written"); `tokens list --mine`
|
|
66
|
+
after cleanup reads this directory's agents from the token recovery journal; `receipt` prints resolved identities
|
|
67
|
+
only on a terminal (a pipe or a log gets a one-line note; `--json` hands them over explicitly); the ④ frame says the
|
|
68
|
+
issuer DID was first seen over this connection and where to cross-check it (`<api>/api/did`, public, no sign-in);
|
|
69
|
+
the help page and README say what cleanup leaves behind; the README no longer calls the lab ONE SHARED workspace
|
|
70
|
+
(every invited tester has their own), explains the in-memory quota counter, lists `receipt --json` and `dev --port`,
|
|
71
|
+
and calls the journal append-only by behaviour, not by enforcement. Vectors: the dead-host tour, `--mine` from the
|
|
72
|
+
journal, the receipt in a pipe vs on a terminal.
|
|
73
|
+
- Two Lows from the prodsec retest of 0.4.4: `quickstart cleanup` with nothing to clean still ENDS the stored sign-in
|
|
74
|
+
(the early "Nothing to clean up here" return skipped it, contradicting the documented promise; vector in both
|
|
75
|
+
suites), and the resume module's comments say `lost_race` / `already_resolved` where the runtime does (they still
|
|
76
|
+
said `unknown`); `onResolved` is documented as firing for every outcome except `unknown`.
|
|
77
|
+
|
|
7
78
|
## 0.4.4 — the small findings of the 0.4.3 prodsec run: frames at 80 columns, a local install upgrades in place, a key-less .env survives the tour
|
|
8
79
|
|
|
9
80
|
An external prodsec reviewer ran 0.4.3 from a stranger's install (2026-09-04; grade B, "trust in the core mechanics
|
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ 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
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
One command, guided: it registers a fresh agent (key generated locally), gates a demo action
|
|
@@ -29,17 +30,18 @@ prints the approve command and exits rather than approve its own request (CI can
|
|
|
29
30
|
|
|
30
31
|
## Quickstart — seven steps to your first sealed receipt
|
|
31
32
|
|
|
32
|
-
Under 30 minutes, no help needed. You'll need Node 20.6+ and a terminal.
|
|
33
|
+
Under 30 minutes for all seven steps (the tour itself is two minutes), no help needed. You'll need Node 20.6+ and a terminal.
|
|
33
34
|
|
|
34
35
|
**You'll need a Decidio workspace.** → Request one at [decidioai.com](https://decidioai.com/#early-access)
|
|
35
36
|
|
|
36
|
-
The hosted sandbox is synthetic data, safe to experiment in.
|
|
37
|
-
workspace-wide views like `tokens list` or `approvals` show every
|
|
38
|
-
|
|
37
|
+
The hosted sandbox is synthetic data, safe to experiment in. Every invited tester gets their OWN
|
|
38
|
+
workspace: workspace-wide views like `tokens list` or `approvals` show every agent and decision in
|
|
39
|
+
your workspace — yours alone unless you invite others into it. Production workspaces are per-tenant too.
|
|
39
40
|
|
|
40
41
|
Lab workspaces have a **governed-action allowance** (currently 30 actions/hour); hitting it raises
|
|
41
42
|
a typed `DecidioRateLimitError` naming the limit and reset time — nothing is routed or executed —
|
|
42
|
-
and `tokens list` shows your remaining allowance any time.
|
|
43
|
+
and `tokens list` shows your remaining allowance any time. The counter is in memory, per API instance: a
|
|
44
|
+
deploy resets it, so `30/30` right after a governed action means the API restarted, not that the action was free.
|
|
43
45
|
|
|
44
46
|
**PowerShell users** — the equivalents for every Bash idiom used below:
|
|
45
47
|
|
|
@@ -66,7 +68,7 @@ npx @decidio/sdk init --unique my-agent # --unique appends a random suffix: co
|
|
|
66
68
|
> Just ran `npx @decidio/sdk quickstart` in this directory and kept its agent? Its `.env` already
|
|
67
69
|
> works — skip this step and continue at step 3.
|
|
68
70
|
|
|
69
|
-
Every
|
|
71
|
+
Every network command talks to the hosted sandbox by default (`verify` is offline) and announces its target host on
|
|
70
72
|
stderr each run — set `DECIDIO_API_URL` (env or `.env`) to point at another Decidio instead.
|
|
71
73
|
|
|
72
74
|
`init` signs you into your workspace (email + password, or paste the sign-in link from your
|
|
@@ -152,9 +154,10 @@ npx @decidio/sdk verify --issuer <did:key:...> decidio-receipt-<decisionId>.json
|
|
|
152
154
|
```
|
|
153
155
|
|
|
154
156
|
Lead with the pin: `--issuer` binds the check to YOUR workspace's DID (the `receipt` command
|
|
155
|
-
prints it from the authenticated response, so it is already on your screen).
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
prints it from the authenticated response, so it is already on your screen). Since 0.5.0 the
|
|
158
|
+
CLI refuses to run without it: an UNPINNED check proves internal consistency only — any keyholder
|
|
159
|
+
could have issued such a file — so it is a named choice (`--allow-unpinned`), and its verdict says
|
|
160
|
+
so; pinned verification proves *who*. The download is an authenticated fetch from your
|
|
158
161
|
workspace; the *verification* is fully offline — the verifier ships inside this package (no second download, no registry probe, same
|
|
159
162
|
implementation as the standalone `@decidio/verify`). The receipt is a W3C Verifiable
|
|
160
163
|
Credential: signature and content binding verify with no Decidio account and no network — the
|
|
@@ -523,14 +526,14 @@ ever wanted it is an offline job with every writer stopped.
|
|
|
523
526
|
The cryptography is only half the evidence. These are the fields that an outside reader has
|
|
524
527
|
actually misread on a first run, and what each one means.
|
|
525
528
|
|
|
526
|
-
**`ratified: yes` next to `deciders: 0` is not a contradiction.** `
|
|
529
|
+
**`ratified: yes` next to `nominated deciders: 0` is not a contradiction.** `nominatedDeciders` is the list of people
|
|
527
530
|
nominated **in advance** to decide. When an agent's request is *routed* rather than pre-assigned,
|
|
528
531
|
that list is empty — nobody was nominated, because the request had not happened yet. The person who
|
|
529
532
|
actually decided is the **ratifier** (`ratifiedBy`). So a receipt for a human-approved action reads
|
|
530
|
-
`deciders: 0, ratified: yes`, and the human is recorded, in the field named for the role they
|
|
533
|
+
`nominated deciders: 0, ratified: yes`, and the human is recorded, in the field named for the role they
|
|
531
534
|
played. The verifier prints this explanation whenever the pair appears.
|
|
532
535
|
|
|
533
|
-
**`
|
|
536
|
+
**`evaluatedLimits` is what the request was MEASURED AGAINST, not a rule that fired.** (Receipts sealed before 0.5.0 spell these two `deciders` and `appliedLimits` - schemaVersion 1; the verifier reads both.) It holds up to
|
|
534
537
|
three entries, and only those that have a value: `requested_amount` (the amount actually asked for),
|
|
535
538
|
`auto_approve_threshold` and `hard_cap` (the ceilings on the agent's own token). So a decision routed
|
|
536
539
|
to a human because *no rule matched* can still list an `auto_approve_threshold` — that is the bar the
|
|
@@ -551,14 +554,14 @@ the roadmap; today the URN is an identifier only, and this note exists so it doe
|
|
|
551
554
|
|
|
552
555
|
**`prevHash` proves a pointer, not an ancestry.** A non-null `prevHash` shows this record was
|
|
553
556
|
sealed after a specific predecessor, but a single-file verification cannot check that predecessor
|
|
554
|
-
exists or is what it claims. Verify a chain (`verify a.json b.json c.json`) when the sequence
|
|
557
|
+
exists or is what it claims. Verify a chain (`verify --issuer <did> a.json b.json c.json`) when the sequence
|
|
555
558
|
matters.
|
|
556
559
|
|
|
557
560
|
**Pinning is trust-on-first-use.** The first time you fetch a receipt, the record *and* the issuer
|
|
558
561
|
DID arrive in the same authenticated response — so that first check trusts the connection, not the
|
|
559
562
|
key. What pinning buys is everything afterwards: record the DID once, out of band if you can, and
|
|
560
563
|
every later verification is checked against a key you chose rather than one you were handed. The
|
|
561
|
-
verifier
|
|
564
|
+
verifier refuses an unpinned run unless you ask for one (`--allow-unpinned`), and marks that verdict.
|
|
562
565
|
|
|
563
566
|
## Upgrading from 0.2.x to 0.3.0
|
|
564
567
|
|
|
@@ -638,21 +641,23 @@ that fits every other confirm error.
|
|
|
638
641
|
|
|
639
642
|
- `quickstart` — the guided two-minute tour (gate → approve → execute → proof)
|
|
640
643
|
- `--auto-approve-demo` approves the tour's own synthetic decision so it can run headless; it
|
|
641
|
-
then
|
|
644
|
+
then revokes the tour token and deletes its local credentials automatically (the inert
|
|
645
|
+
registration row and the permanent sealed record stay). Pass `--keep-agent` to keep them.
|
|
642
646
|
- An interactive run keeps the agent by default and offers cleanup at the end;
|
|
643
647
|
`quickstart cleanup` works later too.
|
|
644
648
|
- `init [agentId] [--unique] [--force] [--expires-days N]` — sign in, register, mint the agent token, write `.env` (+ `.gitignore` entries). Refuses to overwrite a *different* agent's key in this directory; `--force` backs the old `.env` up first.
|
|
645
649
|
- `doctor` — config, connectivity, and what your current token can do
|
|
646
650
|
- `approvals [--mine] [--json]` / `approvals approve|reject <id> [reason]` — the human side, from your terminal (the list is workspace-wide; `--mine` narrows it to this directory's agent)
|
|
647
|
-
- `receipt <id
|
|
651
|
+
- `receipt <id> [--json]` — download a sealed decision's Authority Receipt to a `.json` file (`--json`: stdout is the JSON result only; the identities your access covers print only on a terminal, or in the JSON)
|
|
648
652
|
- `verify [--issuer <did>] <file>...` — verify receipts offline (the canonical verifier; the standalone `@decidio/verify` package is the same implementation, for auditors who want zero Decidio dependency — the sdk depends on the verifier released alongside it, at the same version). Unknown flags are refused, never ignored. Fully offline AND fully decoupled: it touches no network and reads no `.env` — a receipt verifies identically from an empty directory.
|
|
649
653
|
- `tokens list [--mine]` / `tokens revoke <jti>` — see and kill agent tokens (values are never re-shown; the jti is the revocation handle). Each row carries its status — `active`, `expired`, or `REVOKED <date>` — so the list confirms the revocations it performs; re-revoking is an honest no-op (exit 1). The list is workspace-wide (a shared workspace shows every member's agents); `--mine` narrows to this directory's agent
|
|
650
654
|
- `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.
|
|
651
655
|
- **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.
|
|
652
|
-
- **The token recovery journal** (`.decidio-token-journal`, owner-only, gitignored; ids and timestamps, never a token value). 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.
|
|
656
|
+
- **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.
|
|
657
|
+
- `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.
|
|
653
658
|
- `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).
|
|
654
659
|
- `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).
|
|
655
|
-
- `dev [--target URL]` — local relay for the resume webhook (operator setups)
|
|
660
|
+
- `dev [--target URL] [--port N]` — local relay for the resume webhook (operator setups)
|
|
656
661
|
|
|
657
662
|
The CLI's stream contract: **stdout carries results only** (safe to pipe — `--json` output is
|
|
658
663
|
pure JSON); errors, usage shown on *error* paths, and the per-run target-host announcement go
|
|
@@ -706,7 +711,7 @@ the next reader grades a promise, not a moving target:
|
|
|
706
711
|
- **The issuer DID is trust on first use.** The tour and `receipt` print your workspace's issuer DID
|
|
707
712
|
from the same authenticated response that carries the receipt. Pinned verification is only as strong
|
|
708
713
|
as where you keep that DID: record it through a channel you trust independently (your app's
|
|
709
|
-
record-verify panel
|
|
714
|
+
record-verify panel, and published in the open by the Decidio host you use at `<api>/api/did` — for the hosted sandbox https://decidio-api.onrender.com/api/did).
|
|
710
715
|
- **Verification is cryptographic, not schema validation.** `VALID` means the issuer's key signed these
|
|
711
716
|
canonical claims; the sealed-payload schema the receipt names by hash is not shipped, so a
|
|
712
717
|
structurally odd but correctly signed document is not refused on shape. Shipping the schema is on
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { join as joinPath, resolve as resolvePath, sep as pathSep } from "node:path";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
7
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
@@ -12,7 +12,8 @@ import { createRequire } from "node:module";
|
|
|
12
12
|
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
|
-
import { verifyReceipt, runVerify } from "@decidio/verify";
|
|
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
|
}
|
|
@@ -564,6 +573,7 @@ async function loginFlow(purpose, emailArg) {
|
|
|
564
573
|
}
|
|
565
574
|
log(` ✔ sign-in email requested for ${email} — check your inbox and spam; paste the link (or its login= code; single-use, 15 minutes)`);
|
|
566
575
|
log(dim(` invite-only lab: a link arrives only for an invited address — not invited yet? request access: decidioai.com/#early-access`));
|
|
576
|
+
log(dim(` or see the whole story on this machine right now, no account: npx @decidio/sdk quickstart --demo`));
|
|
567
577
|
const pasted = await ask(" link or code: ");
|
|
568
578
|
if (!pasted) {
|
|
569
579
|
log(" ✘ nothing pasted — re-run when you have the email.");
|
|
@@ -1040,7 +1050,10 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
1040
1050
|
}
|
|
1041
1051
|
if (conf?.status)
|
|
1042
1052
|
log(` Sealed credential: the authority decision (its confirmation field reads "pending" forever — immutable). Current record evidence tier: ${conf.status}${conf.confirmed ? " (confirmed)" : ""}.`);
|
|
1043
|
-
if (resolved && Object.keys(resolved).length) {
|
|
1053
|
+
if (resolved && Object.keys(resolved).length && !process.stdout.isTTY) {
|
|
1054
|
+
log(` ${Object.keys(resolved).length} identity field(s) your access covers are shown only on a terminal (this is a pipe or a log) — use --json to receive them explicitly.`);
|
|
1055
|
+
}
|
|
1056
|
+
else if (resolved && Object.keys(resolved).length) {
|
|
1044
1057
|
log(` Resolved for you (${Object.keys(resolved).length} field${Object.keys(resolved).length === 1 ? "" : "s"} your access covers — the FILE keeps the tokens, so it stays verifiable and safe to hand on):`);
|
|
1045
1058
|
for (const [tok, val] of Object.entries(resolved))
|
|
1046
1059
|
log(` ${tok} → ${val}`);
|
|
@@ -1051,11 +1064,12 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
1051
1064
|
log(``);
|
|
1052
1065
|
log(`${issuer}`);
|
|
1053
1066
|
log(``);
|
|
1067
|
+
log(` Cross-check it out of band — the same DID is published by this Decidio host at ${API}/api/did (public, no sign-in).`);
|
|
1054
1068
|
log(` Then verify, pinned to it (the same DID, inline — copy from the line above, not this one,`);
|
|
1055
1069
|
log(` which your terminal may wrap):`);
|
|
1056
1070
|
log(` npx @decidio/sdk verify --issuer ${issuer} ${name}`);
|
|
1057
|
-
log(`
|
|
1058
|
-
log(`
|
|
1071
|
+
log(` The verifier refuses to run without --issuer: a pin is what makes VALID mean "issued by your`);
|
|
1072
|
+
log(` workspace"; --allow-unpinned runs a structural check only, and says so.`);
|
|
1059
1073
|
}
|
|
1060
1074
|
else {
|
|
1061
1075
|
log(` To also prove WHO issued it, pin your workspace's issuer DID (shown in your app's record verify panel):`);
|
|
@@ -1065,6 +1079,7 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
1065
1079
|
}
|
|
1066
1080
|
function cmdVerify(argsIn) {
|
|
1067
1081
|
let expectedIssuer;
|
|
1082
|
+
let allowUnpinned = false;
|
|
1068
1083
|
const files = [];
|
|
1069
1084
|
for (let i = 0; i < argsIn.length; i++) {
|
|
1070
1085
|
const a = argsIn[i];
|
|
@@ -1072,6 +1087,10 @@ function cmdVerify(argsIn) {
|
|
|
1072
1087
|
files.push(...argsIn.slice(i + 1));
|
|
1073
1088
|
break;
|
|
1074
1089
|
}
|
|
1090
|
+
if (a === "--allow-unpinned") {
|
|
1091
|
+
allowUnpinned = true;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1075
1094
|
if (a === "--issuer") {
|
|
1076
1095
|
const v = argsIn[i + 1];
|
|
1077
1096
|
if (v === undefined || v === "" || v.startsWith("-")) {
|
|
@@ -1102,10 +1121,18 @@ function cmdVerify(argsIn) {
|
|
|
1102
1121
|
}
|
|
1103
1122
|
}
|
|
1104
1123
|
if (!files.length) {
|
|
1105
|
-
err("usage: npx @decidio/sdk verify
|
|
1124
|
+
err("usage: npx @decidio/sdk verify --issuer <did:key> <receipt.json> [more.json ...] (or --allow-unpinned)");
|
|
1106
1125
|
err(" Verifies Authority Receipts OFFLINE — signature, content binding, and (for several files) chain order.");
|
|
1107
1126
|
return 2;
|
|
1108
1127
|
}
|
|
1128
|
+
if (expectedIssuer !== undefined && allowUnpinned) {
|
|
1129
|
+
err("✘ --allow-unpinned and --issuer together — an unpinned check and a pinned one are different questions; ask one.");
|
|
1130
|
+
return 2;
|
|
1131
|
+
}
|
|
1132
|
+
if (expectedIssuer === undefined && !allowUnpinned) {
|
|
1133
|
+
err(pinRequiredMessage("npx @decidio/sdk verify"));
|
|
1134
|
+
return 2;
|
|
1135
|
+
}
|
|
1109
1136
|
const texts = new Map();
|
|
1110
1137
|
for (const f of files) {
|
|
1111
1138
|
try {
|
|
@@ -1129,6 +1156,38 @@ function valuedOpt(args, flag) {
|
|
|
1129
1156
|
return eq ? eq.slice(flag.length + 1) : undefined;
|
|
1130
1157
|
}
|
|
1131
1158
|
async function cmdQuickstart(opts = {}) {
|
|
1159
|
+
if (!opts.demo)
|
|
1160
|
+
return cmdQuickstartInner(opts);
|
|
1161
|
+
const relayBase = SHELL_ENV.DECIDIO_DEMO_RELAY_URL === undefined ? "https://decidio-api.onrender.com" : (SHELL_ENV.DECIDIO_DEMO_RELAY_URL || null);
|
|
1162
|
+
const gate = await startDemoGate({ relayBase });
|
|
1163
|
+
enterDemo(gate.url);
|
|
1164
|
+
const origin = process.cwd();
|
|
1165
|
+
const work = mkdtempSync(joinPath(tmpdir(), "decidio-demo-"));
|
|
1166
|
+
process.chdir(work);
|
|
1167
|
+
try {
|
|
1168
|
+
return await cmdQuickstartInner(opts);
|
|
1169
|
+
}
|
|
1170
|
+
finally {
|
|
1171
|
+
process.chdir(origin);
|
|
1172
|
+
for (const f of readdirSync(work)) {
|
|
1173
|
+
if (!/^decidio-receipt-.*\.json$/.test(f))
|
|
1174
|
+
continue;
|
|
1175
|
+
let target = joinPath(origin, f);
|
|
1176
|
+
if (existsSync(target))
|
|
1177
|
+
target = joinPath(origin, f.replace(/\.json$/, `.${Date.now()}.json`));
|
|
1178
|
+
try {
|
|
1179
|
+
copyFileSync(joinPath(work, f), target);
|
|
1180
|
+
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
|
+
}
|
|
1182
|
+
catch (e) {
|
|
1183
|
+
err(` ⚠ could not copy the receipt into this folder (${e?.message ?? e}) — it is at ${joinPath(work, f)}`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
rmSync(work, { recursive: true, force: true });
|
|
1187
|
+
await gate.close();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function cmdQuickstartInner(opts = {}) {
|
|
1132
1191
|
const t0 = Date.now();
|
|
1133
1192
|
EXPLAIN = !!opts.explain;
|
|
1134
1193
|
if (opts.phone !== undefined && opts.phone !== "app" && opts.phone !== "web") {
|
|
@@ -1136,7 +1195,10 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1136
1195
|
return 2;
|
|
1137
1196
|
}
|
|
1138
1197
|
log(bold("Decidio quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
|
|
1139
|
-
|
|
1198
|
+
if (DEMO)
|
|
1199
|
+
log(` Target: LOCAL DEMO — an in-process stand-in for the Decidio API on this machine (${API}); nothing hosted, nothing kept`);
|
|
1200
|
+
else
|
|
1201
|
+
log(` Target: ${API}${process.env.DECIDIO_API_URL ? "" : dim(" (the hosted synthetic sandbox — set DECIDIO_API_URL to use your own)")}`);
|
|
1140
1202
|
hostAnnounced = true;
|
|
1141
1203
|
if (EXPLAIN) {
|
|
1142
1204
|
log(" This tour will: register one synthetic agent · mint a 7-DAY floor-limited token ·");
|
|
@@ -1146,19 +1208,29 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1146
1208
|
log(" · create one synthetic decision and its sealed record (records are permanent by design) · wait up to");
|
|
1147
1209
|
log(" 5 minutes for your y (approve) or n (reject) at the human step. Cleanup offer at the end.");
|
|
1148
1210
|
}
|
|
1211
|
+
else if (DEMO) {
|
|
1212
|
+
log(dim(" Creates here: the receipt only (decidio-receipt-<id>.json). The demo's agent, token and working files live in a temporary"));
|
|
1213
|
+
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."));
|
|
1214
|
+
}
|
|
1149
1215
|
else {
|
|
1150
1216
|
log(dim(" Creates here: a synthetic agent + 7-day token (.env), .decidio-token-journal (revocation ids, no secrets), .gitignore entries incl."));
|
|
1151
1217
|
log(dim(" .decidio-pending/ (the durable store), the receipt as decidio-receipt-<id>.json, one sealed record (permanent). --explain for the long form."));
|
|
1152
1218
|
}
|
|
1153
1219
|
log("");
|
|
1154
|
-
const session = await acquireSession("The quickstart");
|
|
1220
|
+
const session = DEMO ? "demo-session" : await acquireSession("The quickstart");
|
|
1155
1221
|
if (!session)
|
|
1156
1222
|
return 1;
|
|
1157
1223
|
const prior = readEnvFile();
|
|
1158
1224
|
const envFile = ["DECIDIO_AGENT_KEY", "DECIDIO_AGENT_ID", "DECIDIO_AGENT_DID", "DECIDIO_API_TOKEN"].some((k) => (prior[k] ?? "").trim()) ? ".env.quickstart" : ENV_PATH;
|
|
1159
1225
|
if (!assertNotTracked(envFile))
|
|
1160
1226
|
return 1;
|
|
1161
|
-
|
|
1227
|
+
const reach = await api("/api/health", {}, "", true);
|
|
1228
|
+
if (!(reach.status >= 200 && reach.status < 300)) {
|
|
1229
|
+
err(`✘ cannot reach ${API}${reach.status ? ` (HTTP ${reach.status})` : " (no response)"} — check the network (or DECIDIO_API_URL) and re-run. Nothing was written.`);
|
|
1230
|
+
return 1;
|
|
1231
|
+
}
|
|
1232
|
+
if (!DEMO)
|
|
1233
|
+
ensureGitignore();
|
|
1162
1234
|
let agentId = `quickstart-${randomBytes(5).toString("hex")}`;
|
|
1163
1235
|
if (EXPLAIN)
|
|
1164
1236
|
log(` Registering agent "${agentId}"…`);
|
|
@@ -1282,7 +1354,7 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1282
1354
|
block("①", "Register a synthetic agent", [
|
|
1283
1355
|
`${bold("agent")} ${agentId}`,
|
|
1284
1356
|
`${bold("key")} generated locally · only the public half was registered`,
|
|
1285
|
-
`${bold("token")} 7-day, floor-limited${mint.body.expiresAt ? ` · expires ${String(mint.body.expiresAt).slice(0, 10)}` : ""} · written to ${envFile}`,
|
|
1357
|
+
`${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}`,
|
|
1286
1358
|
]);
|
|
1287
1359
|
if (EXPLAIN) {
|
|
1288
1360
|
log(` The tour runs exactly this, the BLOCKING primitive (your real agent uses guard.protect instead:`);
|
|
@@ -1342,10 +1414,12 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1342
1414
|
const plainLink = `${APP_URL}/queue?focus=${encodeURIComponent(decisionId)}`;
|
|
1343
1415
|
const rows = [
|
|
1344
1416
|
`${bold("here")} Type y to approve or n to reject, then Enter ${dim("(waits up to 5 minutes)")}`,
|
|
1345
|
-
|
|
1417
|
+
DEMO
|
|
1418
|
+
? (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")}`)
|
|
1419
|
+
: `${bold("web app")} ${link(doorLink ?? plainLink)} ${dim(doorLink ? "(one-time sign-in · 5 minutes)" : "(sign in there if it asks)")}`,
|
|
1346
1420
|
];
|
|
1347
1421
|
if (qr) {
|
|
1348
|
-
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"}`);
|
|
1422
|
+
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"}`);
|
|
1349
1423
|
for (const l of qr.split("\n"))
|
|
1350
1424
|
if (l.trim().length)
|
|
1351
1425
|
rows.push(" " + l);
|
|
@@ -1469,17 +1543,17 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1469
1543
|
return failTour(1);
|
|
1470
1544
|
const issuer = rec.body?.issuer;
|
|
1471
1545
|
const v = verifyReceipt(rec.body.credential, issuer);
|
|
1472
|
-
const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ",
|
|
1546
|
+
const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", " + pinWords() : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
|
|
1473
1547
|
if (!v.valid || !issuer) {
|
|
1474
1548
|
log(` ${kindLine}`);
|
|
1475
1549
|
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.`);
|
|
1476
1550
|
return failTour(1);
|
|
1477
1551
|
}
|
|
1478
1552
|
block("④", "The record", [
|
|
1479
|
-
`✔ VALID — ${fname} ${dim(
|
|
1553
|
+
`✔ VALID — ${fname} ${dim(`(signature + content binding, offline, ${pinWords()})`)}`,
|
|
1480
1554
|
`${bold("decision")} payInvoice $86,000 · REJECTED · routed to a human by policy (no auto-approve rule for this agent)`,
|
|
1481
1555
|
`${bold("record")} record kind: ${recordKindOf(rec.body.credential) ?? "?"} ${dim("— the denial is sealed too")}`,
|
|
1482
|
-
`${bold("issuer")} ${issuer} ${dim(
|
|
1556
|
+
`${bold("issuer")} ${issuer} ${dim(issuerNote())}`,
|
|
1483
1557
|
], "bad");
|
|
1484
1558
|
denialVerified = true;
|
|
1485
1559
|
}
|
|
@@ -1507,7 +1581,8 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1507
1581
|
}
|
|
1508
1582
|
log("");
|
|
1509
1583
|
sessionReminder();
|
|
1510
|
-
|
|
1584
|
+
if (!DEMO)
|
|
1585
|
+
log(`${bold("Where next")} your queue ${link(APP_URL)} · README "Production mode: durable async resume" ${dim("· built by OmniTwin — decidioai.com/#early-access")}`);
|
|
1511
1586
|
}
|
|
1512
1587
|
else {
|
|
1513
1588
|
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);
|
|
@@ -1543,7 +1618,7 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1543
1618
|
return failTour(1);
|
|
1544
1619
|
const issuer = rec.body?.issuer;
|
|
1545
1620
|
const v = verifyReceipt(rec.body.credential, issuer);
|
|
1546
|
-
const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ",
|
|
1621
|
+
const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", " + pinWords() : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
|
|
1547
1622
|
if (!v.valid || !issuer)
|
|
1548
1623
|
log(` ${kindLine}`);
|
|
1549
1624
|
if (!v.valid) {
|
|
@@ -1561,33 +1636,44 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1561
1636
|
const detail = await api(`/api/records/${encodeURIComponent(decisionId)}`, {}, session);
|
|
1562
1637
|
const tier = detail.body?.record?.confirmation?.status;
|
|
1563
1638
|
block("④", "The receipt", [
|
|
1564
|
-
`✔ VALID — ${fname} ${dim(
|
|
1639
|
+
`✔ VALID — ${fname} ${dim(`(signature + content binding, offline, ${pinWords()})`)}`,
|
|
1565
1640
|
`${bold("decision")} payInvoice $86,000 · APPROVED · routed to a human by policy (no auto-approve rule for this agent)`,
|
|
1566
1641
|
`${bold("record")} record kind: ${recordKindOf(rec.body.credential) ?? "?"}${tier ? ` · evidence: ${tier}` : ""} ${dim("— the receipt seals the DECISION; execution evidence lives on the record")}`,
|
|
1567
|
-
`${bold("issuer")} ${issuer} ${dim(
|
|
1642
|
+
`${bold("issuer")} ${issuer} ${dim(issuerNote())}`,
|
|
1568
1643
|
], "good");
|
|
1569
1644
|
const secs = Math.round((Date.now() - t0) / 1000);
|
|
1570
1645
|
block("✔", `Done in ${secs}s`, [
|
|
1571
1646
|
`${bold("gated")} payInvoice $86,000 — ROUTED to a human as ${decisionId}`,
|
|
1572
1647
|
`${bold("decided")} ${decidedOutside ? "approved in the web app (or on your phone)" : opts.autoApproveDemo ? "approved by --auto-approve-demo (automated, and labeled so on the record)" : "approved by you, here"}`,
|
|
1573
1648
|
`${bold("executed")} your function ran once, AFTER approval`,
|
|
1574
|
-
`${bold("receipt")} VALID — verified offline, pinned to your workspace issuer (④ above)`,
|
|
1649
|
+
`${bold("receipt")} VALID — verified offline, pinned to ${DEMO ? "this demo's own key" : "your workspace issuer"} (④ above)`,
|
|
1575
1650
|
dim("gated → routed to a human → approved → executed once → a receipt you can verify offline"),
|
|
1576
1651
|
], "good");
|
|
1577
1652
|
log("");
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
log(
|
|
1653
|
+
if (DEMO) {
|
|
1654
|
+
log(bold("That was the mechanism — on this machine, against a stand-in. The product runs it against your workspace:"));
|
|
1655
|
+
log(` request access: decidioai.com/#early-access · have an invite? npx @decidio/sdk login, then npx @decidio/sdk quickstart`);
|
|
1656
|
+
log("");
|
|
1657
|
+
}
|
|
1658
|
+
else {
|
|
1659
|
+
log(bold("Take it to your real agent — the .env here already works:"));
|
|
1660
|
+
log(` import { guard } from "@decidio/sdk";`);
|
|
1661
|
+
log(` const doIt = guard.protect(doItRaw, (a) => ({ action: "yourAction", amount: a.amount, scope: "YourScope" }));${dim(" // durable by default")}`);
|
|
1662
|
+
log("");
|
|
1663
|
+
if (EXPLAIN) {
|
|
1664
|
+
log(` The README's seven-step flow is this same blocking flow in your own code; if you keep this agent, its .env`);
|
|
1665
|
+
log(` already works here, so skip that flow's init step. Production is DURABLE mode (default) + one worker.`);
|
|
1666
|
+
}
|
|
1585
1667
|
}
|
|
1586
1668
|
sessionReminder();
|
|
1587
|
-
|
|
1669
|
+
if (!DEMO)
|
|
1670
|
+
log(`${bold("Where next")} your queue ${link(APP_URL)} · README "Production mode: durable async resume" ${dim("· built by OmniTwin — decidioai.com/#early-access")}`);
|
|
1588
1671
|
}
|
|
1589
1672
|
let doCleanup = false;
|
|
1590
|
-
if (
|
|
1673
|
+
if (DEMO) {
|
|
1674
|
+
doCleanup = true;
|
|
1675
|
+
}
|
|
1676
|
+
else if (interactive) {
|
|
1591
1677
|
const answer = await ask(`\nKeep this agent for development? [Enter = keep · c + Enter = clean up now]: `);
|
|
1592
1678
|
doCleanup = answer.trim().toLowerCase() === "c";
|
|
1593
1679
|
if (!doCleanup)
|
|
@@ -1735,7 +1821,7 @@ async function quickstartCleanup(session, envFile) {
|
|
|
1735
1821
|
});
|
|
1736
1822
|
if (kept.length === 0) {
|
|
1737
1823
|
rmSync(envFile);
|
|
1738
|
-
log(` ✔ ${envFile} deleted (the agent's local key is gone — the registered agent is now inert)`);
|
|
1824
|
+
log(` ✔ ${envFile} deleted (the agent's local key is gone — ${DEMO ? "the temporary folder follows" : "the registered agent is now inert"})`);
|
|
1739
1825
|
}
|
|
1740
1826
|
else {
|
|
1741
1827
|
writeSecretFile(envFile, kept.join("\n") + "\n");
|
|
@@ -1751,9 +1837,13 @@ async function quickstartCleanup(session, envFile) {
|
|
|
1751
1837
|
log(` npx @decidio/sdk approvals reject ${c.id} "quickstart cleanup"`);
|
|
1752
1838
|
}
|
|
1753
1839
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1840
|
+
if (DEMO)
|
|
1841
|
+
log(` • nothing remains: the demo's stand-in agent, token and files go with the temporary folder — only the receipt is kept`);
|
|
1842
|
+
else {
|
|
1843
|
+
log(` • What remains, by design: the sealed decision record (records are permanent — that is the product)`);
|
|
1844
|
+
log(` and the agent's registration row (inert without key or token; server-side deregistration is on the roadmap).`);
|
|
1845
|
+
}
|
|
1846
|
+
if (!DEMO && existsSync(JOURNAL_PATH))
|
|
1757
1847
|
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.`);
|
|
1758
1848
|
if (existsSync(".decidio-pending")) {
|
|
1759
1849
|
log(` • Also kept: .decidio-pending/ — this agent's own execution ledger: whatever this agent parked (call arguments,`);
|
|
@@ -1761,6 +1851,8 @@ async function quickstartCleanup(session, envFile) {
|
|
|
1761
1851
|
log(` pending and you do not want the history, remove it:`);
|
|
1762
1852
|
log(` ${process.platform === "win32" ? "Remove-Item -Recurse .decidio-pending" : "rm -r .decidio-pending"}`);
|
|
1763
1853
|
}
|
|
1854
|
+
if (DEMO)
|
|
1855
|
+
return 0;
|
|
1764
1856
|
if (endSession())
|
|
1765
1857
|
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).`);
|
|
1766
1858
|
else if (process.env.DECIDIO_SESSION_TOKEN)
|
|
@@ -1778,7 +1870,16 @@ async function cmdTokens(sub, jti, mineOnly = false) {
|
|
|
1778
1870
|
err("usage: npx @decidio/sdk tokens revoke <jti> (find the jti with: npx @decidio/sdk tokens list)");
|
|
1779
1871
|
return 2;
|
|
1780
1872
|
}
|
|
1781
|
-
|
|
1873
|
+
const journalAgents = (() => {
|
|
1874
|
+
try {
|
|
1875
|
+
return [...new Set(journalRows().map((row) => row?.agentId).filter((a) => typeof a === "string" && a.length > 0))];
|
|
1876
|
+
}
|
|
1877
|
+
catch {
|
|
1878
|
+
return [];
|
|
1879
|
+
}
|
|
1880
|
+
})();
|
|
1881
|
+
const myAgents = process.env.DECIDIO_AGENT_ID ? [process.env.DECIDIO_AGENT_ID] : journalAgents;
|
|
1882
|
+
if (mineOnly && !myAgents.length) {
|
|
1782
1883
|
err(`✘ --mine needs an agent identity, and there is no DECIDIO_AGENT_ID in this directory's .env or your environment.`);
|
|
1783
1884
|
err(` Run this from your agent's directory (where init wrote .env), or drop --mine for the workspace-wide list.`);
|
|
1784
1885
|
return 2;
|
|
@@ -1818,8 +1919,8 @@ async function cmdTokens(sub, jti, mineOnly = false) {
|
|
|
1818
1919
|
return 1;
|
|
1819
1920
|
}
|
|
1820
1921
|
const all = r.body?.tokens ?? r.body ?? [];
|
|
1821
|
-
const myAgent = process.env.DECIDIO_AGENT_ID;
|
|
1822
|
-
const tokens = mineOnly ? all.filter((t) => t.agentId
|
|
1922
|
+
const myAgent = process.env.DECIDIO_AGENT_ID || (journalAgents.length ? `${journalAgents.join(", ")} (from the token recovery journal)` : undefined);
|
|
1923
|
+
const tokens = mineOnly ? all.filter((t) => myAgents.includes(t.agentId)) : all;
|
|
1823
1924
|
if (!Array.isArray(tokens) || !tokens.length) {
|
|
1824
1925
|
log(mineOnly ? `No agent tokens for "${myAgent}"${all.length ? ` (${all.length} elsewhere in this workspace — drop --mine to see them)` : ""}.` : "No agent tokens in this workspace.");
|
|
1825
1926
|
return 0;
|
|
@@ -1888,9 +1989,12 @@ THE TOUR
|
|
|
1888
1989
|
automated; ends with a keep-or-cleanup choice; --explain prints
|
|
1889
1990
|
the long narration of each step; --phone web makes the QR open
|
|
1890
1991
|
the phone web rendering even when the Decidio app is installed)
|
|
1891
|
-
npx @decidio/sdk quickstart cleanup revoke the tour's token + delete its local credentials
|
|
1992
|
+
npx @decidio/sdk quickstart cleanup revoke the tour's token + delete its local credentials (the inert
|
|
1993
|
+
registration row and the permanent sealed record stay)
|
|
1892
1994
|
(and end the stored sign-in)
|
|
1893
1995
|
npx @decidio/sdk quickstart --auto-approve-demo headless tour; cleans up after itself
|
|
1996
|
+
npx @decidio/sdk quickstart --demo the same tour against a LOCAL stand-in of the API — no account needed; the
|
|
1997
|
+
web/phone door is a hosted demo page; the receipt is the one thing kept
|
|
1894
1998
|
(add --keep-agent to KEEP the tour agent + token)
|
|
1895
1999
|
|
|
1896
2000
|
SIGN IN (once)
|
|
@@ -1922,7 +2026,8 @@ APPROVALS AND RECEIPTS
|
|
|
1922
2026
|
... approve <id> -- "-50% cut, per the board"
|
|
1923
2027
|
... approve <id> --reason "-50% cut, per the board"
|
|
1924
2028
|
npx @decidio/sdk receipt <id> [--json] download a sealed decision's Authority Receipt (.json)
|
|
1925
|
-
npx @decidio/sdk verify
|
|
2029
|
+
npx @decidio/sdk verify --issuer <did> <receipt.json> [more...] verify receipts OFFLINE, pinned to your
|
|
2030
|
+
workspace's issuer (--allow-unpinned: structural check only)
|
|
1926
2031
|
|
|
1927
2032
|
DEVELOPMENT
|
|
1928
2033
|
npx @decidio/sdk dev [--target URL] [--port N] local relay for the resume webhook
|
|
@@ -2056,14 +2161,14 @@ async function main(argv) {
|
|
|
2056
2161
|
}
|
|
2057
2162
|
const argsIn = argv.slice(3);
|
|
2058
2163
|
const KNOWN = {
|
|
2059
|
-
quickstart: { flags: ["--explain", "--auto-approve-demo", "--keep-agent"], valued: ["--phone"] },
|
|
2164
|
+
quickstart: { flags: ["--explain", "--auto-approve-demo", "--keep-agent", "--demo"], valued: ["--phone"] },
|
|
2060
2165
|
login: { flags: [] },
|
|
2061
2166
|
logout: { flags: [] },
|
|
2062
2167
|
init: { flags: ["--unique", "--force"], valued: ["--expires-days"] },
|
|
2063
2168
|
doctor: { flags: [] },
|
|
2064
2169
|
approvals: { flags: ["--json", "--mine"], valued: ["--reason"] },
|
|
2065
2170
|
receipt: { flags: ["--json"] },
|
|
2066
|
-
verify: { flags: [], valued: ["--issuer"] },
|
|
2171
|
+
verify: { flags: ["--allow-unpinned"], valued: ["--issuer"] },
|
|
2067
2172
|
tokens: { flags: ["--mine"] },
|
|
2068
2173
|
dev: { flags: [], valued: ["--target", "--port"] },
|
|
2069
2174
|
};
|
|
@@ -2143,6 +2248,8 @@ async function main(argv) {
|
|
|
2143
2248
|
log(held.length
|
|
2144
2249
|
? `Nothing to clean up here: no tour credentials. The token recovery journal names ${held.length} live token(s) held by a credential file here (${held.map((j) => j.jti).join(", ")}) — not tour tokens, left alone; doctor lists them.`
|
|
2145
2250
|
: "Nothing to clean up here: no .env.quickstart, .env doesn't belong to a quickstart agent, and the token recovery journal has no live token to act on.");
|
|
2251
|
+
if (endSession())
|
|
2252
|
+
log(` ✔ signed out — the stored sign-in is removed from this computer (the token itself stays valid until it expires).`);
|
|
2146
2253
|
return 0;
|
|
2147
2254
|
}
|
|
2148
2255
|
const session = await acquireSession("Cleaning the quickstart up");
|
|
@@ -2150,7 +2257,7 @@ async function main(argv) {
|
|
|
2150
2257
|
return 1;
|
|
2151
2258
|
return quickstartCleanup(session, envFile);
|
|
2152
2259
|
}
|
|
2153
|
-
return cmdQuickstart({ autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent"), explain: argsIn.includes("--explain"), phone: valuedOpt(argsIn, "--phone") });
|
|
2260
|
+
return cmdQuickstart({ demo: argsIn.includes("--demo"), autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent"), explain: argsIn.includes("--explain"), phone: valuedOpt(argsIn, "--phone") });
|
|
2154
2261
|
}
|
|
2155
2262
|
case "init": {
|
|
2156
2263
|
const unique = argsIn.includes("--unique");
|
|
@@ -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,161 @@
|
|
|
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 relayCode = null;
|
|
39
|
+
let relayOk = null;
|
|
40
|
+
const relayFetch = async (path, init = {}) => {
|
|
41
|
+
if (!relayBase)
|
|
42
|
+
return null;
|
|
43
|
+
const ctl = new AbortController();
|
|
44
|
+
const t = setTimeout(() => ctl.abort(), relayTimeoutMs);
|
|
45
|
+
try {
|
|
46
|
+
const r = await fetch(`${relayBase.replace(/\/$/, "")}${path}`, { ...init, signal: ctl.signal, headers: { "content-type": "application/json", ...(init.headers ?? {}) } });
|
|
47
|
+
if (r.status === 204)
|
|
48
|
+
return {};
|
|
49
|
+
if (!r.ok)
|
|
50
|
+
return null;
|
|
51
|
+
return await r.json().catch(() => ({}));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
clearTimeout(t);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const issueReceipt = (kind) => {
|
|
61
|
+
const canonical = `{"accountableParty":"[PERSON_1]","action":"payInvoice","amount":"[AMOUNT_2]","recordKind":"${kind}","recordSeries":"local-demo"}`;
|
|
62
|
+
const contentHash = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
63
|
+
const context = ["https://www.w3.org/ns/credentials/v2"];
|
|
64
|
+
const doc = {
|
|
65
|
+
"@context": context,
|
|
66
|
+
type: ["VerifiableCredential", "DecidioAuthorityRecord"],
|
|
67
|
+
issuer: issuerDid,
|
|
68
|
+
validFrom: sealedAt,
|
|
69
|
+
credentialSubject: { id: `urn:decidio:decision:${DECISION_ID}`, recordId: DECISION_ID, dNumber: D_NUMBER, contentHash, prevHash: null, recordCanonical: canonical },
|
|
70
|
+
};
|
|
71
|
+
const cfg = {
|
|
72
|
+
"@context": context, type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022",
|
|
73
|
+
created: sealedAt, verificationMethod: `${issuerDid}#${issuerDid.slice("did:key:".length)}`, proofPurpose: "assertionMethod",
|
|
74
|
+
};
|
|
75
|
+
const signingInput = Buffer.concat([
|
|
76
|
+
createHash("sha256").update(Buffer.from(jcsCanonicalize(cfg), "utf8")).digest(),
|
|
77
|
+
createHash("sha256").update(Buffer.from(jcsCanonicalize(doc), "utf8")).digest(),
|
|
78
|
+
]);
|
|
79
|
+
return { ...doc, proof: { ...cfg, proofValue: "z" + b58(edSign(null, signingInput, issuerKey.privateKey)) } };
|
|
80
|
+
};
|
|
81
|
+
const server = createServer((req, res) => {
|
|
82
|
+
let raw = "";
|
|
83
|
+
req.on("data", (c) => (raw += c));
|
|
84
|
+
req.on("end", async () => {
|
|
85
|
+
let body;
|
|
86
|
+
try {
|
|
87
|
+
body = raw ? JSON.parse(raw) : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
body = undefined;
|
|
91
|
+
}
|
|
92
|
+
const path = (req.url ?? "").split("?")[0];
|
|
93
|
+
const send = (o, status = 200) => { res.writeHead(status, { "content-type": "application/json" }); res.end(JSON.stringify(o)); };
|
|
94
|
+
try {
|
|
95
|
+
if (path === "/api/health")
|
|
96
|
+
return send({ ok: true, demo: true });
|
|
97
|
+
if (path === "/api/did")
|
|
98
|
+
return send({ did: issuerDid, method: "did:key", alg: "Ed25519", demo: true });
|
|
99
|
+
if (path === "/api/decisions/cards")
|
|
100
|
+
return send({ cards: [] });
|
|
101
|
+
if (path === "/api/agents/register")
|
|
102
|
+
return send({ agentId: body?.agentId, did: body?.did });
|
|
103
|
+
if (path === "/api/workspace/agent-token" && req.method === "POST")
|
|
104
|
+
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() });
|
|
105
|
+
if (path === "/api/workspace/agent-token" && req.method === "GET")
|
|
106
|
+
return send({ tokens: [] });
|
|
107
|
+
if (/^\/api\/workspace\/agent-token\/[^/]+$/.test(path) && req.method === "DELETE")
|
|
108
|
+
return send({ ok: true, changed: true });
|
|
109
|
+
if (path === "/agent/authorize") {
|
|
110
|
+
verdict = null;
|
|
111
|
+
return send({ decision: "route", decisionId: DECISION_ID, receiptId: "R-DEMO-1" });
|
|
112
|
+
}
|
|
113
|
+
if (path.startsWith("/agent/status/")) {
|
|
114
|
+
if (!verdict && relayCode) {
|
|
115
|
+
const r = await relayFetch(`/demo/relay/${relayCode}`);
|
|
116
|
+
if (r && (r.verdict === "approve" || r.verdict === "reject"))
|
|
117
|
+
verdict = r.verdict;
|
|
118
|
+
}
|
|
119
|
+
return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver" } : { state: verdict === "approve" ? "approved" : "pending" });
|
|
120
|
+
}
|
|
121
|
+
if (/^\/api\/decisions\/[^/]+\/(approve|reject)$/.test(path)) {
|
|
122
|
+
const already = verdict !== null;
|
|
123
|
+
verdict = path.endsWith("/reject") ? "reject" : "approve";
|
|
124
|
+
if (relayCode && !already)
|
|
125
|
+
void relayFetch(`/demo/relay/${relayCode}`, { method: "PUT", body: JSON.stringify({ verdict }) });
|
|
126
|
+
return send({ ok: true, decisionId: DECISION_ID, sealed: true, ...(already ? { alreadyResolved: true } : {}) });
|
|
127
|
+
}
|
|
128
|
+
if (path === "/agent/confirm")
|
|
129
|
+
return send({ ok: true, confirmation: "application_confirmed", recordId: DECISION_ID });
|
|
130
|
+
if (path === "/demo/session-link") {
|
|
131
|
+
const r = await relayFetch("/demo/relay", { method: "POST", body: JSON.stringify({ decisionId: DECISION_ID }) });
|
|
132
|
+
if (r && typeof r.code === "string" && /^[0-9A-HJKMNP-TV-Z]{13}$/.test(r.code) && typeof r.link === "string" && /^https?:\/\//.test(r.link)) {
|
|
133
|
+
relayCode = r.code;
|
|
134
|
+
relayOk = true;
|
|
135
|
+
return send({ status: "ok", link: r.link, qr: typeof r.qr === "string" ? r.qr : null, expiresAt: r.expiresAt ?? null });
|
|
136
|
+
}
|
|
137
|
+
relayOk = false;
|
|
138
|
+
return send({ status: "unavailable", error: "the demo relay is unreachable — this run is terminal-only" }, 503);
|
|
139
|
+
}
|
|
140
|
+
if (/^\/api\/records\/[^/]+\/verify$/.test(path)) {
|
|
141
|
+
const cred = issueReceipt(verdict === "reject" ? "denial" : "approval");
|
|
142
|
+
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 });
|
|
143
|
+
}
|
|
144
|
+
if (/^\/api\/records\/[^/]+$/.test(path))
|
|
145
|
+
return send({ record: { confirmation: verdict === "approve" ? { status: "application_confirmed", confirmed: true } : { status: "pending", confirmed: false } } });
|
|
146
|
+
return send({ error: "not part of the local demo" }, 404);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return send({ error: String(e?.message ?? e) }, 500);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
154
|
+
const port = server.address().port;
|
|
155
|
+
return {
|
|
156
|
+
url: `http://127.0.0.1:${port}`,
|
|
157
|
+
issuerDid,
|
|
158
|
+
relayReachable: () => relayOk,
|
|
159
|
+
close: () => new Promise((r) => server.close(() => r())),
|
|
160
|
+
};
|
|
161
|
+
}
|
package/dist/resume.d.ts
CHANGED
|
@@ -470,7 +470,7 @@ export type RecoveryState =
|
|
|
470
470
|
"parked"
|
|
471
471
|
/** Claimed by an attempt that never reached the invocation boundary, so the handler PROVABLY
|
|
472
472
|
* did not run — but the row is not parked either: `list()` excludes it and every ordinary
|
|
473
|
-
* signal answers `
|
|
473
|
+
* signal answers `lost_race`, so nothing will pick it up on its own. It is stalled until an
|
|
474
474
|
* operator releases it. Reporting this as "parked" (as 0.2.0's first draft did) told a UI an
|
|
475
475
|
* approved action was still queued to resume when in fact it had been silently dropped. */
|
|
476
476
|
| "claimed_stalled"
|
|
@@ -582,8 +582,9 @@ export interface ResumeConfig {
|
|
|
582
582
|
/** The callback URL Decidio should POST the verdict to (webhook transport). Sent
|
|
583
583
|
* on /agent/authorize so the decision carries it; omit when using the poll worker. */
|
|
584
584
|
resumeUrl?: string;
|
|
585
|
-
/** Fired
|
|
586
|
-
* no_handler), via either transport. Use it to log, close a
|
|
585
|
+
/** Fired for every outcome except `unknown` (executed / indeterminate / rejected / blocked /
|
|
586
|
+
* no_handler / already_resolved / lost_race), via either transport. Use it to log, close a
|
|
587
|
+
* listener, or exit a CLI.
|
|
587
588
|
* `indeterminate` is new in 0.2.0 and DOES fire here: a handler that ran and then threw is a
|
|
588
589
|
* terminal outcome for automation — nothing will retry it — so a listener that only handled
|
|
589
590
|
* the old four would silently miss the one case that needs a human. */
|
|
@@ -601,8 +602,9 @@ export interface ResumeController extends ResumeConfig {
|
|
|
601
602
|
/** Park a routed action WITH its integrity binding (prefer this over store.put — the
|
|
602
603
|
* binding is what lets resolve() refuse a tampered payload). */
|
|
603
604
|
park(p: Omit<PendingAction, "createdAt" | "argsSig">): Promise<void>;
|
|
604
|
-
/** Drive a parked decision to completion once a verdict is known. Idempotent:
|
|
605
|
-
*
|
|
605
|
+
/** Drive a parked decision to completion once a verdict is known. Idempotent: a second call
|
|
606
|
+
* after success answers `already_resolved` (no double-write); an id this SDK never parked
|
|
607
|
+
* answers `unknown`; a call that loses the pre-invocation claim answers `lost_race`. */
|
|
606
608
|
resolve(input: {
|
|
607
609
|
decisionId: string;
|
|
608
610
|
verdict: "approved" | "rejected" | "blocked";
|
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.0 # 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.0",
|
|
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.0"
|
|
88
88
|
},
|
|
89
89
|
"author": "OmniTwin Technologies Inc.",
|
|
90
90
|
"bugs": {
|