@decidio/sdk 0.1.4 → 0.1.6
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 +27 -12
- package/dist/adapters/_gate.d.ts +0 -2
- package/dist/adapters/_gate.js +0 -15
- package/dist/adapters/langgraph.d.ts +0 -3
- package/dist/adapters/langgraph.js +0 -10
- package/dist/adapters/openai.d.ts +0 -4
- package/dist/adapters/openai.js +0 -20
- package/dist/adapters/temporal.d.ts +0 -1
- package/dist/adapters/temporal.js +1 -13
- package/dist/cli.js +43 -113
- package/dist/guard.d.ts +0 -14
- package/dist/guard.js +1 -18
- package/dist/index.d.ts +0 -41
- package/dist/index.js +3 -81
- package/dist/inngest.d.ts +0 -20
- package/dist/inngest.js +0 -26
- package/dist/resume.d.ts +5 -61
- package/dist/resume.js +70 -66
- package/dist/signing.d.ts +0 -12
- package/dist/signing.js +0 -25
- package/openapi.yaml +1 -1
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @decidio/sdk CLI — the on-ramp. Run via `npx @decidio/sdk <cmd>` (or tsx in-repo).
|
|
3
|
-
// init [agentId] sign in, register the agent, mint its API token, write .env
|
|
4
|
-
// doctor check config + connectivity + durability readiness
|
|
5
|
-
// approvals list pending agent approvals; `approvals approve|reject <id> [reason]`
|
|
6
|
-
// receipt <id> download a sealed decision's Authority Receipt to a .json file
|
|
7
|
-
// dev [--target U] signal-only relay: forwards Decidio's webhook to your local agent URL
|
|
8
|
-
//
|
|
9
|
-
// TWO TOKEN KINDS, deliberately (self-serve design 2026-08-28): commands that ADMINISTER a
|
|
10
|
-
// workspace (init's register+mint, approvals, receipt) need the owner's SESSION token — acquired
|
|
11
|
-
// interactively per run or from DECIDIO_SESSION_TOKEN, held in memory, NEVER written to disk.
|
|
12
|
-
// What lands in .env as DECIDIO_API_TOKEN is the floor-limited AGENT token the runtime SDK
|
|
13
|
-
// needs (it can call /agent/* and nothing else). A session-capable secret must never live in
|
|
14
|
-
// an agent host's .env — that was the pre-publish design's hole.
|
|
15
|
-
//
|
|
16
|
-
// MESSAGE DISCIPLINE (founder bar 2026-08-29, from the Codex acceptance run): every failure
|
|
17
|
-
// tells the developer WHAT happened, WHY, and the EXACT next command. Every success says what
|
|
18
|
-
// changed, where, and what to do next. No stack traces for expected failures; no green ✔ for a
|
|
19
|
-
// no-op (the replay-approve bug: the server's idempotent alreadyResolved was reported as a
|
|
20
|
-
// fresh success — an authority CLI may never dress a no-op as an action).
|
|
21
2
|
import { readFileSync, writeFileSync, existsSync, appendFileSync } from "node:fs";
|
|
22
3
|
import { randomBytes } from "node:crypto";
|
|
23
4
|
import { createServer } from "node:http";
|
|
@@ -34,8 +15,6 @@ const VERSION = (() => {
|
|
|
34
15
|
return "unknown";
|
|
35
16
|
}
|
|
36
17
|
})();
|
|
37
|
-
// ---- environment: load the CWD .env FIRST (Codex finding: init writes .env, but the next
|
|
38
|
-
// command ignored it and pointed at localhost). Process env always wins — .env only fills gaps.
|
|
39
18
|
function readEnvFile() {
|
|
40
19
|
const out = {};
|
|
41
20
|
if (existsSync(".env"))
|
|
@@ -47,9 +26,6 @@ function readEnvFile() {
|
|
|
47
26
|
return out;
|
|
48
27
|
}
|
|
49
28
|
for (const [k, v] of Object.entries(readEnvFile())) {
|
|
50
|
-
// NEVER autoload the session token from disk: the whole design splits session (memory-only)
|
|
51
|
-
// from the floor-limited agent token (.env). Silently honoring a parked session token would
|
|
52
|
-
// reward exactly the credential-at-rest the split exists to prevent (guard catch 2026-08-29).
|
|
53
29
|
if (k === "DECIDIO_SESSION_TOKEN") {
|
|
54
30
|
console.log("⚠ 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.");
|
|
55
31
|
continue;
|
|
@@ -57,17 +33,11 @@ for (const [k, v] of Object.entries(readEnvFile())) {
|
|
|
57
33
|
if (process.env[k] === undefined)
|
|
58
34
|
process.env[k] = v;
|
|
59
35
|
}
|
|
60
|
-
// `quickstart` is the zero-config guided tour: with no DECIDIO_API_URL anywhere it targets the
|
|
61
|
-
// hosted sandbox (and says so); every other command keeps the localhost dev default.
|
|
62
36
|
const IS_QUICKSTART = process.argv[2] === "quickstart";
|
|
63
37
|
const API = (process.env.DECIDIO_API_URL ?? (IS_QUICKSTART ? "https://decidio-api.onrender.com" : "http://localhost:4000")).replace(/\/$/, "");
|
|
64
|
-
// Token is read AT CALL TIME, not captured at import: init mints a fresh agent token mid-run and
|
|
65
|
-
// its closing doctor must probe with THAT, not the pre-init env (round-3 Codex catch — the stale
|
|
66
|
-
// capture made init's own doctor print "token: REJECTED" seconds after a successful mint).
|
|
67
38
|
const TOKEN = () => process.env.DECIDIO_API_TOKEN;
|
|
68
39
|
const ENV_PATH = ".env";
|
|
69
40
|
const log = (...a) => console.log(...a);
|
|
70
|
-
/** Fetch wrapper that turns transport failures into guidance instead of a stack trace. */
|
|
71
41
|
async function api(path, init = {}, bearer) {
|
|
72
42
|
const auth = bearer ?? TOKEN();
|
|
73
43
|
let res;
|
|
@@ -102,10 +72,8 @@ function upsertEnv(updates) {
|
|
|
102
72
|
const body = Object.entries(merged).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
|
|
103
73
|
writeFileSync(ENV_PATH, body);
|
|
104
74
|
}
|
|
105
|
-
/** Keep secrets out of git without nagging: append the two entries to .gitignore when they're
|
|
106
|
-
* not already covered. Creating the file in a non-git directory is harmless. */
|
|
107
75
|
function ensureGitignore() {
|
|
108
|
-
const entries = [".env", ".decidio-pending/"];
|
|
76
|
+
const entries = [".env", ".env.quickstart", ".decidio-pending/"];
|
|
109
77
|
const existing = existsSync(".gitignore") ? readFileSync(".gitignore", "utf8") : "";
|
|
110
78
|
const missing = entries.filter((e) => !existing.split(/\r?\n/).some((l) => l.trim() === e || l.trim() === e.replace(/\/$/, "")));
|
|
111
79
|
if (!missing.length)
|
|
@@ -113,12 +81,10 @@ function ensureGitignore() {
|
|
|
113
81
|
appendFileSync(".gitignore", (existing && !existing.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n");
|
|
114
82
|
log(`✔ added ${missing.join(" + ")} to .gitignore (your agent key and parked actions stay out of git)`);
|
|
115
83
|
}
|
|
116
|
-
// ---- interactive input (session sign-in) ---------------------------------------------------
|
|
117
84
|
function ask(question, mute = false) {
|
|
118
85
|
return new Promise((resolve) => {
|
|
119
86
|
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
120
87
|
if (mute) {
|
|
121
|
-
// Hide password echo: after the prompt renders, swallow subsequent output writes.
|
|
122
88
|
const anyRl = rl;
|
|
123
89
|
const write = anyRl._writeToOutput?.bind(rl);
|
|
124
90
|
anyRl._writeToOutput = (s) => { if (s.includes(question))
|
|
@@ -128,14 +94,9 @@ function ask(question, mute = false) {
|
|
|
128
94
|
process.stdout.write("\n"); resolve(answer.trim()); });
|
|
129
95
|
});
|
|
130
96
|
}
|
|
131
|
-
/** Owner SESSION token, memory-only. Sources in order: DECIDIO_SESSION_TOKEN env, interactive
|
|
132
|
-
* email+password (`/demo/login`), pasted sign-in link (`/demo/login-consume` — for the
|
|
133
|
-
* password-less accounts the welcome flow creates). Never persisted. */
|
|
134
97
|
async function acquireSession(purpose) {
|
|
135
98
|
if (process.env.DECIDIO_SESSION_TOKEN)
|
|
136
99
|
return process.env.DECIDIO_SESSION_TOKEN;
|
|
137
|
-
// Same rule the server applies to resume URLs: credentials never ride plaintext http to a
|
|
138
|
-
// non-loopback host. Refuse before prompting, not after collecting a password.
|
|
139
100
|
if (/^http:\/\//i.test(API) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])([:/]|$)/i.test(API)) {
|
|
140
101
|
log(`✘ refusing to send sign-in credentials over plaintext http to ${API}.`);
|
|
141
102
|
log(` Use https (the hosted sandbox is https://decidio-api.onrender.com) or localhost for local dev.`);
|
|
@@ -179,14 +140,10 @@ async function acquireSession(purpose) {
|
|
|
179
140
|
}
|
|
180
141
|
async function cmdInit(agentId) {
|
|
181
142
|
log(`Registering agent "${agentId}" with Decidio at ${API}…`);
|
|
182
|
-
// CLIENT-SIDE KEYGEN: generate the Ed25519 keypair LOCALLY and register only the PUBLIC
|
|
183
|
-
// did:key. The private key NEVER crosses the wire — it goes straight to this host's env.
|
|
184
143
|
const existingKey = readEnvFile().DECIDIO_AGENT_KEY;
|
|
185
144
|
const sourceSystem = process.env.DECIDIO_SOURCE_SYSTEM || readEnvFile().DECIDIO_SOURCE_SYSTEM;
|
|
186
145
|
const { did: localDid, privateKeyBase64: localKey } = generateKeypair();
|
|
187
146
|
const registerBody = JSON.stringify({ agentId, did: localDid, ...(sourceSystem ? { sourceSystem } : {}) });
|
|
188
|
-
// First try whatever bearer the env already carries (the local-dev / operator path — a static
|
|
189
|
-
// admin token, or dev-open auth). On 401/403 fall through to the self-serve session flow.
|
|
190
147
|
let session = null;
|
|
191
148
|
let r = await api("/api/agents/register", { method: "POST", body: registerBody });
|
|
192
149
|
if (r.status === 401 || r.status === 403) {
|
|
@@ -197,10 +154,6 @@ async function cmdInit(agentId) {
|
|
|
197
154
|
}
|
|
198
155
|
let did, privateKey;
|
|
199
156
|
if (r.status === 409) {
|
|
200
|
-
// Already registered to a server-held did. WITHOUT the original private key on this machine
|
|
201
|
-
// this is a HARD STOP, not a degraded continue (round-3 Codex catch: init used to write a
|
|
202
|
-
// keyless .env and print happy-path next steps — every request then routed unproven and
|
|
203
|
-
// execution evidence silently capped at agent_asserted).
|
|
204
157
|
did = r.body?.did;
|
|
205
158
|
if (existingKey) {
|
|
206
159
|
log(`• "${agentId}" is already registered in this workspace (did ${did}) — reusing it with the DECIDIO_AGENT_KEY already in .env.`);
|
|
@@ -227,10 +180,6 @@ async function cmdInit(agentId) {
|
|
|
227
180
|
log(` Fix the cause above and re-run — init is safe to repeat.`);
|
|
228
181
|
return 1;
|
|
229
182
|
}
|
|
230
|
-
// Mint the runtime credential: the floor-limited AGENT token (can call /agent/* only). This is
|
|
231
|
-
// what .env gets — the session token stays in memory and dies with this process. On boxes
|
|
232
|
-
// without the tester-lab fence (plain local dev) the mint endpoint refuses; that's fine —
|
|
233
|
-
// local dev auths with its existing env and we say so instead of failing the init.
|
|
234
183
|
let agentToken, workspaceId;
|
|
235
184
|
const mintBearer = session ?? TOKEN();
|
|
236
185
|
if (mintBearer) {
|
|
@@ -256,15 +205,12 @@ async function cmdInit(agentId) {
|
|
|
256
205
|
if (did)
|
|
257
206
|
envOut.DECIDIO_AGENT_DID = did;
|
|
258
207
|
if (privateKey)
|
|
259
|
-
envOut.DECIDIO_AGENT_KEY = privateKey;
|
|
208
|
+
envOut.DECIDIO_AGENT_KEY = privateKey;
|
|
260
209
|
if (agentToken)
|
|
261
|
-
envOut.DECIDIO_API_TOKEN = agentToken;
|
|
210
|
+
envOut.DECIDIO_API_TOKEN = agentToken;
|
|
262
211
|
if (workspaceId)
|
|
263
212
|
envOut.DECIDIO_WORKSPACE_ID = workspaceId;
|
|
264
213
|
upsertEnv(envOut);
|
|
265
|
-
// The closing doctor (and anything else this process does) must see what init just wrote —
|
|
266
|
-
// OVERWRITE process.env with the fresh values (round-3 Codex catch: doctor probed with the
|
|
267
|
-
// stale pre-init token and reported the mint it just watched as REJECTED).
|
|
268
214
|
for (const [k, v] of Object.entries(envOut))
|
|
269
215
|
process.env[k] = v;
|
|
270
216
|
log(`✔ wrote ${ENV_PATH} — every decidio command reads it from this directory (your script loads it with node --env-file=.env)`);
|
|
@@ -274,7 +220,7 @@ async function cmdInit(agentId) {
|
|
|
274
220
|
log("");
|
|
275
221
|
log("Next: protect one function and watch it route. Two steps:");
|
|
276
222
|
log("");
|
|
277
|
-
log(" 1. Save this as quickstart.mjs (the
|
|
223
|
+
log(" 1. Save this as quickstart.mjs (the README's step-3 snippet — comments trimmed):");
|
|
278
224
|
log("");
|
|
279
225
|
log(` import { guard } from "@decidio/sdk";`);
|
|
280
226
|
log("");
|
|
@@ -304,28 +250,18 @@ async function cmdDoctor() {
|
|
|
304
250
|
line(!!(process.env.DECIDIO_API_URL || env.DECIDIO_API_URL), `API URL: ${API}${(process.env.DECIDIO_API_URL || env.DECIDIO_API_URL) ? "" : " (unset — defaulting to localhost; the hosted sandbox is https://decidio-api.onrender.com)"}`);
|
|
305
251
|
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>)"}`);
|
|
306
252
|
const secret = process.env.DECIDIO_WEBHOOK_SECRET || env.DECIDIO_WEBHOOK_SECRET;
|
|
307
|
-
line(!!secret, secret ? "webhook secret: set (
|
|
308
|
-
// Signing key — REQUIRED to be ELIGIBLE for auto-approve. Whether anything auto-approves is
|
|
309
|
-
// the workspace policy's call, not this key's (Codex finding: the old wording claimed
|
|
310
|
-
// "auto-approve is in effect" while deny-by-default routed everything).
|
|
253
|
+
line(!!secret, secret ? "webhook secret: set — operator-only transport (inactive against the hosted sandbox; the poll worker guard.worker() is the self-serve resume path)" : "webhook secret: MISSING — the resume webhook (operator deployments) would fail closed; init generates one");
|
|
311
254
|
const signing = (process.env.DECIDIO_AGENT_KEY || env.DECIDIO_AGENT_KEY) && (process.env.DECIDIO_AGENT_DID || env.DECIDIO_AGENT_DID);
|
|
312
255
|
if (signing)
|
|
313
256
|
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).");
|
|
314
257
|
else
|
|
315
258
|
log("⚠ signing key: MISSING (DECIDIO_AGENT_KEY/DID) — the agent can't prove identity, so EVERY request routes to a human. Run `npx @decidio/sdk init` to generate one.");
|
|
316
|
-
// Reachability and token role are SEPARATE checks (the old probe called any <500 green, so a
|
|
317
|
-
// wrong-kind token looked healthy). /api/health is unauthenticated truth about reachability;
|
|
318
|
-
// /api/records then classifies the token.
|
|
319
259
|
const h = await api("/api/health", {}, "");
|
|
320
260
|
line(h.status >= 200 && h.status < 300, `connectivity: API reachable (${h.status})`);
|
|
321
261
|
const rec = await api("/api/records");
|
|
322
262
|
if (rec.status >= 200 && rec.status < 300)
|
|
323
263
|
log("✔ token scope: session/admin (init, approvals and receipt work from this env)");
|
|
324
264
|
else if (rec.status === 401) {
|
|
325
|
-
// A 401 off the agent floor could also mean an INVALID token (Codex catch: don't
|
|
326
|
-
// false-green expiry). Disambiguate with a floor probe: a valid agent token gets 404 for an
|
|
327
|
-
// unknown decision (authorized, not found); an invalid one gets 401 again. A rejection here
|
|
328
|
-
// flips the overall verdict — "all green" after a red line was round 3's other doctor bug.
|
|
329
265
|
const probe = await api("/agent/status/doctor-scope-probe");
|
|
330
266
|
if (probe.status === 404)
|
|
331
267
|
log("✔ token scope: agent floor — correct for the runtime SDK (approvals/receipt will ask you to sign in)");
|
|
@@ -340,8 +276,6 @@ async function cmdDoctor() {
|
|
|
340
276
|
return ok ? 0 : 1;
|
|
341
277
|
}
|
|
342
278
|
async function cmdApprovals(sub, id, reason) {
|
|
343
|
-
// Approvals are a HUMAN act on the workspace — session scope, never the agent token. If the
|
|
344
|
-
// env's token is agent-floor (401 on /api/*), sign in interactively; memory-only as always.
|
|
345
279
|
const jsonMode = sub === "--json";
|
|
346
280
|
if (jsonMode)
|
|
347
281
|
sub = undefined;
|
|
@@ -360,9 +294,6 @@ async function cmdApprovals(sub, id, reason) {
|
|
|
360
294
|
}
|
|
361
295
|
const r = await api(`/api/decisions/${encodeURIComponent(id)}/${sub === "approve" ? "approve" : "reject"}`, { method: "POST", body: JSON.stringify({ reason: reason ?? undefined }) }, bearer);
|
|
362
296
|
if (r.status >= 200 && r.status < 300) {
|
|
363
|
-
// HONESTY over green checkmarks (Codex finding: the server resolves each decision exactly
|
|
364
|
-
// once and answers replays idempotently — a replay is a NO-OP and must read as one, never
|
|
365
|
-
// as a fresh approval). Non-zero exit so scripts can tell the difference.
|
|
366
297
|
if (r.body?.alreadyResolved) {
|
|
367
298
|
log(`• ${id} was already resolved — nothing changed.`);
|
|
368
299
|
log(` Decisions are single-use: a second ${sub} can never re-execute the action (that's the replay protection working).`);
|
|
@@ -392,7 +323,6 @@ async function cmdApprovals(sub, id, reason) {
|
|
|
392
323
|
const cards = r.body?.cards ?? [];
|
|
393
324
|
const pending = cards.filter((c) => c.isAgentRequest && c.status !== "sealed");
|
|
394
325
|
if (jsonMode) {
|
|
395
|
-
// Stable machine output (round-3 ask): agents scripting the queue parse this, not prose.
|
|
396
326
|
log(JSON.stringify(pending.map((c) => ({ decisionId: c.id, action: c.agentRequest?.action ?? null, amount: c.agentRequest?.amount ?? null, title: c.title ?? null })), null, 2));
|
|
397
327
|
return 0;
|
|
398
328
|
}
|
|
@@ -400,24 +330,24 @@ async function cmdApprovals(sub, id, reason) {
|
|
|
400
330
|
log("No pending agent approvals. Trigger one from your protected function, then re-run this.");
|
|
401
331
|
return 0;
|
|
402
332
|
}
|
|
403
|
-
|
|
333
|
+
const myAgent = process.env.DECIDIO_AGENT_ID;
|
|
334
|
+
log(`Pending agent approvals (${pending.length})${myAgent ? ` — workspace-wide; "←" marks this directory's agent (${myAgent})` : ""}:`);
|
|
404
335
|
for (const c of pending) {
|
|
405
336
|
const ar = c.agentRequest ?? {};
|
|
406
|
-
|
|
337
|
+
const mine = myAgent && typeof c.title === "string" && c.title.startsWith(`${myAgent}:`);
|
|
338
|
+
log(` ${c.id} ${ar.action ?? "?"}${ar.amount != null ? ` $${Number(ar.amount).toLocaleString()}` : ""} — ${c.title ?? ""}${mine ? " ←" : ""}`);
|
|
407
339
|
}
|
|
408
340
|
log(`\nApprove: npx @decidio/sdk approvals approve <id> [reason]`);
|
|
409
341
|
log(`Reject: npx @decidio/sdk approvals reject <id> [reason]`);
|
|
410
342
|
return 0;
|
|
411
343
|
}
|
|
412
|
-
async function cmdReceipt(id) {
|
|
344
|
+
async function cmdReceipt(id, jsonMode = false) {
|
|
413
345
|
if (!id) {
|
|
414
346
|
log("usage: npx @decidio/sdk receipt <decisionId>");
|
|
415
347
|
log(" Downloads the decision's sealed Authority Receipt (a W3C Verifiable Credential) to a .json file.");
|
|
416
348
|
log(" Find decision ids via `npx @decidio/sdk approvals` or in your Decidio app.");
|
|
417
349
|
return 2;
|
|
418
350
|
}
|
|
419
|
-
// Receipts are workspace data — session scope, like approvals. (The record-detail /verify
|
|
420
|
-
// route carries the credential; the plain record route does not — Codex finding.)
|
|
421
351
|
let bearer;
|
|
422
352
|
let r = await api(`/api/records/${encodeURIComponent(id)}/verify`);
|
|
423
353
|
if (r.status === 401 || r.status === 403) {
|
|
@@ -437,28 +367,32 @@ async function cmdReceipt(id) {
|
|
|
437
367
|
return 1;
|
|
438
368
|
}
|
|
439
369
|
const name = `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
370
|
+
const nextBytes = JSON.stringify(r.body.credential, null, 2) + "\n";
|
|
371
|
+
if (existsSync(name)) {
|
|
372
|
+
if (readFileSync(name, "utf8") !== nextBytes) {
|
|
373
|
+
log(`✘ ${name} already exists with DIFFERENT bytes — refusing to overwrite an audit artifact. Move it aside or delete it, then re-run.`);
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
log(`• ${name} already exists (byte-identical — receipts are immutable); leaving it in place.`);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
writeFileSync(name, nextBytes);
|
|
380
|
+
log(`✔ wrote ${name} (downloaded from your workspace — the download is online; VERIFYING it is offline)`);
|
|
381
|
+
}
|
|
445
382
|
const detail = await api(`/api/records/${encodeURIComponent(id)}`, {}, bearer);
|
|
446
383
|
const conf = detail.body?.record?.confirmation;
|
|
384
|
+
if (jsonMode) {
|
|
385
|
+
log(JSON.stringify({ file: name, decisionId: id, evidenceTier: conf?.status ?? null, confirmed: !!conf?.confirmed }, null, 2));
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
447
388
|
if (conf?.status)
|
|
448
389
|
log(` Sealed credential: the authority decision (its confirmation field reads "pending" forever — immutable). Current record evidence tier: ${conf.status}${conf.confirmed ? " (confirmed)" : ""}.`);
|
|
449
|
-
// Emit EXACTLY the deterministic command we document (round-4 catch: the README and this hint
|
|
450
|
-
// disagreed, and bare npx re-probes the registry) — the verifier ships INSIDE this package.
|
|
451
390
|
log(` Verify it offline — no Decidio account or network needed (the verifier is built in):`);
|
|
452
391
|
log(` npx @decidio/sdk verify ${name}`);
|
|
453
392
|
log(` To also prove WHO issued it, pin your workspace's issuer DID (shown in your app's record verify panel):`);
|
|
454
393
|
log(` npx @decidio/sdk verify --issuer <did:key:...> ${name}`);
|
|
455
394
|
return 0;
|
|
456
395
|
}
|
|
457
|
-
// ---- verify (embedded) ----------------------------------------------------------------------
|
|
458
|
-
/** Duplicate-key-refusing parse — the SAME semantics as the decidio-verify CLI (RFC 8785 §3.1:
|
|
459
|
-
* duplicate keys make different parsers disagree on which value is real; refuse instead).
|
|
460
|
-
* Conformance-locked: the cross-language runner asserts this door refuses the same vectors for
|
|
461
|
-
* the same reasons as decidio-verify and python -m decidio.verify. */
|
|
462
396
|
function parseReceiptStrict(text, file) {
|
|
463
397
|
const value = JSON.parse(text);
|
|
464
398
|
const stack = [];
|
|
@@ -508,6 +442,10 @@ function parseReceiptStrict(text, file) {
|
|
|
508
442
|
}
|
|
509
443
|
function cmdVerify(argsIn) {
|
|
510
444
|
const issIdx = argsIn.indexOf("--issuer");
|
|
445
|
+
if (issIdx >= 0 && (argsIn[issIdx + 1] === undefined || argsIn[issIdx + 1].startsWith("--"))) {
|
|
446
|
+
log("✘ --issuer needs a did:key value.");
|
|
447
|
+
return 2;
|
|
448
|
+
}
|
|
511
449
|
const expectedIssuer = issIdx >= 0 ? argsIn[issIdx + 1] : undefined;
|
|
512
450
|
const files = argsIn.filter((a, i) => !a.startsWith("--") && !(issIdx >= 0 && i === issIdx + 1));
|
|
513
451
|
if (!files.length) {
|
|
@@ -528,7 +466,7 @@ function cmdVerify(argsIn) {
|
|
|
528
466
|
const res = verifyReceipt(vc, expectedIssuer);
|
|
529
467
|
allOk = allOk && res.valid;
|
|
530
468
|
log(`${res.valid ? "✔ VALID " : "✘ INVALID"} ${files[i]}`);
|
|
531
|
-
log(` record: ${res.recordId ?? "?"} issuer: ${res.issuer}`);
|
|
469
|
+
log(` record: ${res.recordId ?? "?"} issuer: ${res.issuer}${expectedIssuer ? "" : " (unpinned — pass --issuer to bind to your workspace's DID)"}`);
|
|
532
470
|
log(` content-bound: ${res.contentBound}${res.reason ? ` reason: ${res.reason}` : ""}`);
|
|
533
471
|
});
|
|
534
472
|
if (vcs.length > 1) {
|
|
@@ -538,7 +476,6 @@ function cmdVerify(argsIn) {
|
|
|
538
476
|
}
|
|
539
477
|
return allOk ? 0 : 1;
|
|
540
478
|
}
|
|
541
|
-
// ---- quickstart (the guided two-minute tour) ------------------------------------------------
|
|
542
479
|
async function cmdQuickstart() {
|
|
543
480
|
const t0 = Date.now();
|
|
544
481
|
log("Decidio quickstart — gate an action, route it to a human, execute on approval, hold the proof.");
|
|
@@ -547,9 +484,6 @@ async function cmdQuickstart() {
|
|
|
547
484
|
const session = await acquireSession("The quickstart");
|
|
548
485
|
if (!session)
|
|
549
486
|
return 1;
|
|
550
|
-
// 1. A collision-proof agent, registered fresh every run (round-4 ask: idempotent sample
|
|
551
|
-
// registration). Key generated locally; floor-limited token minted; .env written so the
|
|
552
|
-
// take-away snippet works in this directory afterward.
|
|
553
487
|
const agentId = `quickstart-${randomBytes(2).toString("hex")}`;
|
|
554
488
|
log(`① Registering agent "${agentId}" (fresh name each run — no collisions)…`);
|
|
555
489
|
const { did, privateKeyBase64 } = generateKeypair();
|
|
@@ -568,9 +502,6 @@ async function cmdQuickstart() {
|
|
|
568
502
|
DECIDIO_AGENT_KEY: privateKeyBase64, DECIDIO_API_TOKEN: mint.body.token,
|
|
569
503
|
DECIDIO_WORKSPACE_ID: mint.body.workspaceId ?? "", DECIDIO_WEBHOOK_SECRET: readEnvFile().DECIDIO_WEBHOOK_SECRET || randomBytes(32).toString("hex"),
|
|
570
504
|
};
|
|
571
|
-
// NEVER clobber a real agent's key (guard catch: keys never leave this machine — overwriting
|
|
572
|
-
// .env's DECIDIO_AGENT_KEY orphans that agent irrecoverably). A directory that already holds
|
|
573
|
-
// one gets a TOUR-SCOPED env file instead.
|
|
574
505
|
let envFile = ENV_PATH;
|
|
575
506
|
if (readEnvFile().DECIDIO_AGENT_KEY) {
|
|
576
507
|
envFile = ".env.quickstart";
|
|
@@ -583,11 +514,11 @@ async function cmdQuickstart() {
|
|
|
583
514
|
ensureGitignore();
|
|
584
515
|
log(` ✔ key generated locally (public half registered) · floor-limited token minted · ${envFile} written`);
|
|
585
516
|
log("");
|
|
586
|
-
|
|
587
|
-
log(`② Gating a demo action with ONE line — guard.protect(payInvoice, …):`);
|
|
517
|
+
log(`② Gating a demo action — the tour runs this (blocking mode: watch it live):`);
|
|
588
518
|
log("");
|
|
589
519
|
log(` const payInvoice = guard.protect(payInvoiceRaw,`);
|
|
590
|
-
log(` (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }))
|
|
520
|
+
log(` (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }),`);
|
|
521
|
+
log(` { mode: "blocking" }); // your real agent typically omits this — durable + a worker`);
|
|
591
522
|
log("");
|
|
592
523
|
let decisionId = "";
|
|
593
524
|
const cfg = {
|
|
@@ -609,7 +540,6 @@ async function cmdQuickstart() {
|
|
|
609
540
|
log(` ✔ $86,000 payInvoice ROUTED to a human (decision ${decisionId}) — this agent has no auto-approve rule,`);
|
|
610
541
|
log(` so deny-by-default sends everything to a person. Nothing executes until someone decides.`);
|
|
611
542
|
log("");
|
|
612
|
-
// 3. The human moment — approve right here, or from the Decidio app.
|
|
613
543
|
if (process.stdin.isTTY) {
|
|
614
544
|
await ask(`③ YOU are the approver. Press Enter to approve it here (or approve it in your Decidio queue first, then Enter): `);
|
|
615
545
|
}
|
|
@@ -622,7 +552,6 @@ async function cmdQuickstart() {
|
|
|
622
552
|
return 1;
|
|
623
553
|
}
|
|
624
554
|
log(` ✔ approved${ap.body?.alreadyResolved ? " (it was already approved from your app — even better)" : ""}`);
|
|
625
|
-
// 4. The blocking wrapper wakes and runs the agent's own function.
|
|
626
555
|
let result;
|
|
627
556
|
try {
|
|
628
557
|
result = await running;
|
|
@@ -633,7 +562,6 @@ async function cmdQuickstart() {
|
|
|
633
562
|
}
|
|
634
563
|
log(` ✔ your function executed AFTER approval: ${JSON.stringify(result)}`);
|
|
635
564
|
log("");
|
|
636
|
-
// 5. The proof: download the receipt and verify it offline, all in-process.
|
|
637
565
|
log("④ Downloading the sealed Authority Receipt and verifying it OFFLINE…");
|
|
638
566
|
const rec = await api(`/api/records/${encodeURIComponent(decisionId)}/verify`, {}, session);
|
|
639
567
|
if (!(rec.status >= 200 && rec.status < 300) || !rec.body?.credential) {
|
|
@@ -643,25 +571,24 @@ async function cmdQuickstart() {
|
|
|
643
571
|
const fname = `decidio-receipt-${decisionId.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
|
|
644
572
|
writeFileSync(fname, JSON.stringify(rec.body.credential, null, 2) + "\n");
|
|
645
573
|
const v = verifyReceipt(rec.body.credential, undefined);
|
|
646
|
-
log(` ${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding
|
|
574
|
+
log(` ${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline; issuer UNPINNED — bind it with: npx @decidio/sdk verify --issuer <your workspace DID>)`);
|
|
647
575
|
const detail = await api(`/api/records/${encodeURIComponent(decisionId)}`, {}, session);
|
|
648
576
|
const tier = detail.body?.record?.confirmation?.status;
|
|
649
577
|
if (tier)
|
|
650
|
-
log(` ✔ execution evidence on the record: ${tier} — the wrapper's report
|
|
578
|
+
log(` ✔ execution evidence on the record: ${tier} — the wrapper's signed report. (The receipt seals the DECISION; execution evidence accrues on the record — see the README.)`);
|
|
651
579
|
log("");
|
|
652
580
|
const secs = Math.round((Date.now() - t0) / 1000);
|
|
653
|
-
log(`Done in ${secs}s: gated → routed to a human → approved → executed once →
|
|
581
|
+
log(`Done in ${secs}s: gated → routed to a human → approved → your function executed once → an authority-decision receipt you can verify offline.`);
|
|
654
582
|
log("");
|
|
655
583
|
log("Take it to your real agent — the .env here already works:");
|
|
656
584
|
log(` import { guard } from "@decidio/sdk";`);
|
|
657
585
|
log(` const doIt = guard.protect(doItRaw, (a) => ({ action: "yourAction", amount: a.amount, scope: "YourScope" }));`);
|
|
586
|
+
log(` // production is DURABLE mode (default) + one worker: guard.worker(); // README has the full pattern`);
|
|
658
587
|
log("");
|
|
659
|
-
log("Next: the README's 7-step quickstart (durable
|
|
588
|
+
log(`Your workspace + queue: ${API.replace("decidio-api.onrender.com", "app.decidioai.com")} · Next: the README's 7-step quickstart (durable, workers, adapters).`);
|
|
660
589
|
return v.valid ? 0 : 1;
|
|
661
590
|
}
|
|
662
591
|
function cmdDev(target, port) {
|
|
663
|
-
// Signal-only relay: receive Decidio's webhook here (a stable URL) and forward it to
|
|
664
|
-
// your local agent's resume listener. Useful when the agent isn't directly reachable.
|
|
665
592
|
log(`decidio dev relay on http://localhost:${port}/decidio/resume → forwarding to ${target}`);
|
|
666
593
|
createServer((req, res) => {
|
|
667
594
|
if (req.method !== "POST") {
|
|
@@ -738,7 +665,10 @@ async function main(argv) {
|
|
|
738
665
|
}
|
|
739
666
|
case "doctor": return cmdDoctor();
|
|
740
667
|
case "approvals": return cmdApprovals(a, b, rest.join(" ") || undefined);
|
|
741
|
-
case "receipt":
|
|
668
|
+
case "receipt": {
|
|
669
|
+
const argsIn = argv.slice(3);
|
|
670
|
+
return cmdReceipt(argsIn.find((x) => !x.startsWith("-")), argsIn.includes("--json"));
|
|
671
|
+
}
|
|
742
672
|
case "verify": return cmdVerify(argv.slice(3));
|
|
743
673
|
case "dev": {
|
|
744
674
|
const args = argv.slice(3);
|
package/dist/guard.d.ts
CHANGED
|
@@ -8,22 +8,14 @@ export interface GuardFacadeConfig {
|
|
|
8
8
|
resumeUrl?: string;
|
|
9
9
|
webhookSecret?: string;
|
|
10
10
|
pendingDir?: string;
|
|
11
|
-
/** Local signing key (DECIDIO_AGENT_KEY) + public did (DECIDIO_AGENT_DID), from `init`.
|
|
12
|
-
* When present the gate authorizes against a PROVEN identity, not a claimed label. */
|
|
13
11
|
agentKey?: string;
|
|
14
12
|
agentDid?: string;
|
|
15
|
-
/** The workspace this agent acts in (DECIDIO_WORKSPACE_ID). Bound into the signed proof (D-B3.6)
|
|
16
|
-
* so a captured proof can't be replayed in another tenant. */
|
|
17
13
|
workspaceId?: string;
|
|
18
|
-
/** Base URL of the Decidio app (DECIDIO_APP_URL) — used to deep-link a routed decision. */
|
|
19
14
|
appUrl?: string;
|
|
20
15
|
fetchImpl?: typeof fetch;
|
|
21
16
|
}
|
|
22
17
|
export interface ProtectOptions {
|
|
23
|
-
/** null/"generic" → the core gate; engine adapters (langgraph/openai/temporal/inngest/mcp)
|
|
24
|
-
* live in @decidio/sdk/adapters and take the engine handle. */
|
|
25
18
|
adapter?: "generic" | "langgraph" | "openai" | "temporal" | "inngest" | "mcp";
|
|
26
|
-
/** "durable" (default) suspends for async approval; "blocking" polls (short approvals only). */
|
|
27
19
|
mode?: "durable" | "blocking";
|
|
28
20
|
}
|
|
29
21
|
export declare class Guard {
|
|
@@ -31,13 +23,8 @@ export declare class Guard {
|
|
|
31
23
|
resume?: ResumeController;
|
|
32
24
|
constructor(config: GuardFacadeConfig, resume?: ResumeController);
|
|
33
25
|
static fromEnv(): Guard;
|
|
34
|
-
/** Mount on a node/Express/Fastify route: `app.post("/decidio/resume", guard.resumeHandler())`. */
|
|
35
26
|
resumeHandler(): import("http").RequestListener;
|
|
36
|
-
/** Mount on a Next.js/Hono/CF/Bun route: `export const POST = (req) => guard.resumeFetchHandler()(req)`. */
|
|
37
27
|
resumeFetchHandler(): (req: Request) => Promise<Response>;
|
|
38
|
-
/** Durable resume with NOTHING to host: a background watcher polls Decidio outward and finishes
|
|
39
|
-
* any parked approval — so a suspended action survives a process restart. One line at startup.
|
|
40
|
-
* Production may swap this for the webhook (resumeUrl); the suspend/resume shape is identical. */
|
|
41
28
|
worker(opts?: {
|
|
42
29
|
intervalMs?: number;
|
|
43
30
|
timeoutMs?: number;
|
|
@@ -48,5 +35,4 @@ export declare class Guard {
|
|
|
48
35
|
private ensureResume;
|
|
49
36
|
protect<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<R>;
|
|
50
37
|
}
|
|
51
|
-
/** Preconfigured singleton — `import { guard } from "@decidio/sdk"`. */
|
|
52
38
|
export declare const guard: Guard;
|
package/dist/guard.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// The Guard facade — `guard.protect(fn, describe)`, the one agnostic line (TS mirror
|
|
2
|
-
// of decidio-guard Python). A thin layer over the verified primitives: durable mode
|
|
3
|
-
// builds a resume controller and suspends; blocking mode polls (opt-in, loud). The
|
|
4
|
-
// agent executes its own action; Decidio gates + records + signals — never executes.
|
|
5
1
|
import { withApproval } from "./index.js";
|
|
6
2
|
import { createDecidioResume, FilePendingStore } from "./resume.js";
|
|
7
3
|
export class Guard {
|
|
@@ -26,13 +22,8 @@ export class Guard {
|
|
|
26
22
|
appUrl: process.env.DECIDIO_APP_URL,
|
|
27
23
|
});
|
|
28
24
|
}
|
|
29
|
-
/** Mount on a node/Express/Fastify route: `app.post("/decidio/resume", guard.resumeHandler())`. */
|
|
30
25
|
resumeHandler() { return this.ensureResume().webhookHandler(); }
|
|
31
|
-
/** Mount on a Next.js/Hono/CF/Bun route: `export const POST = (req) => guard.resumeFetchHandler()(req)`. */
|
|
32
26
|
resumeFetchHandler() { return this.ensureResume().fetchHandler(); }
|
|
33
|
-
/** Durable resume with NOTHING to host: a background watcher polls Decidio outward and finishes
|
|
34
|
-
* any parked approval — so a suspended action survives a process restart. One line at startup.
|
|
35
|
-
* Production may swap this for the webhook (resumeUrl); the suspend/resume shape is identical. */
|
|
36
27
|
worker(opts) {
|
|
37
28
|
return this.ensureResume().worker(opts);
|
|
38
29
|
}
|
|
@@ -42,8 +33,6 @@ export class Guard {
|
|
|
42
33
|
this.resume = createDecidioResume({
|
|
43
34
|
apiUrl: c.apiUrl, apiToken: c.apiToken, webhookSecret: c.webhookSecret,
|
|
44
35
|
resumeUrl: c.resumeUrl, store: new FilePendingStore(c.pendingDir ?? ".decidio-pending"),
|
|
45
|
-
// Carry the signing identity so the durable/cold confirm on resume is signed too (incl. the
|
|
46
|
-
// workspace binding — D-B3.6 — so a cold-resume confirm proof is tenant-scoped as well).
|
|
47
36
|
agentId: c.agentId, agentKey: c.agentKey, agentDid: c.agentDid, workspaceId: c.workspaceId,
|
|
48
37
|
fetchImpl: c.fetchImpl,
|
|
49
38
|
});
|
|
@@ -52,9 +41,7 @@ export class Guard {
|
|
|
52
41
|
}
|
|
53
42
|
protect(fn, describe, opts) {
|
|
54
43
|
if (opts?.adapter && opts.adapter !== "generic") {
|
|
55
|
-
|
|
56
|
-
// engine handle at call time). Surfaced as a clear error until wired in this build.
|
|
57
|
-
throw new Error(`[decidio] adapter "${opts.adapter}" must be bound via @decidio/sdk/adapters/${opts.adapter} (it needs the engine handle).`);
|
|
44
|
+
throw new Error(`[decidio] adapter "${opts.adapter}" must be bound via the @decidio/sdk/${opts.adapter} subpath (it needs the engine handle). See that adapter's exports.`);
|
|
58
45
|
}
|
|
59
46
|
const c = this.config;
|
|
60
47
|
const base = {
|
|
@@ -64,13 +51,9 @@ export class Guard {
|
|
|
64
51
|
if ((opts?.mode ?? "durable") === "blocking") {
|
|
65
52
|
console.warn("[decidio] WARNING: blocking wait — only safe for short, watched approvals. " +
|
|
66
53
|
"Omit mode:\"blocking\" for durable async resume on real (minutes-to-days) approvals.");
|
|
67
|
-
// No resumeUrl on the blocking path — the server must not fire a webhook to an
|
|
68
|
-
// endpoint a blocking agent isn't serving; the poll is the only resume channel.
|
|
69
54
|
return withApproval(fn, describe, base);
|
|
70
55
|
}
|
|
71
|
-
// durable: refuse-don't-downgrade — always wire a resume transport, never silently block.
|
|
72
56
|
return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume: this.ensureResume() });
|
|
73
57
|
}
|
|
74
58
|
}
|
|
75
|
-
/** Preconfigured singleton — `import { guard } from "@decidio/sdk"`. */
|
|
76
59
|
export const guard = Guard.fromEnv();
|
package/dist/index.d.ts
CHANGED
|
@@ -6,49 +6,22 @@ export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
|
6
6
|
export { generateKeypair, signBoundProof, signConfirm, canonicalize } from "./signing.js";
|
|
7
7
|
export type { RequesterIdentity } from "./signing.js";
|
|
8
8
|
export interface GuardConfig {
|
|
9
|
-
/** Base URL of the Decidio gate, e.g. "https://api.decidio.io". */
|
|
10
9
|
apiUrl: string;
|
|
11
|
-
/** The agent identity registered with Decidio (onboarding wizard), e.g. "salesforce/SalesforceBot". */
|
|
12
10
|
agentId: string;
|
|
13
|
-
/** Bearer token for the REST floor (a trusted, already-authenticated internal
|
|
14
|
-
* caller). Lives ONLY in the agent host's environment — never in client code. */
|
|
15
11
|
apiToken?: string;
|
|
16
|
-
/** Source system label sealed onto the receipt (e.g. "salesforce"). */
|
|
17
12
|
sourceSystem?: string;
|
|
18
|
-
/** The agent's PRIVATE signing key (PKCS8 base64), generated locally by `init` and held
|
|
19
|
-
* ONLY in the agent host's env as DECIDIO_AGENT_KEY. When set (with `agentDid`), the SDK
|
|
20
|
-
* signs a request-bound identity proof on each authorize so the floor PROVES the requester
|
|
21
|
-
* instead of trusting the label — required for a registered agent to auto-approve. */
|
|
22
13
|
agentKey?: string;
|
|
23
|
-
/** The agent's PUBLIC did:key (the one registered with Decidio). Pairs with `agentKey`. */
|
|
24
14
|
agentDid?: string;
|
|
25
|
-
/** The workspace this agent acts in (DECIDIO_WORKSPACE_ID, from the owner's agent-token mint).
|
|
26
|
-
* Bound into the signed proof (D-B3.6) so a captured proof can't be replayed in another tenant.
|
|
27
|
-
* For a tester's per-workspace agent token this is the tester's workspace id. */
|
|
28
15
|
workspaceId?: string;
|
|
29
|
-
/** Poll cadence + ceiling while a routed action awaits human approval. Used ONLY
|
|
30
|
-
* by the blocking transport (no `resume` configured). */
|
|
31
16
|
pollIntervalMs?: number;
|
|
32
17
|
pollTimeoutMs?: number;
|
|
33
|
-
/** Optional async notifier so a long human wait surfaces in your logs/UI. */
|
|
34
18
|
onPending?: (info: {
|
|
35
19
|
decisionId: string;
|
|
36
20
|
receiptId: string;
|
|
37
21
|
url: string | null;
|
|
38
22
|
}) => void;
|
|
39
|
-
/** Base URL of the Decidio app (DECIDIO_APP_URL). When set, a routed action can say WHERE to
|
|
40
|
-
* approve it — `<appUrl>/d/<decisionId>` opens that decision directly, on web or on the phone
|
|
41
|
-
* build. This link used to be built only inside our own example, so a developer who pasted the
|
|
42
|
-
* env block set DECIDIO_APP_URL and nothing ever read it. */
|
|
43
23
|
appUrl?: string;
|
|
44
|
-
/** Durable async resume. When set, a routed action SUSPENDS (parks its args in the
|
|
45
|
-
* store, throws DecidioSuspendedError) instead of blocking — the process may exit
|
|
46
|
-
* and resume later via the controller's webhook handler or poll worker. This is
|
|
47
|
-
* the production transport for real (minutes-to-days) human approvals. */
|
|
48
24
|
resume?: ResumeController;
|
|
49
|
-
/** Callback URL Decidio POSTs the verdict to on approval (webhook transport).
|
|
50
|
-
* Sent on /agent/authorize so the decision carries it. Falls back to
|
|
51
|
-
* resume.resumeUrl. Omit when using the poll worker. */
|
|
52
25
|
resumeUrl?: string;
|
|
53
26
|
fetchImpl?: typeof fetch;
|
|
54
27
|
}
|
|
@@ -56,9 +29,6 @@ export interface ApprovalContext {
|
|
|
56
29
|
action: string;
|
|
57
30
|
amount: number;
|
|
58
31
|
scope?: string;
|
|
59
|
-
/** Optional human-facing DETAILS of the action (e.g. { Account, "Lead Source", Stage }) shown to
|
|
60
|
-
* the approver as "what you're approving". Display-only — the policy decides on `amount`/`scope`,
|
|
61
|
-
* never these. Residual-safe tokenized server-side before persistence. */
|
|
62
32
|
context?: Record<string, string | number>;
|
|
63
33
|
}
|
|
64
34
|
export declare class DecidioBlockedError extends Error {
|
|
@@ -75,22 +45,11 @@ export declare class DecidioTimeoutError extends Error {
|
|
|
75
45
|
decisionId: string;
|
|
76
46
|
constructor(decisionId: string);
|
|
77
47
|
}
|
|
78
|
-
/** A transport failure with GUIDANCE instead of a raw undici stack (round-4 Codex catch: the CLI
|
|
79
|
-
* wrapped network errors kindly but the runtime leaked `TypeError: fetch failed`). Carries the
|
|
80
|
-
* endpoint, the operation, the underlying code, and the safety state a caller cares about most:
|
|
81
|
-
* whether anything could have executed. */
|
|
82
48
|
export declare class DecidioNetworkError extends Error {
|
|
83
49
|
endpoint: string;
|
|
84
50
|
op: string;
|
|
85
51
|
code: string | undefined;
|
|
86
52
|
constructor(endpoint: string, op: string, code: string | undefined, cause: unknown);
|
|
87
53
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Wrap an async action behind the Decidio gate. `describe` extracts the approval
|
|
90
|
-
* context (action, amount, scope) from the call arguments. Returns a function with
|
|
91
|
-
* the SAME signature as `fn` — drop-in.
|
|
92
|
-
*/
|
|
93
|
-
/** `<appUrl>/d/<decisionId>` — the deep link that opens this decision directly, on the web app or
|
|
94
|
-
* the phone build. Null when DECIDIO_APP_URL is not configured, never a half-built URL. */
|
|
95
54
|
export declare function decisionUrl(appUrl: string | undefined, decisionId: string): string | null;
|
|
96
55
|
export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig): (...args: A) => Promise<R>;
|