@decidio/sdk 0.1.7 → 0.1.8
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/README.md +30 -13
- package/dist/cli.js +401 -89
- package/dist/index.d.ts +6 -0
- package/dist/index.js +20 -0
- package/openapi.yaml +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,22 +11,29 @@ npx @decidio/sdk quickstart
|
|
|
11
11
|
One command, guided: it registers a fresh agent (key generated locally), gates a demo action
|
|
12
12
|
with the same one line you'll use on your real function, routes it to a human — **you** — lets
|
|
13
13
|
you approve right there (or from your Decidio app), executes on approval, then downloads the
|
|
14
|
-
sealed Authority Receipt and verifies it offline in front of you
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
sealed Authority Receipt and verifies it offline in front of you, pinned to your workspace's
|
|
15
|
+
issuer. It mints a **7-day** tour token (`init` mints 90-day tokens — different jobs, different
|
|
16
|
+
lifetimes) and ends by offering to clean itself up (`quickstart cleanup` works later too).
|
|
17
|
+
Sign in when prompted. No config: with no `DECIDIO_API_URL` set it targets the hosted synthetic
|
|
18
|
+
sandbox. Headless, the tour **stops at the routed decision** — approving is a human act, so it
|
|
19
|
+
prints the approve command and exits rather than approve its own request (CI can pass
|
|
20
|
+
`--auto-approve-demo`, which labels that authority as automated).
|
|
17
21
|
|
|
18
22
|
## Quickstart — seven steps to your first sealed receipt
|
|
19
23
|
|
|
20
24
|
Under 30 minutes, no help needed. You'll need Node 20.6+, a terminal, and a Decidio workspace
|
|
21
25
|
(request one at [decidioai.com](https://decidioai.com/#early-access) — the hosted sandbox is
|
|
22
|
-
synthetic data, safe to experiment in).
|
|
26
|
+
synthetic data, safe to experiment in). Lab workspaces have a **governed-action allowance**
|
|
27
|
+
(currently 30 actions/hour); hitting it raises a typed `DecidioRateLimitError` naming the limit
|
|
28
|
+
and reset time — nothing is routed or executed — and `tokens list` shows your remaining
|
|
29
|
+
allowance any time.
|
|
23
30
|
|
|
24
31
|
**PowerShell users** — the equivalents for every Bash idiom used below:
|
|
25
32
|
|
|
26
33
|
```powershell
|
|
27
34
|
mkdir decidio-quickstart; cd decidio-quickstart # instead of mkdir ... && cd ...
|
|
28
35
|
$env:DECIDIO_API_URL = "https://decidio-api.onrender.com" # instead of export NAME=value
|
|
29
|
-
$env:
|
|
36
|
+
$env:DECIDIO_AGENT_ID = "my-agent"; npx @decidio/sdk init
|
|
30
37
|
# instead of VAR=value npx ... (inline env)
|
|
31
38
|
```
|
|
32
39
|
|
|
@@ -37,13 +44,15 @@ mkdir decidio-quickstart && cd decidio-quickstart
|
|
|
37
44
|
npm install @decidio/sdk
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
**2.
|
|
47
|
+
**2. Register your agent**
|
|
41
48
|
|
|
42
49
|
```bash
|
|
43
|
-
export DECIDIO_API_URL=https://decidio-api.onrender.com # the hosted sandbox
|
|
44
50
|
npx @decidio/sdk init --unique my-agent # --unique appends a random suffix: collision-proof
|
|
45
51
|
```
|
|
46
52
|
|
|
53
|
+
Every CLI command talks to the hosted sandbox by default and announces its target host on
|
|
54
|
+
stderr each run — set `DECIDIO_API_URL` (env or `.env`) to point at another Decidio instead.
|
|
55
|
+
|
|
47
56
|
`init` signs you into your workspace (email + password, or paste the sign-in link from your
|
|
48
57
|
workspace email), generates the agent's Ed25519 keypair **locally** (only the public `did:key`
|
|
49
58
|
is sent), registers the agent, mints its **floor-limited API token** (limited to the agent runtime — request authorization, poll status, report signed confirmation; it cannot administer the workspace or approve anything — and your workspace session never touches the agent's disk), and writes `.env`.
|
|
@@ -89,11 +98,9 @@ node --env-file=.env quickstart.mjs
|
|
|
89
98
|
A brand-new agent matches **no** auto-approve rule, so Decidio **routes every request to a
|
|
90
99
|
human by default** — that is the point: nothing executes without either a named policy rule or
|
|
91
100
|
a person. (Routing-by-default is distinct from a policy **block**, which refuses the action
|
|
92
|
-
outright — no human can approve a blocked request.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
named rule auto-approves amounts under $1M when the request carries the agent's signed
|
|
96
|
-
identity proof, so even this $86k example would proceed there.)
|
|
101
|
+
outright — no human can approve a blocked request. Policy `proceed` rules — named auto-approve
|
|
102
|
+
for requests carrying the agent's signed identity proof — are configured per workspace by its
|
|
103
|
+
owner.) The script prints the decision id and waits.
|
|
97
104
|
|
|
98
105
|
**5. Approve it** — in a second terminal (same directory; replace `<decisionId>` with the id
|
|
99
106
|
step 4 printed — angle brackets are placeholders, never typed literally). Approvals are a
|
|
@@ -142,6 +149,10 @@ The server caps the evidence at the weaker `agent_asserted`, and the SDK warns a
|
|
|
142
149
|
time.) Immutable seal + accruing evidence is the design, not an omission — corrections and
|
|
143
150
|
later facts supersede, they never rewrite.
|
|
144
151
|
|
|
152
|
+
One sharing note: a receipt is **not a secret, but it is not metadata-free** — it names the
|
|
153
|
+
action, amount limits, policy, agent and issuer identifiers, and the decision reason. Share it
|
|
154
|
+
with auditors and counterparties deliberately, the way you would any business record.
|
|
155
|
+
|
|
145
156
|
**7. Prove a replay is rejected:**
|
|
146
157
|
|
|
147
158
|
```bash
|
|
@@ -264,9 +275,15 @@ imports none of them. Each adapter's shipped `.d.ts` header documents its wiring
|
|
|
264
275
|
- `doctor` — config, connectivity, and what your current token can do
|
|
265
276
|
- `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)
|
|
266
277
|
- `receipt <id>` — download a sealed decision's Authority Receipt to a `.json` file
|
|
267
|
-
- `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)
|
|
278
|
+
- `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). Unknown flags are refused, never ignored.
|
|
279
|
+
- `tokens list` / `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)
|
|
280
|
+
- `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)
|
|
268
281
|
- `dev [--target URL]` — local relay for the resume webhook (operator setups)
|
|
269
282
|
|
|
283
|
+
The CLI's stream contract: **stdout carries results only** (safe to pipe — `--json` output is
|
|
284
|
+
pure JSON); errors, usage guidance, and the per-run target-host announcement go to **stderr**.
|
|
285
|
+
Exit codes: 0 success · 1 failure or honest no-op · 2 usage error.
|
|
286
|
+
|
|
270
287
|
Commands that administer your workspace (`init`, `approvals`, `receipt`) use a **session**
|
|
271
288
|
sign-in held in memory for that run. What lands on disk in `.env`: the agent's floor-limited
|
|
272
289
|
API token, its locally-generated private signing key, and a webhook secret (used only in
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, appendFileSync, chmodSync } from "node:fs";
|
|
3
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync, chmodSync, rmSync } from "node:fs";
|
|
3
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
5
6
|
import { createInterface } from "node:readline";
|
|
6
7
|
import { createRequire } from "node:module";
|
|
7
8
|
import { generateKeypair } from "./signing.js";
|
|
8
|
-
import { withApproval } from "./index.js";
|
|
9
|
+
import { withApproval, DecidioRateLimitError } from "./index.js";
|
|
9
10
|
import { verifyReceipt } from "@decidio/verify";
|
|
10
11
|
const VERSION = (() => {
|
|
11
12
|
try {
|
|
@@ -27,32 +28,37 @@ function readEnvFile() {
|
|
|
27
28
|
}
|
|
28
29
|
for (const [k, v] of Object.entries(readEnvFile())) {
|
|
29
30
|
if (k === "DECIDIO_SESSION_TOKEN") {
|
|
30
|
-
console.
|
|
31
|
+
console.error("⚠ ignoring DECIDIO_SESSION_TOKEN found in .env — session tokens belong in your shell or CI secret store, never on disk. Remove that line; export it in the shell instead.");
|
|
31
32
|
continue;
|
|
32
33
|
}
|
|
33
34
|
if (process.env[k] === undefined)
|
|
34
35
|
process.env[k] = v;
|
|
35
36
|
}
|
|
36
|
-
const
|
|
37
|
-
const API = (process.env.DECIDIO_API_URL ?? (IS_QUICKSTART ? "https://decidio-api.onrender.com" : "http://localhost:4000")).replace(/\/$/, "");
|
|
37
|
+
const API = (process.env.DECIDIO_API_URL ?? "https://decidio-api.onrender.com").replace(/\/$/, "");
|
|
38
38
|
const TOKEN = () => process.env.DECIDIO_API_TOKEN;
|
|
39
39
|
const ENV_PATH = ".env";
|
|
40
40
|
const log = (...a) => console.log(...a);
|
|
41
|
+
const err = (...a) => console.error(...a);
|
|
42
|
+
let hostAnnounced = false;
|
|
41
43
|
async function api(path, init = {}, bearer) {
|
|
44
|
+
if (!hostAnnounced) {
|
|
45
|
+
hostAnnounced = true;
|
|
46
|
+
err(`· decidio → ${API}${process.env.DECIDIO_API_URL ? "" : " (default: the hosted sandbox — set DECIDIO_API_URL to point elsewhere)"}`);
|
|
47
|
+
}
|
|
42
48
|
const auth = bearer ?? TOKEN();
|
|
43
49
|
let res;
|
|
44
50
|
try {
|
|
45
51
|
res = await fetch(API + path, { ...init, headers: { "content-type": "application/json", ...(auth ? { authorization: `Bearer ${auth}` } : {}), ...(init.headers ?? {}) } });
|
|
46
52
|
}
|
|
47
53
|
catch (e) {
|
|
48
|
-
|
|
54
|
+
err(`✘ could not reach Decidio at ${API} (${e?.cause?.code ?? e?.message ?? "network error"}).`);
|
|
49
55
|
if (API.includes("localhost")) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
err(` You're pointed at localhost — for the hosted sandbox run:`);
|
|
57
|
+
err(` export DECIDIO_API_URL=https://decidio-api.onrender.com`);
|
|
58
|
+
err(` (or put that line in .env), then re-run this command.`);
|
|
53
59
|
}
|
|
54
60
|
else {
|
|
55
|
-
|
|
61
|
+
err(` Check your network, then re-run. If the URL is wrong, set DECIDIO_API_URL and retry.`);
|
|
56
62
|
}
|
|
57
63
|
process.exit(1);
|
|
58
64
|
}
|
|
@@ -72,6 +78,14 @@ function writeSecretFile(path, body) {
|
|
|
72
78
|
chmodSync(path, 0o600);
|
|
73
79
|
}
|
|
74
80
|
catch { }
|
|
81
|
+
if (process.platform === "win32") {
|
|
82
|
+
try {
|
|
83
|
+
execFileSync("icacls", [path, "/inheritance:r", "/grant:r", `${process.env.USERNAME ?? process.env.USER ?? ""}:F`], { stdio: "ignore" });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
console.warn(`⚠ could not restrict ${path} to owner-only on this Windows volume — it keeps the directory's inherited permissions. For production, move these values to a secret manager.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
function upsertEnv(updates) {
|
|
77
91
|
const cur = readEnvFile();
|
|
@@ -103,14 +117,14 @@ function ask(question, mute = false) {
|
|
|
103
117
|
}
|
|
104
118
|
async function acquireSession(purpose) {
|
|
105
119
|
if (/^http:\/\//i.test(API) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])([:/]|$)/i.test(API)) {
|
|
106
|
-
|
|
120
|
+
err(`✘ refusing to send workspace credentials over plaintext http to ${API}.`);
|
|
107
121
|
log(` Use https (the hosted sandbox is https://decidio-api.onrender.com) or localhost for local dev.`);
|
|
108
122
|
return null;
|
|
109
123
|
}
|
|
110
124
|
if (process.env.DECIDIO_SESSION_TOKEN)
|
|
111
125
|
return process.env.DECIDIO_SESSION_TOKEN;
|
|
112
126
|
if (!process.stdin.isTTY) {
|
|
113
|
-
|
|
127
|
+
err(`✘ ${purpose} needs a Decidio workspace sign-in, and this terminal can't prompt for one.`);
|
|
114
128
|
log(` Fix: set DECIDIO_SESSION_TOKEN and re-run. To mint one:`);
|
|
115
129
|
log(` 1. Request a sign-in link from your workspace's sign-in screen (or use your welcome email).`);
|
|
116
130
|
log(` 2. POST its ?login= value — the response's "token" is your session token:`);
|
|
@@ -151,7 +165,7 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
151
165
|
const existingKey = existingEnv.DECIDIO_AGENT_KEY;
|
|
152
166
|
const sourceSystem = process.env.DECIDIO_SOURCE_SYSTEM || existingEnv.DECIDIO_SOURCE_SYSTEM;
|
|
153
167
|
if (existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId && !opts.force) {
|
|
154
|
-
|
|
168
|
+
err(`✘ this directory's .env already holds the key for a DIFFERENT agent ("${existingEnv.DECIDIO_AGENT_ID}").`);
|
|
155
169
|
log(` Registering "${agentId}" here would overwrite that key — and a private key that is`);
|
|
156
170
|
log(` overwritten is gone for good (it never left this machine). Nothing was written.`);
|
|
157
171
|
log(` Pick one:`);
|
|
@@ -179,7 +193,7 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
179
193
|
}
|
|
180
194
|
else {
|
|
181
195
|
const suggestion = `${agentId}-${randomBytes(2).toString("hex")}`;
|
|
182
|
-
|
|
196
|
+
err(`✘ "${agentId}" is already registered in this workspace, and its private key is not on this machine`);
|
|
183
197
|
log(` (keys never leave the machine that generated them — likely a previous run, or another directory).`);
|
|
184
198
|
log(` Nothing was written. Two ways forward:`);
|
|
185
199
|
log(` 1. Fresh agent (most common): npx @decidio/sdk init ${suggestion}`);
|
|
@@ -193,11 +207,11 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
193
207
|
log(`✔ registered (did ${did}) — key generated locally; only the public half went to Decidio`);
|
|
194
208
|
}
|
|
195
209
|
else {
|
|
196
|
-
|
|
210
|
+
err(`✘ registration failed (${r.status}): ${JSON.stringify(r.body)}`);
|
|
197
211
|
log(` Fix the cause above and re-run — init is safe to repeat.`);
|
|
198
212
|
return 1;
|
|
199
213
|
}
|
|
200
|
-
let agentToken, workspaceId;
|
|
214
|
+
let agentToken, workspaceId, tokenJti;
|
|
201
215
|
const mintBearer = session ?? TOKEN();
|
|
202
216
|
if (mintBearer) {
|
|
203
217
|
const expiresInDays = opts.expiresDays ?? 90;
|
|
@@ -205,10 +219,11 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
205
219
|
if (mint.status >= 200 && mint.status < 300 && mint.body?.token) {
|
|
206
220
|
agentToken = mint.body.token;
|
|
207
221
|
workspaceId = mint.body.workspaceId;
|
|
222
|
+
tokenJti = mint.body.jti;
|
|
208
223
|
log(`✔ minted the agent's API token — floor-limited: it can request authorization, poll status and report confirmation; it cannot administer the workspace or approve anything${mint.body.expiresAt ? ` (expires ${mint.body.expiresAt})` : ` (${expiresInDays} days)`}`);
|
|
209
224
|
}
|
|
210
225
|
else if (session) {
|
|
211
|
-
|
|
226
|
+
err(`✘ could not mint the agent's API token (${mint.status}): ${JSON.stringify(mint.body)}`);
|
|
212
227
|
log(` Without it the SDK has no runtime credential. Fix the cause above and re-run init — it's safe to repeat.`);
|
|
213
228
|
return 1;
|
|
214
229
|
}
|
|
@@ -228,6 +243,8 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
228
243
|
envOut.DECIDIO_API_TOKEN = agentToken;
|
|
229
244
|
if (workspaceId)
|
|
230
245
|
envOut.DECIDIO_WORKSPACE_ID = workspaceId;
|
|
246
|
+
if (tokenJti)
|
|
247
|
+
envOut.DECIDIO_TOKEN_JTI = tokenJti;
|
|
231
248
|
ensureGitignore();
|
|
232
249
|
if (opts.force && existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId) {
|
|
233
250
|
const backup = `.env.${existingEnv.DECIDIO_AGENT_ID.replace(/[^\w.-]/g, "_")}.bak`;
|
|
@@ -270,10 +287,11 @@ async function cmdDoctor() {
|
|
|
270
287
|
const env = readEnvFile();
|
|
271
288
|
let ok = true;
|
|
272
289
|
const line = (good, label) => { log(`${good ? "✔" : "✘"} ${label}`); ok = ok && good; };
|
|
273
|
-
|
|
290
|
+
log(`✔ API URL: ${API}${(process.env.DECIDIO_API_URL || env.DECIDIO_API_URL) ? "" : " (default: the hosted sandbox — set DECIDIO_API_URL to point elsewhere)"}`);
|
|
274
291
|
line(!!(process.env.DECIDIO_AGENT_ID || env.DECIDIO_AGENT_ID), `agent id: ${process.env.DECIDIO_AGENT_ID || env.DECIDIO_AGENT_ID || "(unset — run: npx @decidio/sdk init <agent-name>)"}`);
|
|
275
292
|
const secret = process.env.DECIDIO_WEBHOOK_SECRET || env.DECIDIO_WEBHOOK_SECRET;
|
|
276
|
-
|
|
293
|
+
log(secret ? "✔ webhook secret: set — operator-only transport (inactive against the hosted sandbox; the poll worker guard.worker() is the self-serve resume path)"
|
|
294
|
+
: "• webhook secret: not set — only needed for the operator resume-webhook transport; the poll worker guard.worker() is the self-serve resume path (init generates one)");
|
|
277
295
|
const signing = (process.env.DECIDIO_AGENT_KEY || env.DECIDIO_AGENT_KEY) && (process.env.DECIDIO_AGENT_DID || env.DECIDIO_AGENT_DID);
|
|
278
296
|
if (signing)
|
|
279
297
|
log("✔ signing key: set — the agent can prove its identity, making it ELIGIBLE for policy auto-approve. Whether a rule matches is your workspace policy's decision; with no matching rule every request routes to a human (deny-by-default).");
|
|
@@ -286,11 +304,13 @@ async function cmdDoctor() {
|
|
|
286
304
|
try {
|
|
287
305
|
const payload = JSON.parse(Buffer.from(tok.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
288
306
|
if (typeof payload?.exp === "number") {
|
|
289
|
-
const
|
|
290
|
-
|
|
307
|
+
const msLeft = payload.exp * 1000 - Date.now();
|
|
308
|
+
const days = Math.floor(msLeft / 86_400_000);
|
|
309
|
+
const left = msLeft < 86_400_000 ? `${Math.max(1, Math.floor(msLeft / 3_600_000))} hour(s)` : `${days} day(s)`;
|
|
310
|
+
if (msLeft < 0)
|
|
291
311
|
line(false, `agent token: EXPIRED ${-days} day(s) ago — re-run \`npx @decidio/sdk init ${env.DECIDIO_AGENT_ID ?? "<agent>"}\` to mint a fresh one.`);
|
|
292
312
|
else if (days <= 14)
|
|
293
|
-
log(`⚠ agent token: expires in ${
|
|
313
|
+
log(`⚠ agent token: expires in ${left} — mint a fresh one before it does: npx @decidio/sdk init ${env.DECIDIO_AGENT_ID ?? "<agent>"}`);
|
|
294
314
|
else
|
|
295
315
|
log(`✔ agent token: valid for another ${days} days`);
|
|
296
316
|
}
|
|
@@ -319,6 +339,16 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
|
319
339
|
const mineOnly = !!flags.mine;
|
|
320
340
|
if (sub?.startsWith("--"))
|
|
321
341
|
sub = undefined;
|
|
342
|
+
if ((sub === "approve" || sub === "reject") && !id) {
|
|
343
|
+
err(`usage: npx @decidio/sdk approvals ${sub} <decisionId> [reason]`);
|
|
344
|
+
return 2;
|
|
345
|
+
}
|
|
346
|
+
if (mineOnly && !process.env.DECIDIO_AGENT_ID) {
|
|
347
|
+
err(`✘ --mine needs an agent identity, and there is no DECIDIO_AGENT_ID in this directory's .env or your environment.`);
|
|
348
|
+
err(` Run this from your agent's directory (where init wrote .env), or drop --mine for the workspace-wide list.`);
|
|
349
|
+
return 2;
|
|
350
|
+
}
|
|
351
|
+
const decisionArg = id ?? "";
|
|
322
352
|
let bearer;
|
|
323
353
|
const probe = await api("/api/decisions/cards");
|
|
324
354
|
if (probe.status === 401 || probe.status === 403) {
|
|
@@ -328,11 +358,7 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
|
328
358
|
bearer = session;
|
|
329
359
|
}
|
|
330
360
|
if (sub === "approve" || sub === "reject") {
|
|
331
|
-
|
|
332
|
-
log(`usage: npx @decidio/sdk approvals ${sub} <decisionId> [reason]`);
|
|
333
|
-
return 2;
|
|
334
|
-
}
|
|
335
|
-
const r = await api(`/api/decisions/${encodeURIComponent(id)}/${sub === "approve" ? "approve" : "reject"}`, { method: "POST", body: JSON.stringify({ reason: reason ?? undefined }) }, bearer);
|
|
361
|
+
const r = await api(`/api/decisions/${encodeURIComponent(decisionArg)}/${sub === "approve" ? "approve" : "reject"}`, { method: "POST", body: JSON.stringify({ reason: reason ?? undefined }) }, bearer);
|
|
336
362
|
if (r.status >= 200 && r.status < 300) {
|
|
337
363
|
if (r.body?.alreadyResolved) {
|
|
338
364
|
log(`• ${id} was already resolved — nothing changed.`);
|
|
@@ -340,7 +366,7 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
|
340
366
|
log(` See its sealed record: npx @decidio/sdk receipt ${id}`);
|
|
341
367
|
return 1;
|
|
342
368
|
}
|
|
343
|
-
log(`✔ ${sub}
|
|
369
|
+
log(`✔ ${sub === "approve" ? "approved" : "rejected"} ${id}${r.body?.resumeSignal ? ` — agent signalled (${r.body.resumeSignal.ok ? "ok" : r.body.resumeSignal.error})` : ""}`);
|
|
344
370
|
if (sub === "approve")
|
|
345
371
|
log(` The agent's function runs next — a blocking call resumes now; a durable one when its worker or resume route picks this up.`);
|
|
346
372
|
else
|
|
@@ -349,30 +375,30 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
|
349
375
|
return 0;
|
|
350
376
|
}
|
|
351
377
|
if (r.status === 404) {
|
|
352
|
-
|
|
378
|
+
err(`✘ no decision "${id}" in this workspace — check the id (npx @decidio/sdk approvals lists pending ones).`);
|
|
353
379
|
return 1;
|
|
354
380
|
}
|
|
355
|
-
|
|
381
|
+
err(`✘ ${sub} failed (${r.status}): ${JSON.stringify(r.body).slice(0, 300)}`);
|
|
356
382
|
return 1;
|
|
357
383
|
}
|
|
358
384
|
const r = bearer ? await api("/api/decisions/cards", {}, bearer) : probe;
|
|
359
385
|
if (!(r.status >= 200 && r.status < 300)) {
|
|
360
|
-
|
|
386
|
+
err(`✘ could not list approvals (${r.status}): ${JSON.stringify(r.body).slice(0, 300)}`);
|
|
361
387
|
return 1;
|
|
362
388
|
}
|
|
363
389
|
const cards = r.body?.cards ?? [];
|
|
364
390
|
const pending = cards.filter((c) => c.isAgentRequest && c.status !== "sealed");
|
|
391
|
+
const myAgent = process.env.DECIDIO_AGENT_ID;
|
|
392
|
+
const isMine = (c) => !!myAgent && typeof c.title === "string" && c.title.startsWith(`${myAgent}:`);
|
|
393
|
+
const shown = mineOnly ? pending.filter(isMine) : pending;
|
|
365
394
|
if (jsonMode) {
|
|
366
|
-
log(JSON.stringify(
|
|
395
|
+
log(JSON.stringify(shown.map((c) => ({ decisionId: c.id, action: c.agentRequest?.action ?? null, amount: c.agentRequest?.amount ?? null, title: c.title ?? null })), null, 2));
|
|
367
396
|
return 0;
|
|
368
397
|
}
|
|
369
398
|
if (!pending.length) {
|
|
370
399
|
log("No pending agent approvals. Trigger one from your protected function, then re-run this.");
|
|
371
400
|
return 0;
|
|
372
401
|
}
|
|
373
|
-
const myAgent = process.env.DECIDIO_AGENT_ID;
|
|
374
|
-
const isMine = (c) => !!myAgent && typeof c.title === "string" && c.title.startsWith(`${myAgent}:`);
|
|
375
|
-
const shown = mineOnly ? pending.filter(isMine) : pending;
|
|
376
402
|
if (mineOnly && !shown.length) {
|
|
377
403
|
log(`No pending approvals for "${myAgent}" (${pending.length} pending elsewhere in this workspace — drop --mine to see them).`);
|
|
378
404
|
return 0;
|
|
@@ -388,9 +414,9 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
|
388
414
|
}
|
|
389
415
|
async function cmdReceipt(id, jsonMode = false) {
|
|
390
416
|
if (!id) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
417
|
+
err("usage: npx @decidio/sdk receipt <decisionId>");
|
|
418
|
+
err(" Downloads the decision's sealed Authority Receipt (a W3C Verifiable Credential) to a .json file.");
|
|
419
|
+
err(" Find decision ids via `npx @decidio/sdk approvals` or in your Decidio app.");
|
|
394
420
|
return 2;
|
|
395
421
|
}
|
|
396
422
|
let bearer;
|
|
@@ -403,26 +429,26 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
403
429
|
r = await api(`/api/records/${encodeURIComponent(id)}/verify`, {}, bearer);
|
|
404
430
|
}
|
|
405
431
|
if (r.status === 404) {
|
|
406
|
-
|
|
407
|
-
|
|
432
|
+
err(`✘ no sealed record for "${id}" in this workspace.`);
|
|
433
|
+
err(` Receipts exist once a decision is SEALED (approved or rejected). Still pending? npx @decidio/sdk approvals`);
|
|
408
434
|
return 1;
|
|
409
435
|
}
|
|
410
436
|
if (!(r.status >= 200 && r.status < 300) || !r.body?.credential) {
|
|
411
|
-
|
|
437
|
+
err(`✘ could not fetch the receipt (${r.status}): ${JSON.stringify(r.body).slice(0, 300)}`);
|
|
412
438
|
return 1;
|
|
413
439
|
}
|
|
414
440
|
const name = `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
|
|
415
441
|
const nextBytes = JSON.stringify(r.body.credential, null, 2) + "\n";
|
|
416
442
|
if (existsSync(name)) {
|
|
417
443
|
if (readFileSync(name, "utf8") !== nextBytes) {
|
|
418
|
-
|
|
444
|
+
err(`✘ ${name} already exists with DIFFERENT bytes — refusing to overwrite an audit artifact. Move it aside or delete it, then re-run.`);
|
|
419
445
|
return 1;
|
|
420
446
|
}
|
|
421
|
-
log(`• ${name} already exists (byte-identical — receipts are immutable); leaving it in place.`);
|
|
447
|
+
(jsonMode ? err : log)(`• ${name} already exists (byte-identical — receipts are immutable); leaving it in place.`);
|
|
422
448
|
}
|
|
423
449
|
else {
|
|
424
450
|
writeFileSync(name, nextBytes);
|
|
425
|
-
log(`✔ wrote ${name} (downloaded from your workspace — the download is online; VERIFYING it is offline)`);
|
|
451
|
+
(jsonMode ? err : log)(`✔ wrote ${name} (downloaded from your workspace — the download is online; VERIFYING it is offline)`);
|
|
426
452
|
}
|
|
427
453
|
const detail = await api(`/api/records/${encodeURIComponent(id)}`, {}, bearer);
|
|
428
454
|
const conf = detail.body?.record?.confirmation;
|
|
@@ -502,7 +528,7 @@ function chainOf(vcs, expectedIssuer) {
|
|
|
502
528
|
let prev = null;
|
|
503
529
|
let anchor = "segment";
|
|
504
530
|
for (let i = 0; i < vcs.length; i++) {
|
|
505
|
-
const r =
|
|
531
|
+
const r = verifyReceiptDoor(vcs[i], expectedIssuer);
|
|
506
532
|
if (!r.valid)
|
|
507
533
|
return { valid: false, length: i, reason: `receipt ${i} (${r.recordId ?? "?"}) invalid: ${r.reason}` };
|
|
508
534
|
const cs = vcs[i]?.credentialSubject ?? {};
|
|
@@ -514,22 +540,61 @@ function chainOf(vcs, expectedIssuer) {
|
|
|
514
540
|
}
|
|
515
541
|
return { valid: true, length: vcs.length, anchor };
|
|
516
542
|
}
|
|
543
|
+
function verifyReceiptDoor(vc, expectedIssuer) {
|
|
544
|
+
const res = verifyReceipt(vc, expectedIssuer);
|
|
545
|
+
if (!res.valid && res.reason === "issuer DID is not the expected issuer") {
|
|
546
|
+
const cs = vc?.credentialSubject ?? {};
|
|
547
|
+
const derive = (canonical, prevHash) => createHash("sha256").update((prevHash ?? "") + canonical).digest("hex");
|
|
548
|
+
const bound = typeof cs.recordCanonical === "string" && typeof cs.contentHash === "string"
|
|
549
|
+
&& derive(cs.recordCanonical, typeof cs.prevHash === "string" ? cs.prevHash : null) === cs.contentHash;
|
|
550
|
+
return { ...res, contentBound: bound, recordId: typeof cs.recordId === "string" ? cs.recordId : res.recordId, reason: "issuer DID is not the expected issuer (the receipt itself may be internally consistent — it was just not issued by who you pinned)" };
|
|
551
|
+
}
|
|
552
|
+
return res;
|
|
553
|
+
}
|
|
517
554
|
function cmdVerify(argsIn) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
555
|
+
let expectedIssuer;
|
|
556
|
+
const files = [];
|
|
557
|
+
for (let i = 0; i < argsIn.length; i++) {
|
|
558
|
+
const a = argsIn[i];
|
|
559
|
+
if (a === "--issuer") {
|
|
560
|
+
const v = argsIn[i + 1];
|
|
561
|
+
if (v === undefined || v.startsWith("-")) {
|
|
562
|
+
err("✘ --issuer needs a did:key value.");
|
|
563
|
+
return 2;
|
|
564
|
+
}
|
|
565
|
+
expectedIssuer = v;
|
|
566
|
+
i++;
|
|
567
|
+
}
|
|
568
|
+
else if (a.startsWith("--issuer=")) {
|
|
569
|
+
const v = a.slice("--issuer=".length);
|
|
570
|
+
if (!v) {
|
|
571
|
+
err("✘ --issuer needs a did:key value.");
|
|
572
|
+
return 2;
|
|
573
|
+
}
|
|
574
|
+
expectedIssuer = v;
|
|
575
|
+
}
|
|
576
|
+
else if (!a.startsWith("-")) {
|
|
577
|
+
files.push(a);
|
|
578
|
+
}
|
|
522
579
|
}
|
|
523
|
-
const expectedIssuer = issIdx >= 0 ? argsIn[issIdx + 1] : undefined;
|
|
524
|
-
const files = argsIn.filter((a, i) => !a.startsWith("--") && !(issIdx >= 0 && i === issIdx + 1));
|
|
525
580
|
if (!files.length) {
|
|
526
|
-
|
|
527
|
-
|
|
581
|
+
err("usage: npx @decidio/sdk verify [--issuer <did:key>] <receipt.json> [more.json ...]");
|
|
582
|
+
err(" Verifies Authority Receipts OFFLINE — signature, content binding, and (for several files) chain order.");
|
|
528
583
|
return 2;
|
|
529
584
|
}
|
|
585
|
+
const texts = [];
|
|
586
|
+
for (const f of files) {
|
|
587
|
+
try {
|
|
588
|
+
texts.push(readFileSync(f, "utf8"));
|
|
589
|
+
}
|
|
590
|
+
catch (e) {
|
|
591
|
+
err(`✘ cannot read ${f}: ${e?.code === "ENOENT" ? "no such file" : e?.message ?? e} (nothing was verified)`);
|
|
592
|
+
return 1;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
530
595
|
let vcs;
|
|
531
596
|
try {
|
|
532
|
-
vcs = files.map((f) => parseReceiptStrict(
|
|
597
|
+
vcs = files.map((f, i) => parseReceiptStrict(texts[i], f));
|
|
533
598
|
}
|
|
534
599
|
catch (e) {
|
|
535
600
|
log(`✘ INVALID ${String(e?.message ?? e)}`);
|
|
@@ -537,7 +602,7 @@ function cmdVerify(argsIn) {
|
|
|
537
602
|
}
|
|
538
603
|
let allOk = true;
|
|
539
604
|
vcs.forEach((vc, i) => {
|
|
540
|
-
const res =
|
|
605
|
+
const res = verifyReceiptDoor(vc, expectedIssuer);
|
|
541
606
|
allOk = allOk && res.valid;
|
|
542
607
|
log(`${res.valid ? "✔ VALID " : "✘ INVALID"} ${files[i]}`);
|
|
543
608
|
log(` record: ${res.recordId ?? "?"} issuer: ${res.issuer}${expectedIssuer ? "" : " (unpinned — pass --issuer to bind to your workspace's DID)"}`);
|
|
@@ -550,11 +615,15 @@ function cmdVerify(argsIn) {
|
|
|
550
615
|
}
|
|
551
616
|
return allOk ? 0 : 1;
|
|
552
617
|
}
|
|
553
|
-
async function cmdQuickstart() {
|
|
618
|
+
async function cmdQuickstart(opts = {}) {
|
|
554
619
|
const t0 = Date.now();
|
|
555
620
|
log("Decidio quickstart — gate an action, route it to a human, execute on approval, hold the proof.");
|
|
556
621
|
log(` Target: ${API}${process.env.DECIDIO_API_URL ? "" : " (the hosted synthetic sandbox — set DECIDIO_API_URL to use your own)"}`);
|
|
557
622
|
log("");
|
|
623
|
+
log(" This tour will: register one synthetic agent · mint a 7-DAY floor-limited token ·");
|
|
624
|
+
log(" write .env (or .env.quickstart) + .gitignore entries · create one synthetic decision");
|
|
625
|
+
log(" and its sealed record (records are permanent by design). Cleanup offer at the end.");
|
|
626
|
+
log("");
|
|
558
627
|
const session = await acquireSession("The quickstart");
|
|
559
628
|
if (!session)
|
|
560
629
|
return 1;
|
|
@@ -563,30 +632,40 @@ async function cmdQuickstart() {
|
|
|
563
632
|
const { did, privateKeyBase64 } = generateKeypair();
|
|
564
633
|
const reg = await api("/api/agents/register", { method: "POST", body: JSON.stringify({ agentId, did }) }, session);
|
|
565
634
|
if (!(reg.status >= 200 && reg.status < 300)) {
|
|
566
|
-
|
|
635
|
+
err(`✘ registration failed (${reg.status}): ${JSON.stringify(reg.body)}`);
|
|
567
636
|
return 1;
|
|
568
637
|
}
|
|
569
638
|
const mint = await api("/api/workspace/agent-token", { method: "POST", body: JSON.stringify({ agentId, label: "quickstart", expiresInDays: 7 }) }, session);
|
|
570
639
|
if (!(mint.status >= 200 && mint.status < 300) || !mint.body?.token) {
|
|
571
|
-
|
|
640
|
+
err(`✘ token mint failed (${mint.status}): ${JSON.stringify(mint.body)}`);
|
|
572
641
|
return 1;
|
|
573
642
|
}
|
|
574
643
|
const envOut = {
|
|
575
644
|
DECIDIO_API_URL: API, DECIDIO_AGENT_ID: agentId, DECIDIO_AGENT_DID: reg.body?.did ?? did,
|
|
576
645
|
DECIDIO_AGENT_KEY: privateKeyBase64, DECIDIO_API_TOKEN: mint.body.token,
|
|
577
|
-
DECIDIO_WORKSPACE_ID: mint.body.workspaceId ?? "",
|
|
646
|
+
DECIDIO_WORKSPACE_ID: mint.body.workspaceId ?? "",
|
|
647
|
+
...(mint.body.jti ? { DECIDIO_TOKEN_JTI: mint.body.jti } : {}),
|
|
578
648
|
};
|
|
579
649
|
let envFile = ENV_PATH;
|
|
580
650
|
if (readEnvFile().DECIDIO_AGENT_KEY) {
|
|
581
651
|
envFile = ".env.quickstart";
|
|
582
|
-
|
|
652
|
+
ensureGitignore();
|
|
653
|
+
if (existsSync(envFile)) {
|
|
654
|
+
const prev = readFileSync(envFile, "utf8");
|
|
655
|
+
const prevId = prev.match(/^DECIDIO_AGENT_ID=(.+)$/m)?.[1];
|
|
656
|
+
if (prevId && prevId !== agentId) {
|
|
657
|
+
writeSecretFile(".env.quickstart.bak", prev);
|
|
658
|
+
log(` ✔ previous tour agent (${prevId}) backed up to .env.quickstart.bak — its key would otherwise be unrecoverable`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
writeSecretFile(envFile, Object.entries(envOut).map(([k, v]) => `${k}=${v}`).join("\n") + "\n");
|
|
583
662
|
log(` ✔ this directory already has an agent in .env — tour credentials went to ${envFile} (your agent is untouched)`);
|
|
584
663
|
}
|
|
585
664
|
else {
|
|
665
|
+
ensureGitignore();
|
|
586
666
|
upsertEnv(envOut);
|
|
587
667
|
}
|
|
588
|
-
|
|
589
|
-
log(` ✔ key generated locally (public half registered) · floor-limited token minted · ${envFile} written`);
|
|
668
|
+
log(` ✔ key generated locally (public half registered) · 7-day floor-limited token minted${mint.body.expiresAt ? ` (expires ${mint.body.expiresAt})` : ""} · ${envFile} written`);
|
|
590
669
|
log("");
|
|
591
670
|
log(`② Gating a demo action — the tour runs this (blocking mode: watch it live):`);
|
|
592
671
|
log("");
|
|
@@ -603,26 +682,50 @@ async function cmdQuickstart() {
|
|
|
603
682
|
};
|
|
604
683
|
const payInvoice = withApproval(async (inv) => ({ paid: true, id: inv.id }), (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }), cfg);
|
|
605
684
|
const running = payInvoice({ id: "INV-2026-001", amount: 86_000 });
|
|
606
|
-
|
|
685
|
+
let earlyError;
|
|
686
|
+
running.catch((e) => { earlyError = e; });
|
|
607
687
|
const waitStart = Date.now();
|
|
608
|
-
while (!decisionId && Date.now() - waitStart < 30_000)
|
|
688
|
+
while (!decisionId && !earlyError && Date.now() - waitStart < 30_000)
|
|
609
689
|
await new Promise((r) => setTimeout(r, 250));
|
|
690
|
+
if (!decisionId && earlyError instanceof DecidioRateLimitError) {
|
|
691
|
+
err(`✘ this workspace's governed-action allowance is used up${earlyError.limit != null ? ` (${earlyError.limit} actions/hour on lab workspaces)` : ""}.`);
|
|
692
|
+
err(` Nothing was routed or executed — the cap is the sandbox protecting itself, not a fault${earlyError.resetAt ? `. It resets ${earlyError.resetAt}` : ""}.`);
|
|
693
|
+
err(` Re-run the tour after the reset (or talk to us about a production workspace).`);
|
|
694
|
+
return 1;
|
|
695
|
+
}
|
|
696
|
+
if (!decisionId && earlyError) {
|
|
697
|
+
err(`✘ the gate refused before routing: ${earlyError?.message ?? earlyError}`);
|
|
698
|
+
return 1;
|
|
699
|
+
}
|
|
610
700
|
if (!decisionId) {
|
|
611
|
-
|
|
701
|
+
err("✘ the gate did not route within 30s — check connectivity (npx @decidio/sdk doctor) and re-run.");
|
|
612
702
|
return 1;
|
|
613
703
|
}
|
|
614
704
|
log(` ✔ $86,000 payInvoice ROUTED to a human (decision ${decisionId}) — this agent has no auto-approve rule,`);
|
|
615
705
|
log(` so deny-by-default sends everything to a person. Nothing executes until someone decides.`);
|
|
616
706
|
log("");
|
|
617
|
-
|
|
707
|
+
const interactive = process.stdin.isTTY;
|
|
708
|
+
if (interactive) {
|
|
618
709
|
await ask(`③ YOU are the approver. Press Enter to approve it here (or approve it in your Decidio queue first, then Enter): `);
|
|
619
710
|
}
|
|
711
|
+
else if (opts.autoApproveDemo) {
|
|
712
|
+
log("③ --auto-approve-demo: approving with your session credential. This is AUTOMATED authority");
|
|
713
|
+
log(" (a flag you passed, not a human decision in the moment) — fine for CI, not the real story.");
|
|
714
|
+
}
|
|
620
715
|
else {
|
|
621
|
-
log("③
|
|
716
|
+
log("③ This terminal can't put a human in the loop, and an approval IS a human act — the tour");
|
|
717
|
+
log(" stops here rather than approve its own request. The decision is routed and waiting:");
|
|
718
|
+
log("");
|
|
719
|
+
log(` npx @decidio/sdk approvals approve ${decisionId} (or approve it in your Decidio app)`);
|
|
720
|
+
log("");
|
|
721
|
+
log(" Then re-run the quickstart interactively to watch the whole loop — or, for CI, re-run");
|
|
722
|
+
log(" with --auto-approve-demo to let the tour approve itself under that explicit label.");
|
|
723
|
+
log(` Tidy up this tour's agent + token any time: npx @decidio/sdk quickstart cleanup`);
|
|
724
|
+
process.exit(0);
|
|
622
725
|
}
|
|
623
|
-
const ap = await api(`/api/decisions/${encodeURIComponent(decisionId)}/approve`, { method: "POST", body: JSON.stringify({ reason: "quickstart" }) }, session);
|
|
726
|
+
const ap = await api(`/api/decisions/${encodeURIComponent(decisionId)}/approve`, { method: "POST", body: JSON.stringify({ reason: opts.autoApproveDemo && !interactive ? "quickstart --auto-approve-demo (automated)" : "quickstart" }) }, session);
|
|
624
727
|
if (!(ap.status >= 200 && ap.status < 300)) {
|
|
625
|
-
|
|
728
|
+
err(`✘ approve failed (${ap.status}): ${JSON.stringify(ap.body)}`);
|
|
626
729
|
return 1;
|
|
627
730
|
}
|
|
628
731
|
log(` ✔ approved${ap.body?.alreadyResolved ? " (it was already approved from your app — even better)" : ""}`);
|
|
@@ -631,7 +734,7 @@ async function cmdQuickstart() {
|
|
|
631
734
|
result = await running;
|
|
632
735
|
}
|
|
633
736
|
catch (e) {
|
|
634
|
-
|
|
737
|
+
err(`✘ the gated call did not complete: ${e?.message ?? e}`);
|
|
635
738
|
return 1;
|
|
636
739
|
}
|
|
637
740
|
log(` ✔ your function executed AFTER approval: ${JSON.stringify(result)}`);
|
|
@@ -639,7 +742,7 @@ async function cmdQuickstart() {
|
|
|
639
742
|
log("④ Downloading the sealed Authority Receipt and verifying it OFFLINE…");
|
|
640
743
|
const rec = await api(`/api/records/${encodeURIComponent(decisionId)}/verify`, {}, session);
|
|
641
744
|
if (!(rec.status >= 200 && rec.status < 300) || !rec.body?.credential) {
|
|
642
|
-
|
|
745
|
+
err(`✘ receipt fetch failed (${rec.status})`);
|
|
643
746
|
return 1;
|
|
644
747
|
}
|
|
645
748
|
const fname = `decidio-receipt-${decisionId.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
|
|
@@ -663,8 +766,129 @@ async function cmdQuickstart() {
|
|
|
663
766
|
log(` // production is DURABLE mode (default) + one worker: guard.worker(); // README has the full pattern`);
|
|
664
767
|
log("");
|
|
665
768
|
log(`Your workspace + queue: ${API.replace("decidio-api.onrender.com", "app.decidioai.com")} · Next: the README's 7-step quickstart (durable, workers, adapters).`);
|
|
769
|
+
let doCleanup = false;
|
|
770
|
+
if (interactive) {
|
|
771
|
+
const answer = await ask(`\nKeep this agent for development? [Enter = keep · c + Enter = clean up now]: `);
|
|
772
|
+
doCleanup = answer.trim().toLowerCase() === "c";
|
|
773
|
+
if (!doCleanup)
|
|
774
|
+
log(` Kept. Tidy up any time: npx @decidio/sdk quickstart cleanup`);
|
|
775
|
+
}
|
|
776
|
+
else if (opts.autoApproveDemo && !opts.keepAgent) {
|
|
777
|
+
doCleanup = true;
|
|
778
|
+
log(`\nAutomated run: cleaning the tour up (pass --keep-agent to keep the agent + token).`);
|
|
779
|
+
}
|
|
780
|
+
if (doCleanup)
|
|
781
|
+
await quickstartCleanup(session, envFile);
|
|
666
782
|
return v.valid ? 0 : 1;
|
|
667
783
|
}
|
|
784
|
+
async function quickstartCleanup(session, envFile) {
|
|
785
|
+
const env = (() => {
|
|
786
|
+
const out = {};
|
|
787
|
+
if (existsSync(envFile))
|
|
788
|
+
for (const line of readFileSync(envFile, "utf8").split(/\r?\n/)) {
|
|
789
|
+
const m = line.match(/^\s*([A-Z_]+)=(.*)$/);
|
|
790
|
+
if (m)
|
|
791
|
+
out[m[1]] = m[2];
|
|
792
|
+
}
|
|
793
|
+
return out;
|
|
794
|
+
})();
|
|
795
|
+
if (env.DECIDIO_TOKEN_JTI) {
|
|
796
|
+
const rev = await api(`/api/workspace/agent-token/${encodeURIComponent(env.DECIDIO_TOKEN_JTI)}`, { method: "DELETE" }, session);
|
|
797
|
+
if (rev.status >= 200 && rev.status < 300)
|
|
798
|
+
log(` ✔ token revoked (jti ${env.DECIDIO_TOKEN_JTI})`);
|
|
799
|
+
else
|
|
800
|
+
log(` ⚠ token revoke returned ${rev.status} — it still expires on its own within 7 days. List tokens: npx @decidio/sdk tokens list`);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
log(` ⚠ no DECIDIO_TOKEN_JTI in ${envFile} (older tour) — revoke by hand: npx @decidio/sdk tokens list, then tokens revoke <jti>. It expires on its own within 7 days.`);
|
|
804
|
+
}
|
|
805
|
+
if (existsSync(envFile)) {
|
|
806
|
+
const TOUR_KEYS = new Set(["DECIDIO_API_URL", "DECIDIO_AGENT_ID", "DECIDIO_AGENT_DID", "DECIDIO_AGENT_KEY", "DECIDIO_API_TOKEN", "DECIDIO_WORKSPACE_ID", "DECIDIO_TOKEN_JTI"]);
|
|
807
|
+
const kept = readFileSync(envFile, "utf8").split(/\r?\n/).filter((line) => {
|
|
808
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
809
|
+
return m ? !TOUR_KEYS.has(m[1]) : line.trim() !== "";
|
|
810
|
+
});
|
|
811
|
+
if (kept.length === 0) {
|
|
812
|
+
rmSync(envFile);
|
|
813
|
+
log(` ✔ ${envFile} deleted (the agent's local key is gone — the registered agent is now inert)`);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
writeSecretFile(envFile, kept.join("\n") + "\n");
|
|
817
|
+
log(` ✔ tour credentials removed from ${envFile} — its ${kept.length} other line${kept.length === 1 ? "" : "s"} untouched (the agent's local key is gone — the registered agent is now inert)`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const tourAgent = env.DECIDIO_AGENT_ID;
|
|
821
|
+
if (tourAgent) {
|
|
822
|
+
const cards = await api("/api/decisions/cards", {}, session);
|
|
823
|
+
const stranded = (cards.body?.cards ?? []).filter((c) => c.isAgentRequest && c.status !== "sealed" && typeof c.title === "string" && c.title.startsWith(`${tourAgent}:`));
|
|
824
|
+
for (const c of stranded) {
|
|
825
|
+
log(` ⚠ this tour's decision ${c.id} is still PENDING in your workspace queue — clear it with:`);
|
|
826
|
+
log(` npx @decidio/sdk approvals reject ${c.id} "quickstart cleanup"`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
log(` • What remains, by design: the sealed decision record (records are permanent — that is the product)`);
|
|
830
|
+
log(` and the agent's registration row (inert without key or token; server-side deregistration is on the roadmap).`);
|
|
831
|
+
return 0;
|
|
832
|
+
}
|
|
833
|
+
async function cmdTokens(sub, jti) {
|
|
834
|
+
if (sub !== "list" && sub !== "revoke") {
|
|
835
|
+
err("usage: npx @decidio/sdk tokens list");
|
|
836
|
+
err(" npx @decidio/sdk tokens revoke <jti>");
|
|
837
|
+
err(" Agent tokens are shown by jti (their revocation id) — the token VALUE is never shown again after minting.");
|
|
838
|
+
return 2;
|
|
839
|
+
}
|
|
840
|
+
if (sub === "revoke" && !jti) {
|
|
841
|
+
err("usage: npx @decidio/sdk tokens revoke <jti> (find the jti with: npx @decidio/sdk tokens list)");
|
|
842
|
+
return 2;
|
|
843
|
+
}
|
|
844
|
+
let bearer;
|
|
845
|
+
let r = await api("/api/workspace/agent-token");
|
|
846
|
+
if (r.status === 401 || r.status === 403) {
|
|
847
|
+
const session = await acquireSession("Managing tokens");
|
|
848
|
+
if (!session)
|
|
849
|
+
return 1;
|
|
850
|
+
bearer = session;
|
|
851
|
+
if (sub === "list")
|
|
852
|
+
r = await api("/api/workspace/agent-token", {}, bearer);
|
|
853
|
+
}
|
|
854
|
+
if (sub === "revoke") {
|
|
855
|
+
const rev = await api(`/api/workspace/agent-token/${encodeURIComponent(jti)}`, { method: "DELETE" }, bearer);
|
|
856
|
+
if (rev.status >= 200 && rev.status < 300) {
|
|
857
|
+
if (rev.body?.changed === false) {
|
|
858
|
+
log(`• ${jti} was already revoked — nothing changed.`);
|
|
859
|
+
log(` Revocation is permanent; there is no re-revoke. See it: npx @decidio/sdk tokens list`);
|
|
860
|
+
return 1;
|
|
861
|
+
}
|
|
862
|
+
log(`✔ revoked ${jti} — the token is dead now (its agent stays registered; mint a new one with init).`);
|
|
863
|
+
return 0;
|
|
864
|
+
}
|
|
865
|
+
if (rev.status === 404) {
|
|
866
|
+
err(`✘ no token with jti "${jti}" — list them: npx @decidio/sdk tokens list`);
|
|
867
|
+
return 1;
|
|
868
|
+
}
|
|
869
|
+
err(`✘ revoke failed (${rev.status}): ${JSON.stringify(rev.body).slice(0, 300)}`);
|
|
870
|
+
return 1;
|
|
871
|
+
}
|
|
872
|
+
if (!(r.status >= 200 && r.status < 300)) {
|
|
873
|
+
err(`✘ could not list tokens (${r.status}): ${JSON.stringify(r.body).slice(0, 300)}`);
|
|
874
|
+
return 1;
|
|
875
|
+
}
|
|
876
|
+
const tokens = r.body?.tokens ?? r.body ?? [];
|
|
877
|
+
if (!Array.isArray(tokens) || !tokens.length) {
|
|
878
|
+
log("No agent tokens in this workspace.");
|
|
879
|
+
return 0;
|
|
880
|
+
}
|
|
881
|
+
const status = (t) => t.revokedAt ? `REVOKED ${String(t.revokedAt).slice(0, 10)}`
|
|
882
|
+
: (t.expiresAt && Date.parse(t.expiresAt) < Date.now()) ? "expired" : "active";
|
|
883
|
+
log(`Agent tokens (${tokens.length}):`);
|
|
884
|
+
for (const t of tokens)
|
|
885
|
+
log(` ${t.jti} ${status(t)} agent=${t.agentId ?? "?"} label=${t.label ?? ""} expires=${t.expiresAt ?? "?"}`);
|
|
886
|
+
const al = r.body?.allowance;
|
|
887
|
+
if (al?.limit != null)
|
|
888
|
+
log(`\nGoverned-action allowance: ${al.remaining}/${al.limit} remaining this hour${al.resetAt ? ` (resets ${al.resetAt})` : ""}.`);
|
|
889
|
+
log(`\nRevoke one: npx @decidio/sdk tokens revoke <jti>`);
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
668
892
|
function cmdDev(target, port) {
|
|
669
893
|
log(`decidio dev relay on http://localhost:${port}/decidio/resume → forwarding to ${target}`);
|
|
670
894
|
createServer((req, res) => {
|
|
@@ -701,6 +925,13 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
701
925
|
in, or writes files unless you ask it to.)
|
|
702
926
|
|
|
703
927
|
npx @decidio/sdk quickstart the guided two-minute tour: gate → approve → execute → proof
|
|
928
|
+
(interactive: YOUR Enter is the approval. Headless it stops
|
|
929
|
+
at the routed decision; --auto-approve-demo for CI, labeled
|
|
930
|
+
automated; ends with a keep-or-cleanup choice)
|
|
931
|
+
npx @decidio/sdk quickstart cleanup revoke the tour's token + delete its local credentials
|
|
932
|
+
npx @decidio/sdk tokens list agent tokens with status (active/expired/REVOKED) by jti —
|
|
933
|
+
token values are never re-shown
|
|
934
|
+
npx @decidio/sdk tokens revoke <jti> kill a token now instead of waiting out its lifetime
|
|
704
935
|
npx @decidio/sdk init [agentId] sign in, register the agent, mint its API token, write .env
|
|
705
936
|
--unique append a random suffix (collision-proof samples)
|
|
706
937
|
--force replace a different agent already in this .env
|
|
@@ -715,12 +946,15 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
715
946
|
npx @decidio/sdk dev [--target URL] [--port N] local relay for the resume webhook
|
|
716
947
|
npx @decidio/sdk --version print the CLI version
|
|
717
948
|
|
|
949
|
+
Every command talks to the hosted sandbox (https://decidio-api.onrender.com) unless
|
|
950
|
+
DECIDIO_API_URL says otherwise; the target host is announced on stderr each run.
|
|
718
951
|
Exit codes: 0 success · 1 failure or honest no-op (e.g. replay refused) · 2 usage error.
|
|
952
|
+
Errors and usage go to stderr; stdout carries results only (safe to pipe).
|
|
719
953
|
Docs: the README that shipped with this package (quickstart, durable resume, adapters).`;
|
|
720
954
|
async function main(argv) {
|
|
721
955
|
const [cmd, a, b, ...rest] = argv.slice(2);
|
|
722
956
|
const wantsHelp = (s) => s === "help" || s === "--help" || s === "-h";
|
|
723
|
-
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
957
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "-V" || cmd === "version") {
|
|
724
958
|
log(VERSION);
|
|
725
959
|
return 0;
|
|
726
960
|
}
|
|
@@ -732,15 +966,80 @@ async function main(argv) {
|
|
|
732
966
|
log(USAGE);
|
|
733
967
|
return 0;
|
|
734
968
|
}
|
|
969
|
+
const argsIn = argv.slice(3);
|
|
970
|
+
const KNOWN = {
|
|
971
|
+
quickstart: { flags: ["--auto-approve-demo", "--keep-agent"] },
|
|
972
|
+
init: { flags: ["--unique", "--force"], valued: ["--expires-days"] },
|
|
973
|
+
doctor: { flags: [] },
|
|
974
|
+
approvals: { flags: ["--json", "--mine"] },
|
|
975
|
+
receipt: { flags: ["--json"] },
|
|
976
|
+
verify: { flags: [], valued: ["--issuer"] },
|
|
977
|
+
tokens: { flags: [] },
|
|
978
|
+
dev: { flags: [], valued: ["--target", "--port"] },
|
|
979
|
+
};
|
|
980
|
+
const spec = KNOWN[cmd];
|
|
981
|
+
if (spec) {
|
|
982
|
+
const valued = spec.valued ?? [];
|
|
983
|
+
const positionals = [];
|
|
984
|
+
for (let i = 0; i < argsIn.length; i++) {
|
|
985
|
+
const a = argsIn[i];
|
|
986
|
+
if (!a.startsWith("--")) {
|
|
987
|
+
positionals.push(a);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (valued.includes(a)) {
|
|
991
|
+
i++;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (valued.some((v) => a.startsWith(v + "=")))
|
|
995
|
+
continue;
|
|
996
|
+
if (!spec.flags.includes(a)) {
|
|
997
|
+
err(`✘ unknown option "${a}" for \`${cmd}\` — refusing to guess (a flag you meant to matter must never be silently ignored).\n\n${USAGE}`);
|
|
998
|
+
return 2;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const MAX_POS = { quickstart: 1, init: 1, doctor: 0, receipt: 1, tokens: 2, dev: 0 };
|
|
1002
|
+
const exemptTail = (cmd === "approvals" && (positionals[0] === "approve" || positionals[0] === "reject")) || cmd === "verify";
|
|
1003
|
+
if (!exemptTail && cmd === "approvals" && positionals.length > 0) {
|
|
1004
|
+
err(`✘ unknown approvals subcommand "${positionals[0]}" — use approve or reject (or no subcommand to list).`);
|
|
1005
|
+
return 2;
|
|
1006
|
+
}
|
|
1007
|
+
if (!exemptTail && MAX_POS[cmd] !== undefined && positionals.length > MAX_POS[cmd]) {
|
|
1008
|
+
err(`✘ unexpected argument "${positionals[MAX_POS[cmd]]}" for \`${cmd}\` — refusing to guess (an argument you meant to matter must never be silently ignored).\n\n${USAGE}`);
|
|
1009
|
+
return 2;
|
|
1010
|
+
}
|
|
1011
|
+
if (cmd === "quickstart" && positionals.length === 1 && positionals[0] !== "cleanup") {
|
|
1012
|
+
err(`✘ unknown quickstart subcommand "${positionals[0]}" — the only one is: npx @decidio/sdk quickstart cleanup`);
|
|
1013
|
+
return 2;
|
|
1014
|
+
}
|
|
1015
|
+
if (cmd === "tokens" && positionals[0] === "list" && positionals.length > 1) {
|
|
1016
|
+
err(`✘ unexpected argument "${positionals[1]}" for \`tokens list\` — it takes none (an argument you meant to matter must never be silently ignored).`);
|
|
1017
|
+
return 2;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
735
1020
|
switch (cmd) {
|
|
736
|
-
case "quickstart":
|
|
1021
|
+
case "quickstart": {
|
|
1022
|
+
if (argsIn.includes("cleanup")) {
|
|
1023
|
+
const envFile = existsSync(".env.quickstart") ? ".env.quickstart"
|
|
1024
|
+
: (readEnvFile().DECIDIO_AGENT_ID ?? "").startsWith("quickstart-") ? ENV_PATH : undefined;
|
|
1025
|
+
if (!envFile) {
|
|
1026
|
+
log("Nothing to clean up here: no .env.quickstart, and .env doesn't belong to a quickstart agent.");
|
|
1027
|
+
return 0;
|
|
1028
|
+
}
|
|
1029
|
+
const session = await acquireSession("Cleaning the quickstart up");
|
|
1030
|
+
if (!session)
|
|
1031
|
+
return 1;
|
|
1032
|
+
return quickstartCleanup(session, envFile);
|
|
1033
|
+
}
|
|
1034
|
+
return cmdQuickstart({ autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent") });
|
|
1035
|
+
}
|
|
737
1036
|
case "init": {
|
|
738
|
-
const argsIn = argv.slice(3);
|
|
739
1037
|
const unique = argsIn.includes("--unique");
|
|
740
1038
|
const expIdx = argsIn.indexOf("--expires-days");
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
1039
|
+
const expEq = argsIn.find((x) => x.startsWith("--expires-days="));
|
|
1040
|
+
const expRaw = expIdx >= 0 ? argsIn[expIdx + 1] : expEq ? expEq.slice("--expires-days=".length) : undefined;
|
|
1041
|
+
if ((expIdx >= 0 || expEq) && (!expRaw || !/^\d+$/.test(expRaw) || Number(expRaw) < 1)) {
|
|
1042
|
+
err("✘ --expires-days needs a positive whole number of days (e.g. --expires-days 30).");
|
|
744
1043
|
return 2;
|
|
745
1044
|
}
|
|
746
1045
|
const nameArg = argsIn.find((x, i) => !x.startsWith("-") && !(expIdx >= 0 && i === expIdx + 1));
|
|
@@ -751,25 +1050,38 @@ async function main(argv) {
|
|
|
751
1050
|
}
|
|
752
1051
|
case "doctor": return cmdDoctor();
|
|
753
1052
|
case "approvals": {
|
|
754
|
-
const argsIn = argv.slice(3);
|
|
755
1053
|
const positional = argsIn.filter((x) => !x.startsWith("-"));
|
|
756
1054
|
return cmdApprovals(positional[0], positional[1], positional.slice(2).join(" ") || undefined, { json: argsIn.includes("--json"), mine: argsIn.includes("--mine") });
|
|
757
1055
|
}
|
|
758
|
-
case "receipt":
|
|
759
|
-
|
|
760
|
-
|
|
1056
|
+
case "receipt": return cmdReceipt(argsIn.find((x) => !x.startsWith("-")), argsIn.includes("--json"));
|
|
1057
|
+
case "verify": return cmdVerify(argsIn);
|
|
1058
|
+
case "tokens": {
|
|
1059
|
+
const positional = argsIn.filter((x) => !x.startsWith("-"));
|
|
1060
|
+
return cmdTokens(positional[0], positional[1]);
|
|
761
1061
|
}
|
|
762
|
-
case "verify": return cmdVerify(argv.slice(3));
|
|
763
1062
|
case "dev": {
|
|
764
|
-
const
|
|
765
|
-
|
|
1063
|
+
const flag = (name) => {
|
|
1064
|
+
const i = argsIn.indexOf(name);
|
|
1065
|
+
if (i >= 0)
|
|
1066
|
+
return argsIn[i + 1];
|
|
1067
|
+
const eq = argsIn.find((x) => x.startsWith(name + "="));
|
|
1068
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
1069
|
+
};
|
|
766
1070
|
const target = flag("--target") || process.env.DECIDIO_AGENT_RESUME_URL || "http://localhost:4100/decidio/resume";
|
|
767
|
-
const
|
|
768
|
-
|
|
1071
|
+
const portRaw = flag("--port") || process.env.DECIDIO_DEV_PORT || "4099";
|
|
1072
|
+
if (!/^\d+$/.test(portRaw) || Number(portRaw) < 1 || Number(portRaw) > 65535) {
|
|
1073
|
+
err(`✘ --port needs a number between 1 and 65535 (got "${portRaw}").`);
|
|
1074
|
+
return 2;
|
|
1075
|
+
}
|
|
1076
|
+
if (!/^https?:\/\//.test(target)) {
|
|
1077
|
+
err(`✘ --target needs an http(s) URL to forward the webhook to (got "${target}").`);
|
|
1078
|
+
return 2;
|
|
1079
|
+
}
|
|
1080
|
+
return cmdDev(target, Number(portRaw));
|
|
769
1081
|
}
|
|
770
1082
|
default:
|
|
771
|
-
|
|
772
|
-
return
|
|
1083
|
+
err(`✘ unknown command "${cmd}"\n\n${USAGE}`);
|
|
1084
|
+
return 2;
|
|
773
1085
|
}
|
|
774
1086
|
}
|
|
775
1087
|
main(process.argv).then((code) => { if (code !== 0)
|
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ export declare class DecidioTimeoutError extends Error {
|
|
|
45
45
|
decisionId: string;
|
|
46
46
|
constructor(decisionId: string);
|
|
47
47
|
}
|
|
48
|
+
export declare class DecidioRateLimitError extends Error {
|
|
49
|
+
limit: number | undefined;
|
|
50
|
+
remaining: number | undefined;
|
|
51
|
+
resetAt: string | undefined;
|
|
52
|
+
constructor(limit: number | undefined, remaining: number | undefined, resetAt: string | undefined, serverMessage?: string);
|
|
53
|
+
}
|
|
48
54
|
export declare class DecidioNetworkError extends Error {
|
|
49
55
|
endpoint: string;
|
|
50
56
|
op: string;
|
package/dist/index.js
CHANGED
|
@@ -32,12 +32,28 @@ export class DecidioTimeoutError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
35
|
+
export class DecidioRateLimitError extends Error {
|
|
36
|
+
limit;
|
|
37
|
+
remaining;
|
|
38
|
+
resetAt;
|
|
39
|
+
constructor(limit, remaining, resetAt, serverMessage) {
|
|
40
|
+
super(`[decidio] governed-action rate limit reached${limit != null ? ` (${limit}/hour on this workspace)` : ""}${resetAt ? ` — resets ${resetAt}` : ""}. ` +
|
|
41
|
+
`${serverMessage ? serverMessage + " " : ""}Nothing was routed or executed; retry after the reset.`);
|
|
42
|
+
this.limit = limit;
|
|
43
|
+
this.remaining = remaining;
|
|
44
|
+
this.resetAt = resetAt;
|
|
45
|
+
this.name = "DecidioRateLimitError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
35
48
|
export class DecidioNetworkError extends Error {
|
|
36
49
|
endpoint;
|
|
37
50
|
op;
|
|
38
51
|
code;
|
|
39
52
|
constructor(endpoint, op, code, cause) {
|
|
40
53
|
super(`[decidio] could not reach ${endpoint} during ${op}${code ? ` (${code})` : ""}. ` +
|
|
54
|
+
(/localhost|127\.0\.0\.1/.test(endpoint) && code === "ECONNREFUSED"
|
|
55
|
+
? `A refused localhost connection usually means the SDK never saw your config: if your settings live in .env, run node with --env-file=.env (or export DECIDIO_API_URL — the hosted sandbox is https://decidio-api.onrender.com). `
|
|
56
|
+
: "") +
|
|
41
57
|
`Nothing was executed locally and no verdict was received — if a request reached Decidio it is ` +
|
|
42
58
|
`visible in your queue. Check the network/URL (DECIDIO_API_URL) and retry; retrying is safe ` +
|
|
43
59
|
`(a retry creates a new request — it can never double-execute).`);
|
|
@@ -63,6 +79,10 @@ async function post(cfg, path, body) {
|
|
|
63
79
|
headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
64
80
|
body: JSON.stringify(body),
|
|
65
81
|
});
|
|
82
|
+
if (res.status === 429) {
|
|
83
|
+
const b = await res.json().catch(() => null);
|
|
84
|
+
throw new DecidioRateLimitError(b?.limit, b?.remaining, b?.resetAt, b?.message);
|
|
85
|
+
}
|
|
66
86
|
if (!res.ok)
|
|
67
87
|
throw new Error(`decidio ${path} ${res.status}: ${await res.text().catch(() => "")}`);
|
|
68
88
|
return res.json();
|
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.1.
|
|
8
|
+
version: 0.1.8
|
|
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.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@decidio/verify": "
|
|
84
|
+
"@decidio/verify": "0.1.7"
|
|
85
85
|
},
|
|
86
86
|
"author": "OmniTwin Technologies Inc.",
|
|
87
87
|
"bugs": {
|