@decidio/sdk 0.4.4 → 0.5.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 CHANGED
@@ -4,6 +4,44 @@ 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.5.0 — `verify` pins by default
8
+
9
+ **Breaking, on purpose (founder ruling 2026-09-04, after the prodsec review of 0.4.3).** Every CLI verify door - `npx
10
+ @decidio/sdk verify`, the standalone `decidio-verify`, `python -m decidio verify` and `python -m decidio.verify` -
11
+ now REFUSES to run without `--issuer <did:key>` (exit 2, with the remedy printed). An unpinned VALID only proves the
12
+ file agrees with itself: the reviewer forged a self-signed "wire all funds" receipt and the exit code said yes. Scripts
13
+ trust exit status, not warnings, so the safe check is now the default and the structural check is a named choice:
14
+ `--allow-unpinned` runs it, and its verdict carries the UNPINNED warning as before. `--allow-unpinned` together with
15
+ `--issuer` is refused as ambiguous, like a disagreeing repeated pin.
16
+
17
+ - **What to change:** add `--issuer <your workspace DID>` to any scripted verify (the `receipt` command prints the
18
+ DID; the app's record-verify panel shows it), or `--allow-unpinned` where you genuinely want structure only.
19
+ - **Unchanged:** the LIBRARY (`verifyReceipt(vc)` / `verify_receipt(vc)`) still verifies unpinned when no issuer is
20
+ passed - a caller reads `issuer` in the result and decides; the CLIs are what scripts trust blindly.
21
+ - Vectors in all three suites: the refusal (exit 2, names `--issuer` and `--allow-unpinned`), the named unpinned run
22
+ reaching a verdict, both flags refused together. The usage pages, both READMEs and the `receipt` command's hint
23
+ say the new rule; the live selftest names its one unpinned run.
24
+ - **The sealed payload names two fields for what they are (schemaVersion 2).** `deciders` → `nominatedDeciders` (the
25
+ people nominated IN ADVANCE; the ratifier is who decided, so `nominated deciders: 0, ratified: yes` reads as it
26
+ should) and, inside `authority`, `appliedLimits` → `evaluatedLimits` (what the request was measured against - the
27
+ requested amount, the auto-approve threshold, the hard cap - not limits that "applied"). New records carry the v2
28
+ `payloadSchemaUri`; the canonicalization (`jcs-v1`) is unchanged, so verification is untouched. Both verifiers print
29
+ `nominated deciders: N` and read the v1 names too (every record sealed before 0.5.0 is lab test data). Founder
30
+ ruling 2026-09-04: rename now, while no customer record carries the old names.
31
+ - **From the 0.4.4 first-run audit (B+), the small items** (founder: go): quickstart checks connectivity BEFORE its
32
+ first disk write (a dead host leaves the directory untouched and says "Nothing was written"); `tokens list --mine`
33
+ after cleanup reads this directory's agents from the token recovery journal; `receipt` prints resolved identities
34
+ only on a terminal (a pipe or a log gets a one-line note; `--json` hands them over explicitly); the ④ frame says the
35
+ issuer DID was first seen over this connection and where to cross-check it (`<api>/api/did`, public, no sign-in);
36
+ the help page and README say what cleanup leaves behind; the README no longer calls the lab ONE SHARED workspace
37
+ (every invited tester has their own), explains the in-memory quota counter, lists `receipt --json` and `dev --port`,
38
+ and calls the journal append-only by behaviour, not by enforcement. Vectors: the dead-host tour, `--mine` from the
39
+ journal, the receipt in a pipe vs on a terminal.
40
+ - Two Lows from the prodsec retest of 0.4.4: `quickstart cleanup` with nothing to clean still ENDS the stored sign-in
41
+ (the early "Nothing to clean up here" return skipped it, contradicting the documented promise; vector in both
42
+ suites), and the resume module's comments say `lost_race` / `already_resolved` where the runtime does (they still
43
+ said `unknown`); `onResolved` is documented as firing for every outcome except `unknown`.
44
+
7
45
  ## 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
