@decidio/sdk 0.4.1 → 0.4.3

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,164 @@ 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.4.3 — the stale-install gate, frames you can read at a glance, and a phone door that lands
8
+
9
+ The founder's UAT round 2, second half (2026-09-04). Nobody is on 0.4.2 yet, so it was never tagged; this carries both.
10
+
11
+ - **A stale install must not keep running its defects** (founder ruling). Network commands (`quickstart`, `init`,
12
+ `login`, `doctor`, `approvals`, `receipt`, `tokens`) ask the public registry once a day whether a newer version
13
+ exists — npm for the TypeScript CLI, PyPI for Python, never our servers, cached in the user's profile, silent when
14
+ offline. If one does, an interactive run asks `<latest> is available (you have <yours>). Upgrade now? [y/n]`: `y`
15
+ runs the package manager's own upgrade (`npm i -g @decidio/sdk@latest` / `pip install -U decidio`, with its
16
+ integrity checks) and re-runs your command on the new version; `n` exits 1 naming the command to run when ready.
17
+ Under `npx` there is nothing installed to upgrade, so it prints the `@latest` form and exits. A headless run prints
18
+ the notice and continues; `DECIDIO_REQUIRE_LATEST=1` makes it exit instead (pinned CI is a choice, not a
19
+ surprise). `DECIDIO_NO_UPDATE_CHECK=1` skips the check. `verify`, `--help` and `--version` stay fully offline.
20
+ Seven vectors in both suites: not newer prints nothing; headless + newer notices and runs; the strict flag exits 1;
21
+ interactive `n` exits 1 running nothing; interactive `y` runs the upgrade (a labelled dry-run in the suite); an
22
+ unreachable registry is silent; `verify` never checks.
23
+ - **Frames carry a tone.** ③ (the choice) in orange, ④ and the summary in green for an approval and red for a
24
+ rejection — the frame and its title only, 256-colour orange with a yellow fallback, nothing under `NO_COLOR`
25
+ or a pipe. Founder: "it's hard for the eye to pick out which box means what."
26
+ - **The phone door lands.** iOS opens the installed app for `app.decidioai.com/q/*` (the association file now
27
+ claims it); the app consumes the code, signs in, and opens the decision. A phone without the app lands in its
28
+ browser on the phone build (`/phone/q/<code>`), the same single-decision screen the app shows. The desktop landing
29
+ is a hand-off page: the one decision on stage with its actions in view, the queue a click away (`?stage=1`).
30
+ - **`quickstart --phone web`** is the founder's test fork: the QR points at the phone web rendering, a path the app
31
+ never claims, so it opens in the browser even with the app installed. The default QR opens the app when it is
32
+ there. (`--demo` will pick the sandbox the same way, once it exists.)
33
+ - Founder decisions recorded (backlog): the sandbox for uninvited developers is TOUR-ONLY in v1 (no own agents —
34
+ surface risk and a second policy to maintain), its boxes are torn down at cleanup or by a one-hour sweeper, and
35
+ its phone surface is the web page, never the native app.
36
+
37
+ ## 0.4.2 — the zero-miss release: three gates, and every finding of the two 0.4.1 reviews
38
+
39
+ Built to the plan in `docs/Decidio_SDK_Zero_Miss_Plan.md` after an outside engineer's first-run review of 0.4.1 (B+)
40
+ and the prodsec retest of 0.4.1 (A-, safety A). Every item of both reports has a disposition below; the gates that
41
+ would have caught them now run on every pull request, on Linux AND Windows.
42
+
43
+ **The founder's UAT, round 2 (2026-09-04), folded in before the tag.** Steps 0–6 green on the previous build; the
44
+ web-app door landed on home (fixed twice the same night: the desktop's deep link is `/queue?focus=<id>`, and a browser
45
+ with no session bounced to `/login` before the link was consumed), and approving from the queue proved the terminal
46
+ follows an outside decision. Then six findings on the tour itself, all taken but one (which became a founder call):
47
+ - **One line per event.** The tour's paragraphs are gone from the default output: every step is a frame with two to
48
+ four terse rows, the outcome is one line, the reminder is two, and "Where next" is one. `--explain` prints the
49
+ narration that used to be the default. Target met: the approval tour is about 40 lines without the QR.
50
+ - **Every step framed** (①②③④ and the summary), with an escape-aware word wrap so a styled row never overflows a
51
+ narrow window; continuation lines hang under the label column.
52
+ - **A short hand-off link.** `POST /demo/session-link` now returns `https://app.decidioai.com/q/<13-char code>`
53
+ instead of a 110-character URL: it fits the frame, stays clickable AND copyable, and the credential row remembers
54
+ which decision the code was minted for (`handoff_decision_id`, cleared in the same single-use burn), so the web
55
+ app's new `/q/<code>` route signs in and lands on that decision. A hand-off never flips `emailVerified`.
56
+ - **A small QR, still printed by default** (the founder's call: "it looks impressive in a demo"). The QR carries the
57
+ short link upper-cased, so the encoder uses its alphanumeric mode, at the lowest error-correction level with a
58
+ one-module quiet zone: 25 modules instead of 41, about 14 rows instead of 23.
59
+ - **The lab is invite-only, and the CLI says so.** An uninvited address running `login` used to be told "we emailed
60
+ you a link" and wait for nothing (the server's anti-enumeration answer). The CLI now says a link arrives only for
61
+ an invited address and where to request access.
62
+ - **"The wrapper IS the SDK."** The first sentence of the help (and the READMEs) settles the SDK-versus-wrapper
63
+ question: `guard.protect(fn, describe)` ships in this package; the CLI is the tour, the sign-in and the token admin.
64
+ - Founder calls recorded, not built: a sandbox tenant with device-bound identities and a single-decision preview page
65
+ for developers who are not invited (the SDK's distribution unlock); scan → native app → the decision (iOS claims
66
+ `/d/*` for the installed app; the QR opens Safari today); `record kind: agent-receipt` wording.
67
+
68
+ **Round 2, second look (2026-09-04, same night).** The founder re-ran on the deployed build: web-app approval and the
69
+ deep link now work; the QR was MISSING; the text above and below the frames was still dense. Fixes: (1) `qrcode@1.5`'s
70
+ half-block renderer throws "Invalid array length" on an ODD margin, so the 1-module quiet zone silently produced no QR
71
+ (the server logged it, the CLI showed nothing) - margin 2 with level L renders 25 modules in 13 rows, and the CLI now
72
+ says when no QR came back instead of dropping the row; (2) the sign-in block is six lines instead of eleven (one intro,
73
+ prompts that carry their own hints, one signed-in line); (3) the tour no longer prints the stderr "decidio →" host line
74
+ when its own Target line already said it; (4) ④ is three short rows (VALID + file / record kind + evidence / issuer)
75
+ instead of one wrapping line; (5) the reminder is two lines, the take-away has a breath around it.
76
+
77
+ **The founder's UAT pass (2026-09-03), folded in before the tag.** The script's steps 1–5 went green in a real
78
+ PowerShell; step 6 stopped on the sign-in: every command that needed the workspace asked for a fresh emailed link,
79
+ and "nobody would log in this many times". Ruled: sign in ONCE, no better and no worse than the web app; and a
80
+ human step you can believe from outside the terminal.
81
+ - **Sign in once.** `login [email]`: the CLI requests the emailed link itself, you paste it; the 30-day session is
82
+ kept in your user profile (`%LOCALAPPDATA%\decidio` / `~/.config/decidio`, owner-only, written through the same
83
+ atomic secret writer as `.env`), bound to the terminal window that created it (a second window gets its own) and to
84
+ the token's expiry. `logout` ends it; so does `quickstart cleanup` (the tour's sign-in ends with the tour, the way
85
+ the web app drops its session on sign-out). An exported `DECIDIO_SESSION_TOKEN` still outranks it for headless
86
+ shells. No server-side session revocation exists yet (the web app has none either); the stored token is discarded.
87
+ - **Three doors at the human step.** The ③ frame offers: answer **here** (y/n), open the decision in the **web app**
88
+ (a link that lands on it signed in), or scan a **QR** with **your phone** (the same link). The link comes from a
89
+ new authenticated endpoint, `POST /demo/session-link {decisionId}`: it mints the SAME single-use sign-in token the
90
+ emailed link carries, for the caller's own credential, five minutes instead of fifteen because it is shown on a
91
+ screen and lands in terminal scrollback, and it replaces any pending emailed link (one slot per credential). It
92
+ grants a sign-in and only a sign-in: the token is prefixed `h.` and the password-reset door refuses it, so a
93
+ session can move itself to a browser but never mint itself a durable password (invariant-guard block, fixed, with a
94
+ negative-smoke lock). The terminal races the prompt against the gated call: a decision
95
+ made in the app or on the phone closes the prompt, the CLI posts no approve/reject of its own, says "approved (or
96
+ rejected) in the web app (or on your phone) — this terminal saw it and followed", and the function runs
97
+ once, after the approval, exactly as before. If the hand-off endpoint fails, the terminal door still works alone.
98
+ - **Layout.** Each step is a headed section; the doors and the closing summary are frames sized to their content and
99
+ the terminal; the summary is a table (gated / decided / executed / receipt); the help page is in sections. Colour,
100
+ bold and clickable (OSC-8) links only on a TTY and never under `NO_COLOR`; every phrase the gates assert is unchanged.
101
+ - **Two platform defects found by the doors vector, both fixed:** on Windows a console read abandoned when the app
102
+ decided first kept libuv's reader thread blocked and Node's teardown waited forever (the process printed its last
103
+ line and never exited) — the CLI now drains stdout/stderr and exits explicitly on that path only; and Python's
104
+ `isatty()` is true for `NUL`, so the console-key reader spun on a headless run — a real console is now detected
105
+ through `GetConsoleMode` (and that same check now decides whether the CLI may prompt at all, so `NUL` is headless on every OS), pipes and `NUL` read on a thread, POSIX uses `select`.
106
+ - Vectors, both languages: `login` under a real terminal (email → link → stored session); the next command in the
107
+ same terminal asks nothing; another terminal is refused; `logout` removes the file; a headless tour on the stored
108
+ session; cleanup signs out; and the browser-decided tour (the fake gate approves after two polls; the CLI must post
109
+ no approval, run the function once, exit 0).
110
+
111
+ **Gates (structural):**
112
+ - **The stranger run.** Both CLI suites drive the interactive tour under a REAL terminal on every platform through one
113
+ shared driver (`packages/conformance/tty_driver.py`: POSIX pty, Windows ConPTY). `y`, `n` and three blank answers
114
+ are exercised end to end; a missing terminal driver is red on CI, never a skip. A `windows-latest` gates job runs
115
+ both CLI suites under ConPTY.
116
+ - **Claims as vectors.** `packages/conformance/vectors/claims.json`: retired phrases are tombstones that fail the build
117
+ if they reappear in shipped text; the help must carry what is true (`y approves`, `n rejects`, exit 3 at the human
118
+ moment, `--help` anywhere, `receipt <id> [--json]`); the no-answer stop's exit code is asserted from what the help
119
+ promises. The tombstone gate found a Python twin of a 0.4.1 text fix that had been missed.
120
+ - **Twin parity.** `packages/conformance/parity.mjs`: the vector labels of the TS and Python suites must match; a
121
+ one-sided vector is red unless `parity-allowlist.json` names it with a reason. The pre-0.4.2 asymmetry is
122
+ recorded as a baseline to burn down before 1.0.
123
+ - **No swallowed ledger read.** `packages/conformance/sdk-lint.mjs` refuses `.catch(() => undefined)`,
124
+ `except Exception: x = None` and empty catches around ledger and journal reads; it was red on seven such sites in
125
+ 0.4.1 and is green now, with two justified allowances that carry their reason.
126
+
127
+ **Fixed, both languages unless noted:**
128
+ - The help said "YOUR Enter is the approval" (false since 0.4.1); it now says y approves, n rejects, Enter is not an
129
+ answer. `--help`/`-h` after any command prints the one help page (exit 0); the bare word `help` past the usage
130
+ positions stays a positional. `receipt <id> [--json]` is documented.
131
+ - The rejection half of the tour downloads and verifies the DENIAL's receipt, pinned, and names its record kind; all
132
+ three verifier doors print `kind:` (read from the receipt's own canonical record). The approval half names its kind
133
+ too. The tour's receipt goes through the same no-overwrite writer as the `receipt` command.
134
+ - Receipt filenames escape rather than strip: `a.b` and `ab` can no longer share a file (`~` + hex code point).
135
+ - The unset-session reminder prints on every end of the tour (approved, rejected, stopped) with both the bash and the
136
+ PowerShell command; the README carries both.
137
+ - An answer typed after the tour's blocking call expired (5 minutes) is not posted; the tour says the request must be
138
+ decided in the queue.
139
+ - `resolve()` on a decision whose entry was consumed between reading and claiming it now reads the ledger through one
140
+ shared mapping: `already_resolved` when it ran, never "durable state undefined" or a false live-attempt claim; the
141
+ resolved hook fires for every non-`unknown` outcome including the ledger-fallback ones.
142
+ - **A corrupt terminal ledger head is loud** (prodsec V041-01): `resolve()` propagates the state-corrupt error instead
143
+ of returning `unknown`; the poll worker says it once per decision and keeps serving the other actions; the webhook
144
+ doors answer 500 naming the error.
145
+ - A landed `init` token whose `persisted` journal note failed to write is held, not an orphan (the credential file is
146
+ the proof); the failed note is reported, never silent. `quickstart cleanup` names a held token instead of saying
147
+ the journal owes nothing (prodsec V041-02).
148
+ - The tour prints the call it actually makes (TS: the blocking primitive, with the note that real agents use
149
+ `guard.protect`; Python already ran `guard.protect(..., mode="blocking")` and now prints that too).
150
+ - Node relay: `--target` is never echoed raw, even on the shape-check refusal; the relay binds `127.0.0.1`.
151
+
152
+ **Guard round (one, by ruling): no block, six warnings, all taken.** The verifiers no longer print a record kind on an
153
+ INVALID receipt (an attacker-supplied field on a rejected document); Python's `receipt --json` keeps stdout JSON-only
154
+ and both suites now assert that on a stdout-only capture; an unreadable store directory is corruption, not "no
155
+ revisions", and an unlistable store makes the worker say so once instead of idling silently; the lint reaches
156
+ directory listings and multi-form excepts and refuses a `lint-allow` without a reason; the three-door gate carries a
157
+ correctly signed DENIAL fixture and all three doors print `kind: denial` (69/69); the claims scan walks the adapters too.
158
+
159
+ **Not reproduced:** the reviewer saw the no-answer stop exit 1 in PowerShell; under ConPTY on Windows and under a pty
160
+ on Linux both CLIs exit 3, and the gates now assert that code from the help's own table on both platforms.
161
+
162
+ **Unchanged by ruling:** adapters stay Beta (no real-engine suites yet); provenance and repository metadata stay
163
+ post-1.0.
164
+
7
165
  ## 0.4.1 — "no" means no
8
166
 
9
167
  Two first-run defects found by an outside engineer within sixteen minutes of installing 0.4.0. Both were
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  One line to put a human-or-policy approval gate in front of any AI-agent action — and get a signed, verifiable Authority Receipt for every decision, with execution evidence accruing on the record.
4
4
 
5
5
  `@decidio/sdk` is the client for Decidio — the system of record for enterprise authority, from OmniTwin Technologies.
6
+ The wrapper IS the SDK: `guard.protect(fn, describe)` ships in this package, and the `npx @decidio/sdk` command line
7
+ is the two-minute tour, the sign-in and the token admin around it. Install, wrap one function, run — the tour is optional.
6
8
 
7
9
  ## The two-minute tour
8
10
 
@@ -16,7 +18,11 @@ you approve right there (or from your Decidio app), executes on approval, then d
16
18
  sealed Authority Receipt and verifies it offline in front of you, pinned to your workspace's
17
19
  issuer. It mints a **7-day** tour token (`init` mints 90-day tokens — different jobs, different
18
20
  lifetimes) and ends by offering to clean itself up (`quickstart cleanup` works later too).
19
- Sign in when prompted. No config: with no `DECIDIO_API_URL` set it targets the hosted synthetic
21
+ Sign in once — `npx @decidio/sdk login`, or when the tour asks: the CLI emails you a link, you paste it, and
22
+ the sign-in is kept in your profile (owner-only) for 30 days, or until this terminal closes, `logout`, or
23
+ `quickstart cleanup`. At the human step the tour offers three doors: answer here, open the decision in
24
+ the web app (a one-time link, signed in for you), or scan the QR with your phone — whichever answers
25
+ first wins, and the terminal follows. No config: with no `DECIDIO_API_URL` set it targets the hosted synthetic
20
26
  sandbox. Headless, the tour **stops at the routed decision** — approving is a human act, so it
21
27
  prints the approve command and exits rather than approve its own request (CI can pass
22
28
  `--auto-approve-demo`, which labels that authority as automated).
@@ -78,12 +84,14 @@ What `.env` contains after a successful init: `DECIDIO_API_URL`, `DECIDIO_AGENT_
78
84
  `DECIDIO_WORKSPACE_ID`, and `DECIDIO_WEBHOOK_SECRET` (operator deployments only). Pick any
79
85
  agent name — if it's already taken in your workspace, `init` stops cleanly and suggests a
80
86
  fresh one (or pass `--unique` to auto-append a random suffix — collision-proof for shared
81
- workspaces and CI). Headless terminal (no prompts possible)? Export `DECIDIO_SESSION_TOKEN` first:
87
+ workspaces and CI). Interactive terminals sign in once (`npx @decidio/sdk login` — stored owner-only in your
88
+ profile, bound to this terminal window, 30 days). Headless terminal (no prompts possible)? Export `DECIDIO_SESSION_TOKEN` first:
82
89
  request a sign-in link, then exchange its `?login=` value at `POST /demo/login-consume` — the
83
90
  response's `token` is your session token (the CLI prints these exact instructions when it
84
91
  can't prompt). That session token is your workspace login, not the agent's credential: unset it
85
92
  before launching agent code from the same shell, because child processes inherit exported
86
- variables. In the tour's human step, `y` approves and `n` rejects — the rejection half is worth
93
+ variables (`unset DECIDIO_SESSION_TOKEN` in bash/zsh, `Remove-Item Env:DECIDIO_SESSION_TOKEN` in
94
+ PowerShell). In the tour's human step, `y` approves and `n` rejects — the rejection half is worth
87
95
  watching once: the request is refused and your function does not run.
88
96
 
89
97
  **3. Protect one function** — save this as `quickstart.mjs`:
@@ -639,7 +647,10 @@ that fits every other confirm error.
639
647
  - `receipt <id>` — download a sealed decision's Authority Receipt to a `.json` file
640
648
  - `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.
641
649
  - `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
642
- - `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)
650
+ - `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
+ - **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 through npm and re-runs your command; `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
+ - `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).
653
+ - `login [email]` / `logout` — sign in ONCE (the CLI emails you a link; you paste it) and end that sign-in. 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).
643
654
  - `dev [--target URL]` — local relay for the resume webhook (operator setups)
644
655
 
645
656
  The CLI's stream contract: **stdout carries results only** (safe to pipe — `--json` output is
@@ -647,8 +658,9 @@ pure JSON); errors, usage shown on *error* paths, and the per-run target-host an
647
658
  to **stderr**. Asked-for help (an explicit `--help`, or invoking the bare CLI) is itself a
648
659
  result and prints on stdout. Exit codes: 0 success · 1 failure or honest no-op · 2 usage error · 3 `quickstart` stopped at the human moment (headless: the decision routed, but nothing executed and no receipt was written, so the run is deliberately not reported as a completed tour).
649
660
 
650
- Commands that administer your workspace (`init`, `approvals`, `receipt`) use a **session**
651
- sign-in held in memory for that run. What lands on disk in `.env`: the agent's floor-limited
661
+ Commands that administer your workspace (`init`, `approvals`, `receipt`, `tokens`) use a **session**
662
+ sign-in: `login` stores it in your user profile (owner-only, bound to this terminal window, 30 days;
663
+ `logout` or `quickstart cleanup` ends it), or `DECIDIO_SESSION_TOKEN` supplies it for headless shells. What lands on disk in `.env`: the agent's floor-limited
652
664
  API token, its locally-generated private signing key, and a webhook secret (used only in
653
665
  operator deployments) — `init` adds `.env` to `.gitignore` for you; move secrets to a secret
654
666
  manager for production.
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  export type ApprovalAnswer = "approve" | "reject" | "again";
3
3
  export declare function approvalAnswer(answer: unknown): ApprovalAnswer;
4
+ /** `decidio-receipt-<id>.json`, with every character outside [A-Za-z0-9_-] escaped as `~` + hex code point, so two
5
+ * distinct decision ids can never share a file (first-run review of 0.4.1: dots were STRIPPED, `a.b` and `ab`
6
+ * collided). `~` is outside the allowed set, so the escape is unambiguous. Exported for the smokes. */
7
+ export declare function receiptFileName(id: string): string;