46
 
9
47
  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
@@ -29,17 +29,18 @@ prints the approve command and exits rather than approve its own request (CI can
29
29
 
30
30
  ## Quickstart — seven steps to your first sealed receipt
31
31
 
32
- Under 30 minutes, no help needed. You'll need Node 20.6+ and a terminal.
32
+ 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
33
 
34
34
  **You'll need a Decidio workspace.** → Request one at [decidioai.com](https://decidioai.com/#early-access)
35
35
 
36
- The hosted sandbox is synthetic data, safe to experiment in. Note it is ONE SHARED workspace:
37
- workspace-wide views like `tokens list` or `approvals` show every lab member's agents and
38
- decisions — the shared-lab boundary working as designed. Production workspaces are per-tenant.
36
+ The hosted sandbox is synthetic data, safe to experiment in. Every invited tester gets their OWN
37
+ workspace: workspace-wide views like `tokens list` or `approvals` show every agent and decision in
38
+ your workspace — yours alone unless you invite others into it. Production workspaces are per-tenant too.
39
39
 
40
40
  Lab workspaces have a **governed-action allowance** (currently 30 actions/hour); hitting it raises
41
41
  a typed `DecidioRateLimitError` naming the limit and reset time — nothing is routed or executed —
42
- and `tokens list` shows your remaining allowance any time.
42
+ and `tokens list` shows your remaining allowance any time. The counter is in memory, per API instance: a
43
+ deploy resets it, so `30/30` right after a governed action means the API restarted, not that the action was free.
43
44
 
44
45
  **PowerShell users** — the equivalents for every Bash idiom used below:
45
46
 
@@ -66,7 +67,7 @@ npx @decidio/sdk init --unique my-agent # --unique appends a random suffix: co
66
67
  > Just ran `npx @decidio/sdk quickstart` in this directory and kept its agent? Its `.env` already
67
68
  > works — skip this step and continue at step 3.
68
69
 
69
- Every CLI command talks to the hosted sandbox by default and announces its target host on
70
+ Every network command talks to the hosted sandbox by default (`verify` is offline) and announces its target host on
70
71
  stderr each run — set `DECIDIO_API_URL` (env or `.env`) to point at another Decidio instead.
71
72
 
72
73
  `init` signs you into your workspace (email + password, or paste the sign-in link from your
@@ -152,9 +153,10 @@ npx @decidio/sdk verify --issuer <did:key:...> decidio-receipt-<decisionId>.json
152
153
  ```
153
154
 
154
155
  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). An UNPINNED
156
- `verify` proves internal consistency only — any keyholder could have issued such a file — and
157
- says so; pinned verification proves *who*. The download is an authenticated fetch from your
156
+ prints it from the authenticated response, so it is already on your screen). Since 0.5.0 the
157
+ CLI refuses to run without it: an UNPINNED check proves internal consistency only — any keyholder
158
+ could have issued such a file — so it is a named choice (`--allow-unpinned`), and its verdict says
159
+ so; pinned verification proves *who*. The download is an authenticated fetch from your
158
160
  workspace; the *verification* is fully offline — the verifier ships inside this package (no second download, no registry probe, same
159
161
  implementation as the standalone `@decidio/verify`). The receipt is a W3C Verifiable
160
162
  Credential: signature and content binding verify with no Decidio account and no network — the
@@ -523,14 +525,14 @@ ever wanted it is an offline job with every writer stopped.
523
525
  The cryptography is only half the evidence. These are the fields that an outside reader has
524
526
  actually misread on a first run, and what each one means.
525
527
 
526
- **`ratified: yes` next to `deciders: 0` is not a contradiction.** `deciders` is the list of people
528
+ **`ratified: yes` next to `nominated deciders: 0` is not a contradiction.** `nominatedDeciders` is the list of people
527
529
  nominated **in advance** to decide. When an agent's request is *routed* rather than pre-assigned,
528
530
  that list is empty — nobody was nominated, because the request had not happened yet. The person who
529
531
  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
532
+ `nominated deciders: 0, ratified: yes`, and the human is recorded, in the field named for the role they
531
533
  played. The verifier prints this explanation whenever the pair appears.
532
534
 
533
- **`appliedLimits` is what the request was MEASURED AGAINST, not a rule that fired.** It holds up to
535
+ **`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
536
  three entries, and only those that have a value: `requested_amount` (the amount actually asked for),
535
537
  `auto_approve_threshold` and `hard_cap` (the ceilings on the agent's own token). So a decision routed
536
538
  to a human because *no rule matched* can still list an `auto_approve_threshold` — that is the bar the
@@ -551,14 +553,14 @@ the roadmap; today the URN is an identifier only, and this note exists so it doe
551
553
 
552
554
  **`prevHash` proves a pointer, not an ancestry.** A non-null `prevHash` shows this record was
553
555
  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
556
+ exists or is what it claims. Verify a chain (`verify --issuer <did> a.json b.json c.json`) when the sequence
555
557
  matters.
556
558
 
557
559
  **Pinning is trust-on-first-use.** The first time you fetch a receipt, the record *and* the issuer
558
560
  DID arrive in the same authenticated response — so that first check trusts the connection, not the
559
561
  key. What pinning buys is everything afterwards: record the DID once, out of band if you can, and
560
562
  every later verification is checked against a key you chose rather than one you were handed. The
561
- verifier says so on every unpinned run.
563
+ verifier refuses an unpinned run unless you ask for one (`--allow-unpinned`), and marks that verdict.
562
564
 
563
565
  ## Upgrading from 0.2.x to 0.3.0
564
566
 
@@ -638,21 +640,22 @@ that fits every other confirm error.
638
640
 
639
641
  - `quickstart` — the guided two-minute tour (gate → approve → execute → proof)
640
642
  - `--auto-approve-demo` approves the tour's own synthetic decision so it can run headless; it
641
- then CLEANS UP the tour agent and token automatically. Pass `--keep-agent` to keep them.
643
+ then revokes the tour token and deletes its local credentials automatically (the inert
644
+ registration row and the permanent sealed record stay). Pass `--keep-agent` to keep them.
642
645
  - An interactive run keeps the agent by default and offers cleanup at the end;
643
646
  `quickstart cleanup` works later too.
644
647
  - `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
648
  - `doctor` — config, connectivity, and what your current token can do
646
649
  - `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>` — download a sealed decision's Authority Receipt to a `.json` file
650
+ - `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
651
  - `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
652
  - `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
653
  - `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
654
  - **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.
655
+ - **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.
653
656
  - `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
657
  - `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)
658
+ - `dev [--target URL] [--port N]` — local relay for the resume webhook (operator setups)
656
659
 
657
660
  The CLI's stream contract: **stdout carries results only** (safe to pipe — `--json` output is
658
661
  pure JSON); errors, usage shown on *error* paths, and the per-run target-host announcement go
@@ -706,7 +709,7 @@ the next reader grades a promise, not a moving target:
706
709
  - **The issuer DID is trust on first use.** The tour and `receipt` print your workspace's issuer DID
707
710
  from the same authenticated response that carries the receipt. Pinned verification is only as strong
708
711
  as where you keep that DID: record it through a channel you trust independently (your app's
709
- record-verify panel today; a published well-known location is on the roadmap).
712
+ 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
713
  - **Verification is cryptographic, not schema validation.** `VALID` means the issuer's key signed these
711
714
  canonical claims; the sealed-payload schema the receipt names by hash is not shipped, so a
712
715
  structurally odd but correctly signed document is not refused on shape. Shipping the schema is on
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ 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
16
  const VERSION = (() => {
17
17
  try {
18
18
  return createRequire(import.meta.url)("../package.json").version;
@@ -1040,7 +1040,10 @@ async function cmdReceipt(id, jsonMode = false) {
1040
1040
  }
1041
1041
  if (conf?.status)
1042
1042
  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) {
1043
+ if (resolved && Object.keys(resolved).length && !process.stdout.isTTY) {
1044
+ 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.`);
1045
+ }
1046
+ else if (resolved && Object.keys(resolved).length) {
1044
1047
  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
1048
  for (const [tok, val] of Object.entries(resolved))
1046
1049
  log(` ${tok} → ${val}`);
@@ -1051,11 +1054,12 @@ async function cmdReceipt(id, jsonMode = false) {
1051
1054
  log(``);
1052
1055
  log(`${issuer}`);
1053
1056
  log(``);
1057
+ 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
1058
  log(` Then verify, pinned to it (the same DID, inline — copy from the line above, not this one,`);
1055
1059
  log(` which your terminal may wrap):`);
1056
1060
  log(` npx @decidio/sdk verify --issuer ${issuer} ${name}`);
1057
- log(` Without --issuer the verifier proves the receipt is internally consistent, but NOT who`);
1058
- log(` issued it; it says so in its output.`);
1061
+ log(` The verifier refuses to run without --issuer: a pin is what makes VALID mean "issued by your`);
1062
+ log(` workspace"; --allow-unpinned runs a structural check only, and says so.`);
1059
1063
  }
1060
1064
  else {
1061
1065
  log(` To also prove WHO issued it, pin your workspace's issuer DID (shown in your app's record verify panel):`);
@@ -1065,6 +1069,7 @@ async function cmdReceipt(id, jsonMode = false) {
1065
1069
  }
1066
1070
  function cmdVerify(argsIn) {
1067
1071
  let expectedIssuer;
1072
+ let allowUnpinned = false;
1068
1073
  const files = [];
1069
1074
  for (let i = 0; i < argsIn.length; i++) {
1070
1075
  const a = argsIn[i];
@@ -1072,6 +1077,10 @@ function cmdVerify(argsIn) {
1072
1077
  files.push(...argsIn.slice(i + 1));
1073
1078
  break;
1074
1079
  }
1080
+ if (a === "--allow-unpinned") {
1081
+ allowUnpinned = true;
1082
+ continue;
1083
+ }
1075
1084
  if (a === "--issuer") {
1076
1085
  const v = argsIn[i + 1];
1077
1086
  if (v === undefined || v === "" || v.startsWith("-")) {
@@ -1102,10 +1111,18 @@ function cmdVerify(argsIn) {
1102
1111
  }
1103
1112
  }
1104
1113
  if (!files.length) {
1105
- err("usage: npx @decidio/sdk verify [--issuer <did:key>] <receipt.json> [more.json ...]");
1114
+ err("usage: npx @decidio/sdk verify --issuer <did:key> <receipt.json> [more.json ...] (or --allow-unpinned)");
1106
1115
  err(" Verifies Authority Receipts OFFLINE — signature, content binding, and (for several files) chain order.");
1107
1116
  return 2;
1108
1117
  }
1118
+ if (expectedIssuer !== undefined && allowUnpinned) {
1119
+ err("✘ --allow-unpinned and --issuer together — an unpinned check and a pinned one are different questions; ask one.");
1120
+ return 2;
1121
+ }
1122
+ if (expectedIssuer === undefined && !allowUnpinned) {
1123
+ err(pinRequiredMessage("npx @decidio/sdk verify"));
1124
+ return 2;
1125
+ }
1109
1126
  const texts = new Map();
1110
1127
  for (const f of files) {
1111
1128
  try {
@@ -1158,6 +1175,11 @@ async function cmdQuickstart(opts = {}) {
1158
1175
  const envFile = ["DECIDIO_AGENT_KEY", "DECIDIO_AGENT_ID", "DECIDIO_AGENT_DID", "DECIDIO_API_TOKEN"].some((k) => (prior[k] ?? "").trim()) ? ".env.quickstart" : ENV_PATH;
1159
1176
  if (!assertNotTracked(envFile))
1160
1177
  return 1;
1178
+ const reach = await api("/api/health", {}, "", true);
1179
+ if (!(reach.status >= 200 && reach.status < 300)) {
1180
+ 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
+ return 1;
1182
+ }
1161
1183
  ensureGitignore();
1162
1184
  let agentId = `quickstart-${randomBytes(5).toString("hex")}`;
1163
1185
  if (EXPLAIN)
@@ -1479,7 +1501,7 @@ async function cmdQuickstart(opts = {}) {
1479
1501
  `✔ VALID — ${fname} ${dim("(signature + content binding, offline, PINNED to your workspace issuer)")}`,
1480
1502
  `${bold("decision")} payInvoice $86,000 · REJECTED · routed to a human by policy (no auto-approve rule for this agent)`,
1481
1503
  `${bold("record")} record kind: ${recordKindOf(rec.body.credential) ?? "?"} ${dim("— the denial is sealed too")}`,
1482
- `${bold("issuer")} ${issuer} ${dim("· your trust anchor: verify --issuer <did> <receipt.json>")}`,
1504
+ `${bold("issuer")} ${issuer} ${dim(`· your trust anchor — first seen over this signed-in connection; cross-check ${API}/api/did, then verify --issuer <did> <receipt.json>`)}`,
1483
1505
  ], "bad");
1484
1506
  denialVerified = true;
1485
1507
  }
@@ -1564,7 +1586,7 @@ async function cmdQuickstart(opts = {}) {
1564
1586
  `✔ VALID — ${fname} ${dim("(signature + content binding, offline, PINNED to your workspace issuer)")}`,
1565
1587
  `${bold("decision")} payInvoice $86,000 · APPROVED · routed to a human by policy (no auto-approve rule for this agent)`,
1566
1588
  `${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("· your trust anchor: verify --issuer <did> <receipt.json>")}`,
1589
+ `${bold("issuer")} ${issuer} ${dim(`· your trust anchor — first seen over this signed-in connection; cross-check ${API}/api/did, then verify --issuer <did> <receipt.json>`)}`,
1568
1590
  ], "good");
1569
1591
  const secs = Math.round((Date.now() - t0) / 1000);
1570
1592
  block("✔", `Done in ${secs}s`, [
@@ -1778,7 +1800,16 @@ async function cmdTokens(sub, jti, mineOnly = false) {
1778
1800
  err("usage: npx @decidio/sdk tokens revoke <jti> (find the jti with: npx @decidio/sdk tokens list)");
1779
1801
  return 2;
1780
1802
  }
1781
- if (mineOnly && !process.env.DECIDIO_AGENT_ID) {
1803
+ const journalAgents = (() => {
1804
+ try {
1805
+ return [...new Set(journalRows().map((row) => row?.agentId).filter((a) => typeof a === "string" && a.length > 0))];
1806
+ }
1807
+ catch {
1808
+ return [];
1809
+ }
1810
+ })();
1811
+ const myAgents = process.env.DECIDIO_AGENT_ID ? [process.env.DECIDIO_AGENT_ID] : journalAgents;
1812
+ if (mineOnly && !myAgents.length) {
1782
1813
  err(`✘ --mine needs an agent identity, and there is no DECIDIO_AGENT_ID in this directory's .env or your environment.`);
1783
1814
  err(` Run this from your agent's directory (where init wrote .env), or drop --mine for the workspace-wide list.`);
1784
1815
  return 2;
@@ -1818,8 +1849,8 @@ async function cmdTokens(sub, jti, mineOnly = false) {
1818
1849
  return 1;
1819
1850
  }
1820
1851
  const all = r.body?.tokens ?? r.body ?? [];
1821
- const myAgent = process.env.DECIDIO_AGENT_ID;
1822
- const tokens = mineOnly ? all.filter((t) => t.agentId === myAgent) : all;
1852
+ const myAgent = process.env.DECIDIO_AGENT_ID || (journalAgents.length ? `${journalAgents.join(", ")} (from the token recovery journal)` : undefined);
1853
+ const tokens = mineOnly ? all.filter((t) => myAgents.includes(t.agentId)) : all;
1823
1854
  if (!Array.isArray(tokens) || !tokens.length) {
1824
1855
  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
1856
  return 0;
@@ -1888,7 +1919,8 @@ THE TOUR
1888
1919
  automated; ends with a keep-or-cleanup choice; --explain prints
1889
1920
  the long narration of each step; --phone web makes the QR open
1890
1921
  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
1922
+ npx @decidio/sdk quickstart cleanup revoke the tour's token + delete its local credentials (the inert
1923
+ registration row and the permanent sealed record stay)
1892
1924
  (and end the stored sign-in)
1893
1925
  npx @decidio/sdk quickstart --auto-approve-demo headless tour; cleans up after itself
1894
1926
  (add --keep-agent to KEEP the tour agent + token)
@@ -1922,7 +1954,8 @@ APPROVALS AND RECEIPTS
1922
1954
  ... approve <id> -- "-50% cut, per the board"
1923
1955
  ... approve <id> --reason "-50% cut, per the board"
1924
1956
  npx @decidio/sdk receipt <id> [--json] download a sealed decision's Authority Receipt (.json)
1925
- npx @decidio/sdk verify [--issuer <did>] <receipt.json> [more...] verify receipts OFFLINE
1957
+ npx @decidio/sdk verify --issuer <did> <receipt.json> [more...] verify receipts OFFLINE, pinned to your
1958
+ workspace's issuer (--allow-unpinned: structural check only)
1926
1959
 
1927
1960
  DEVELOPMENT
1928
1961
  npx @decidio/sdk dev [--target URL] [--port N] local relay for the resume webhook
@@ -2063,7 +2096,7 @@ async function main(argv) {
2063
2096
  doctor: { flags: [] },
2064
2097
  approvals: { flags: ["--json", "--mine"], valued: ["--reason"] },
2065
2098
  receipt: { flags: ["--json"] },
2066
- verify: { flags: [], valued: ["--issuer"] },
2099
+ verify: { flags: ["--allow-unpinned"], valued: ["--issuer"] },
2067
2100
  tokens: { flags: ["--mine"] },
2068
2101
  dev: { flags: [], valued: ["--target", "--port"] },
2069
2102
  };
@@ -2143,6 +2176,8 @@ async function main(argv) {
2143
2176
  log(held.length
2144
2177
  ? `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
2178
  : "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.");
2179
+ if (endSession())
2180
+ log(` ✔ signed out — the stored sign-in is removed from this computer (the token itself stays valid until it expires).`);
2146
2181
  return 0;
2147
2182
  }
2148
2183
  const session = await acquireSession("Cleaning the quickstart up");
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 `unknown`, so nothing will pick it up on its own. It is stalled until an
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 after every terminal resolution (executed / indeterminate / rejected / blocked /
586
- * no_handler), via either transport. Use it to log, close a listener, or exit a CLI.
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
- * a second call after success finds nothing parked → "unknown" (no double-write). */
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.4.4 # tracks the SDK release - checked by packages/conformance/manifests.mjs
8
+ version: 0.5.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.4.4",
3
+ "version": "0.5.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.4.4"
87
+ "@decidio/verify": "0.5.0"
88
88
  },
89
89
  "author": "OmniTwin Technologies Inc.",
90
90
  "bugs": {