@decidio/sdk 0.1.5 → 0.1.7
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 +57 -11
- 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 +149 -138
- package/dist/guard.d.ts +2 -14
- package/dist/guard.js +6 -19
- 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 +107 -73
- 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,31 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
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
|
-
import { readFileSync, writeFileSync, existsSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync, chmodSync } from "node:fs";
|
|
22
3
|
import { randomBytes } from "node:crypto";
|
|
23
4
|
import { createServer } from "node:http";
|
|
24
5
|
import { createInterface } from "node:readline";
|
|
25
6
|
import { createRequire } from "node:module";
|
|
26
7
|
import { generateKeypair } from "./signing.js";
|
|
27
8
|
import { withApproval } from "./index.js";
|
|
28
|
-
import { verifyReceipt
|
|
9
|
+
import { verifyReceipt } from "@decidio/verify";
|
|
29
10
|
const VERSION = (() => {
|
|
30
11
|
try {
|
|
31
12
|
return createRequire(import.meta.url)("../package.json").version;
|
|
@@ -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;
|
|
@@ -96,16 +66,21 @@ async function api(path, init = {}, bearer) {
|
|
|
96
66
|
}
|
|
97
67
|
return { status: res.status, body };
|
|
98
68
|
}
|
|
69
|
+
function writeSecretFile(path, body) {
|
|
70
|
+
writeFileSync(path, body, { mode: 0o600 });
|
|
71
|
+
try {
|
|
72
|
+
chmodSync(path, 0o600);
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
99
76
|
function upsertEnv(updates) {
|
|
100
77
|
const cur = readEnvFile();
|
|
101
78
|
const merged = { ...cur, ...updates };
|
|
102
79
|
const body = Object.entries(merged).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
|
|
103
|
-
|
|
80
|
+
writeSecretFile(ENV_PATH, body);
|
|
104
81
|
}
|
|
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
82
|
function ensureGitignore() {
|
|
108
|
-
const entries = [".env", ".decidio-pending/"];
|
|
83
|
+
const entries = [".env", ".env.quickstart", ".env.*.bak", ".decidio-pending/"];
|
|
109
84
|
const existing = existsSync(".gitignore") ? readFileSync(".gitignore", "utf8") : "";
|
|
110
85
|
const missing = entries.filter((e) => !existing.split(/\r?\n/).some((l) => l.trim() === e || l.trim() === e.replace(/\/$/, "")));
|
|
111
86
|
if (!missing.length)
|
|
@@ -113,12 +88,10 @@ function ensureGitignore() {
|
|
|
113
88
|
appendFileSync(".gitignore", (existing && !existing.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n");
|
|
114
89
|
log(`✔ added ${missing.join(" + ")} to .gitignore (your agent key and parked actions stay out of git)`);
|
|
115
90
|
}
|
|
116
|
-
// ---- interactive input (session sign-in) ---------------------------------------------------
|
|
117
91
|
function ask(question, mute = false) {
|
|
118
92
|
return new Promise((resolve) => {
|
|
119
93
|
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
120
94
|
if (mute) {
|
|
121
|
-
// Hide password echo: after the prompt renders, swallow subsequent output writes.
|
|
122
95
|
const anyRl = rl;
|
|
123
96
|
const write = anyRl._writeToOutput?.bind(rl);
|
|
124
97
|
anyRl._writeToOutput = (s) => { if (s.includes(question))
|
|
@@ -128,19 +101,14 @@ function ask(question, mute = false) {
|
|
|
128
101
|
process.stdout.write("\n"); resolve(answer.trim()); });
|
|
129
102
|
});
|
|
130
103
|
}
|
|
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
104
|
async function acquireSession(purpose) {
|
|
135
|
-
if (process.env.DECIDIO_SESSION_TOKEN)
|
|
136
|
-
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
105
|
if (/^http:\/\//i.test(API) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])([:/]|$)/i.test(API)) {
|
|
140
|
-
log(`✘ refusing to send
|
|
106
|
+
log(`✘ refusing to send workspace credentials over plaintext http to ${API}.`);
|
|
141
107
|
log(` Use https (the hosted sandbox is https://decidio-api.onrender.com) or localhost for local dev.`);
|
|
142
108
|
return null;
|
|
143
109
|
}
|
|
110
|
+
if (process.env.DECIDIO_SESSION_TOKEN)
|
|
111
|
+
return process.env.DECIDIO_SESSION_TOKEN;
|
|
144
112
|
if (!process.stdin.isTTY) {
|
|
145
113
|
log(`✘ ${purpose} needs a Decidio workspace sign-in, and this terminal can't prompt for one.`);
|
|
146
114
|
log(` Fix: set DECIDIO_SESSION_TOKEN and re-run. To mint one:`);
|
|
@@ -177,16 +145,22 @@ async function acquireSession(purpose) {
|
|
|
177
145
|
log(` Sign-in links are single-use and expire in 15 minutes — request a fresh one and re-run.`);
|
|
178
146
|
return null;
|
|
179
147
|
}
|
|
180
|
-
async function cmdInit(agentId) {
|
|
148
|
+
async function cmdInit(agentId, opts = {}) {
|
|
181
149
|
log(`Registering agent "${agentId}" with Decidio at ${API}…`);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
150
|
+
const existingEnv = readEnvFile();
|
|
151
|
+
const existingKey = existingEnv.DECIDIO_AGENT_KEY;
|
|
152
|
+
const sourceSystem = process.env.DECIDIO_SOURCE_SYSTEM || existingEnv.DECIDIO_SOURCE_SYSTEM;
|
|
153
|
+
if (existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId && !opts.force) {
|
|
154
|
+
log(`✘ this directory's .env already holds the key for a DIFFERENT agent ("${existingEnv.DECIDIO_AGENT_ID}").`);
|
|
155
|
+
log(` Registering "${agentId}" here would overwrite that key — and a private key that is`);
|
|
156
|
+
log(` overwritten is gone for good (it never left this machine). Nothing was written.`);
|
|
157
|
+
log(` Pick one:`);
|
|
158
|
+
log(` 1. New directory (cleanest): mkdir ${agentId} && cd ${agentId} && npx @decidio/sdk init ${agentId}`);
|
|
159
|
+
log(` 2. Replace the old agent here, backing its .env up first: npx @decidio/sdk init ${agentId} --force`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
186
162
|
const { did: localDid, privateKeyBase64: localKey } = generateKeypair();
|
|
187
163
|
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
164
|
let session = null;
|
|
191
165
|
let r = await api("/api/agents/register", { method: "POST", body: registerBody });
|
|
192
166
|
if (r.status === 401 || r.status === 403) {
|
|
@@ -197,10 +171,6 @@ async function cmdInit(agentId) {
|
|
|
197
171
|
}
|
|
198
172
|
let did, privateKey;
|
|
199
173
|
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
174
|
did = r.body?.did;
|
|
205
175
|
if (existingKey) {
|
|
206
176
|
log(`• "${agentId}" is already registered in this workspace (did ${did}) — reusing it with the DECIDIO_AGENT_KEY already in .env.`);
|
|
@@ -227,18 +197,15 @@ async function cmdInit(agentId) {
|
|
|
227
197
|
log(` Fix the cause above and re-run — init is safe to repeat.`);
|
|
228
198
|
return 1;
|
|
229
199
|
}
|
|
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
200
|
let agentToken, workspaceId;
|
|
235
201
|
const mintBearer = session ?? TOKEN();
|
|
236
202
|
if (mintBearer) {
|
|
237
|
-
const
|
|
203
|
+
const expiresInDays = opts.expiresDays ?? 90;
|
|
204
|
+
const mint = await api("/api/workspace/agent-token", { method: "POST", body: JSON.stringify({ agentId, label: "sdk-cli", expiresInDays }) }, mintBearer);
|
|
238
205
|
if (mint.status >= 200 && mint.status < 300 && mint.body?.token) {
|
|
239
206
|
agentToken = mint.body.token;
|
|
240
207
|
workspaceId = mint.body.workspaceId;
|
|
241
|
-
log(`✔ minted the agent's API token — floor-limited: it can request
|
|
208
|
+
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)`}`);
|
|
242
209
|
}
|
|
243
210
|
else if (session) {
|
|
244
211
|
log(`✘ could not mint the agent's API token (${mint.status}): ${JSON.stringify(mint.body)}`);
|
|
@@ -256,19 +223,21 @@ async function cmdInit(agentId) {
|
|
|
256
223
|
if (did)
|
|
257
224
|
envOut.DECIDIO_AGENT_DID = did;
|
|
258
225
|
if (privateKey)
|
|
259
|
-
envOut.DECIDIO_AGENT_KEY = privateKey;
|
|
226
|
+
envOut.DECIDIO_AGENT_KEY = privateKey;
|
|
260
227
|
if (agentToken)
|
|
261
|
-
envOut.DECIDIO_API_TOKEN = agentToken;
|
|
228
|
+
envOut.DECIDIO_API_TOKEN = agentToken;
|
|
262
229
|
if (workspaceId)
|
|
263
230
|
envOut.DECIDIO_WORKSPACE_ID = workspaceId;
|
|
231
|
+
ensureGitignore();
|
|
232
|
+
if (opts.force && existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId) {
|
|
233
|
+
const backup = `.env.${existingEnv.DECIDIO_AGENT_ID.replace(/[^\w.-]/g, "_")}.bak`;
|
|
234
|
+
writeSecretFile(backup, readFileSync(ENV_PATH, "utf8"));
|
|
235
|
+
log(`✔ backed the previous agent's .env up to ${backup} (its key would otherwise be unrecoverable)`);
|
|
236
|
+
}
|
|
264
237
|
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
238
|
for (const [k, v] of Object.entries(envOut))
|
|
269
239
|
process.env[k] = v;
|
|
270
|
-
log(`✔ wrote ${ENV_PATH} — every decidio command reads it from this directory (your script loads it with node --env-file=.env)`);
|
|
271
|
-
ensureGitignore();
|
|
240
|
+
log(`✔ wrote ${ENV_PATH} (owner-only where the OS supports it) — every decidio command reads it from this directory (your script loads it with node --env-file=.env)`);
|
|
272
241
|
if (privateKey)
|
|
273
242
|
log(` (the agent's private signing key is in there — for production, move it to your secret manager)`);
|
|
274
243
|
log("");
|
|
@@ -305,27 +274,33 @@ async function cmdDoctor() {
|
|
|
305
274
|
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
275
|
const secret = process.env.DECIDIO_WEBHOOK_SECRET || env.DECIDIO_WEBHOOK_SECRET;
|
|
307
276
|
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");
|
|
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).
|
|
311
277
|
const signing = (process.env.DECIDIO_AGENT_KEY || env.DECIDIO_AGENT_KEY) && (process.env.DECIDIO_AGENT_DID || env.DECIDIO_AGENT_DID);
|
|
312
278
|
if (signing)
|
|
313
279
|
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
280
|
else
|
|
315
281
|
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
282
|
const h = await api("/api/health", {}, "");
|
|
320
283
|
line(h.status >= 200 && h.status < 300, `connectivity: API reachable (${h.status})`);
|
|
284
|
+
const tok = process.env.DECIDIO_API_TOKEN || env.DECIDIO_API_TOKEN;
|
|
285
|
+
if (tok) {
|
|
286
|
+
try {
|
|
287
|
+
const payload = JSON.parse(Buffer.from(tok.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
288
|
+
if (typeof payload?.exp === "number") {
|
|
289
|
+
const days = Math.floor((payload.exp * 1000 - Date.now()) / 86_400_000);
|
|
290
|
+
if (days < 0)
|
|
291
|
+
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
|
+
else if (days <= 14)
|
|
293
|
+
log(`⚠ agent token: expires in ${days} day(s) — mint a fresh one before it does: npx @decidio/sdk init ${env.DECIDIO_AGENT_ID ?? "<agent>"}`);
|
|
294
|
+
else
|
|
295
|
+
log(`✔ agent token: valid for another ${days} days`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch { }
|
|
299
|
+
}
|
|
321
300
|
const rec = await api("/api/records");
|
|
322
301
|
if (rec.status >= 200 && rec.status < 300)
|
|
323
302
|
log("✔ token scope: session/admin (init, approvals and receipt work from this env)");
|
|
324
303
|
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
304
|
const probe = await api("/agent/status/doctor-scope-probe");
|
|
330
305
|
if (probe.status === 404)
|
|
331
306
|
log("✔ token scope: agent floor — correct for the runtime SDK (approvals/receipt will ask you to sign in)");
|
|
@@ -339,11 +314,10 @@ async function cmdDoctor() {
|
|
|
339
314
|
log(ok ? "\ndoctor: all green." : "\ndoctor: fix the ✘ items above, then re-run `npx @decidio/sdk doctor`.");
|
|
340
315
|
return ok ? 0 : 1;
|
|
341
316
|
}
|
|
342
|
-
async function cmdApprovals(sub, id, reason) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (jsonMode)
|
|
317
|
+
async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
318
|
+
const jsonMode = !!flags.json;
|
|
319
|
+
const mineOnly = !!flags.mine;
|
|
320
|
+
if (sub?.startsWith("--"))
|
|
347
321
|
sub = undefined;
|
|
348
322
|
let bearer;
|
|
349
323
|
const probe = await api("/api/decisions/cards");
|
|
@@ -360,9 +334,6 @@ async function cmdApprovals(sub, id, reason) {
|
|
|
360
334
|
}
|
|
361
335
|
const r = await api(`/api/decisions/${encodeURIComponent(id)}/${sub === "approve" ? "approve" : "reject"}`, { method: "POST", body: JSON.stringify({ reason: reason ?? undefined }) }, bearer);
|
|
362
336
|
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
337
|
if (r.body?.alreadyResolved) {
|
|
367
338
|
log(`• ${id} was already resolved — nothing changed.`);
|
|
368
339
|
log(` Decisions are single-use: a second ${sub} can never re-execute the action (that's the replay protection working).`);
|
|
@@ -392,7 +363,6 @@ async function cmdApprovals(sub, id, reason) {
|
|
|
392
363
|
const cards = r.body?.cards ?? [];
|
|
393
364
|
const pending = cards.filter((c) => c.isAgentRequest && c.status !== "sealed");
|
|
394
365
|
if (jsonMode) {
|
|
395
|
-
// Stable machine output (round-3 ask): agents scripting the queue parse this, not prose.
|
|
396
366
|
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
367
|
return 0;
|
|
398
368
|
}
|
|
@@ -401,11 +371,16 @@ async function cmdApprovals(sub, id, reason) {
|
|
|
401
371
|
return 0;
|
|
402
372
|
}
|
|
403
373
|
const myAgent = process.env.DECIDIO_AGENT_ID;
|
|
404
|
-
|
|
405
|
-
|
|
374
|
+
const isMine = (c) => !!myAgent && typeof c.title === "string" && c.title.startsWith(`${myAgent}:`);
|
|
375
|
+
const shown = mineOnly ? pending.filter(isMine) : pending;
|
|
376
|
+
if (mineOnly && !shown.length) {
|
|
377
|
+
log(`No pending approvals for "${myAgent}" (${pending.length} pending elsewhere in this workspace — drop --mine to see them).`);
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
log(`Pending agent approvals (${shown.length}${mineOnly ? ` for "${myAgent}"` : ""})${!mineOnly && myAgent ? ` — workspace-wide; "←" marks this directory's agent (${myAgent}). Use --mine to list only those.` : ""}:`);
|
|
381
|
+
for (const c of shown) {
|
|
406
382
|
const ar = c.agentRequest ?? {};
|
|
407
|
-
|
|
408
|
-
log(` ${c.id} ${ar.action ?? "?"}${ar.amount != null ? ` $${Number(ar.amount).toLocaleString()}` : ""} — ${c.title ?? ""}${mine ? " ←" : ""}`);
|
|
383
|
+
log(` ${c.id} ${ar.action ?? "?"}${ar.amount != null ? ` $${Number(ar.amount).toLocaleString()}` : ""} — ${c.title ?? ""}${!mineOnly && isMine(c) ? " ←" : ""}`);
|
|
409
384
|
}
|
|
410
385
|
log(`\nApprove: npx @decidio/sdk approvals approve <id> [reason]`);
|
|
411
386
|
log(`Reject: npx @decidio/sdk approvals reject <id> [reason]`);
|
|
@@ -418,8 +393,6 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
418
393
|
log(" Find decision ids via `npx @decidio/sdk approvals` or in your Decidio app.");
|
|
419
394
|
return 2;
|
|
420
395
|
}
|
|
421
|
-
// Receipts are workspace data — session scope, like approvals. (The record-detail /verify
|
|
422
|
-
// route carries the credential; the plain record route does not — Codex finding.)
|
|
423
396
|
let bearer;
|
|
424
397
|
let r = await api(`/api/records/${encodeURIComponent(id)}/verify`);
|
|
425
398
|
if (r.status === 401 || r.status === 403) {
|
|
@@ -439,32 +412,40 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
439
412
|
return 1;
|
|
440
413
|
}
|
|
441
414
|
const name = `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
415
|
+
const nextBytes = JSON.stringify(r.body.credential, null, 2) + "\n";
|
|
416
|
+
if (existsSync(name)) {
|
|
417
|
+
if (readFileSync(name, "utf8") !== nextBytes) {
|
|
418
|
+
log(`✘ ${name} already exists with DIFFERENT bytes — refusing to overwrite an audit artifact. Move it aside or delete it, then re-run.`);
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
log(`• ${name} already exists (byte-identical — receipts are immutable); leaving it in place.`);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
writeFileSync(name, nextBytes);
|
|
425
|
+
log(`✔ wrote ${name} (downloaded from your workspace — the download is online; VERIFYING it is offline)`);
|
|
426
|
+
}
|
|
447
427
|
const detail = await api(`/api/records/${encodeURIComponent(id)}`, {}, bearer);
|
|
448
428
|
const conf = detail.body?.record?.confirmation;
|
|
429
|
+
const issuer = r.body?.issuer;
|
|
449
430
|
if (jsonMode) {
|
|
450
|
-
log(JSON.stringify({ file: name, decisionId: id, evidenceTier: conf?.status ?? null, confirmed: !!conf?.confirmed }, null, 2));
|
|
431
|
+
log(JSON.stringify({ file: name, decisionId: id, evidenceTier: conf?.status ?? null, confirmed: !!conf?.confirmed, issuer: issuer ?? null }, null, 2));
|
|
451
432
|
return 0;
|
|
452
433
|
}
|
|
453
434
|
if (conf?.status)
|
|
454
435
|
log(` Sealed credential: the authority decision (its confirmation field reads "pending" forever — immutable). Current record evidence tier: ${conf.status}${conf.confirmed ? " (confirmed)" : ""}.`);
|
|
455
|
-
// Emit EXACTLY the deterministic command we document (round-4 catch: the README and this hint
|
|
456
|
-
// disagreed, and bare npx re-probes the registry) — the verifier ships INSIDE this package.
|
|
457
436
|
log(` Verify it offline — no Decidio account or network needed (the verifier is built in):`);
|
|
458
437
|
log(` npx @decidio/sdk verify ${name}`);
|
|
459
|
-
|
|
460
|
-
|
|
438
|
+
if (issuer) {
|
|
439
|
+
log(` Your workspace's issuer DID (from the authenticated response — record it once, then pin it forever):`);
|
|
440
|
+
log(` ${issuer}`);
|
|
441
|
+
log(` npx @decidio/sdk verify --issuer ${issuer} ${name}`);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
log(` To also prove WHO issued it, pin your workspace's issuer DID (shown in your app's record verify panel):`);
|
|
445
|
+
log(` npx @decidio/sdk verify --issuer <did:key:...> ${name}`);
|
|
446
|
+
}
|
|
461
447
|
return 0;
|
|
462
448
|
}
|
|
463
|
-
// ---- verify (embedded) ----------------------------------------------------------------------
|
|
464
|
-
/** Duplicate-key-refusing parse — the SAME semantics as the decidio-verify CLI (RFC 8785 §3.1:
|
|
465
|
-
* duplicate keys make different parsers disagree on which value is real; refuse instead).
|
|
466
|
-
* Conformance-locked: the cross-language runner asserts this door refuses the same vectors for
|
|
467
|
-
* the same reasons as decidio-verify and python -m decidio.verify. */
|
|
468
449
|
function parseReceiptStrict(text, file) {
|
|
469
450
|
const value = JSON.parse(text);
|
|
470
451
|
const stack = [];
|
|
@@ -512,8 +493,33 @@ function parseReceiptStrict(text, file) {
|
|
|
512
493
|
}
|
|
513
494
|
return value;
|
|
514
495
|
}
|
|
496
|
+
function chainOf(vcs, expectedIssuer) {
|
|
497
|
+
if (!vcs.length)
|
|
498
|
+
return { valid: false, length: 0, reason: "no receipts supplied" };
|
|
499
|
+
const issuers = [...new Set(vcs.map((v) => String(v?.issuer ?? "")))];
|
|
500
|
+
if (issuers.length > 1)
|
|
501
|
+
return { valid: false, length: 0, reason: `these receipts come from ${issuers.length} DIFFERENT issuers — a chain must be issued end to end by one issuer` };
|
|
502
|
+
let prev = null;
|
|
503
|
+
let anchor = "segment";
|
|
504
|
+
for (let i = 0; i < vcs.length; i++) {
|
|
505
|
+
const r = verifyReceipt(vcs[i], expectedIssuer);
|
|
506
|
+
if (!r.valid)
|
|
507
|
+
return { valid: false, length: i, reason: `receipt ${i} (${r.recordId ?? "?"}) invalid: ${r.reason}` };
|
|
508
|
+
const cs = vcs[i]?.credentialSubject ?? {};
|
|
509
|
+
if (i === 0)
|
|
510
|
+
anchor = (cs.prevHash ?? null) === null ? "genesis" : "segment";
|
|
511
|
+
else if ((cs.prevHash ?? null) !== prev)
|
|
512
|
+
return { valid: false, length: i, reason: `receipt ${i} does not follow receipt ${i - 1} (prevHash mismatch)` };
|
|
513
|
+
prev = cs.contentHash;
|
|
514
|
+
}
|
|
515
|
+
return { valid: true, length: vcs.length, anchor };
|
|
516
|
+
}
|
|
515
517
|
function cmdVerify(argsIn) {
|
|
516
518
|
const issIdx = argsIn.indexOf("--issuer");
|
|
519
|
+
if (issIdx >= 0 && (argsIn[issIdx + 1] === undefined || argsIn[issIdx + 1].startsWith("--"))) {
|
|
520
|
+
log("✘ --issuer needs a did:key value.");
|
|
521
|
+
return 2;
|
|
522
|
+
}
|
|
517
523
|
const expectedIssuer = issIdx >= 0 ? argsIn[issIdx + 1] : undefined;
|
|
518
524
|
const files = argsIn.filter((a, i) => !a.startsWith("--") && !(issIdx >= 0 && i === issIdx + 1));
|
|
519
525
|
if (!files.length) {
|
|
@@ -538,13 +544,12 @@ function cmdVerify(argsIn) {
|
|
|
538
544
|
log(` content-bound: ${res.contentBound}${res.reason ? ` reason: ${res.reason}` : ""}`);
|
|
539
545
|
});
|
|
540
546
|
if (vcs.length > 1) {
|
|
541
|
-
const c =
|
|
547
|
+
const c = chainOf(vcs, expectedIssuer);
|
|
542
548
|
allOk = allOk && c.valid;
|
|
543
|
-
log(`\nchain: ${c.valid ? "✔ intact" : "✘ broken"} (${c.length}/${vcs.length})${c.reason ? ` — ${c.reason}` : ""}`);
|
|
549
|
+
log(`\nchain: ${c.valid ? "✔ intact" : "✘ broken"} (${c.length}/${vcs.length})${c.valid ? (c.anchor === "segment" ? " — segment: ordering proven from the FIRST file forward; history before it is not covered by these receipts" : " — from genesis") : ""}${c.reason ? ` — ${c.reason}` : ""}`);
|
|
544
550
|
}
|
|
545
551
|
return allOk ? 0 : 1;
|
|
546
552
|
}
|
|
547
|
-
// ---- quickstart (the guided two-minute tour) ------------------------------------------------
|
|
548
553
|
async function cmdQuickstart() {
|
|
549
554
|
const t0 = Date.now();
|
|
550
555
|
log("Decidio quickstart — gate an action, route it to a human, execute on approval, hold the proof.");
|
|
@@ -553,9 +558,6 @@ async function cmdQuickstart() {
|
|
|
553
558
|
const session = await acquireSession("The quickstart");
|
|
554
559
|
if (!session)
|
|
555
560
|
return 1;
|
|
556
|
-
// 1. A collision-proof agent, registered fresh every run (round-4 ask: idempotent sample
|
|
557
|
-
// registration). Key generated locally; floor-limited token minted; .env written so the
|
|
558
|
-
// take-away snippet works in this directory afterward.
|
|
559
561
|
const agentId = `quickstart-${randomBytes(2).toString("hex")}`;
|
|
560
562
|
log(`① Registering agent "${agentId}" (fresh name each run — no collisions)…`);
|
|
561
563
|
const { did, privateKeyBase64 } = generateKeypair();
|
|
@@ -564,7 +566,7 @@ async function cmdQuickstart() {
|
|
|
564
566
|
log(`✘ registration failed (${reg.status}): ${JSON.stringify(reg.body)}`);
|
|
565
567
|
return 1;
|
|
566
568
|
}
|
|
567
|
-
const mint = await api("/api/workspace/agent-token", { method: "POST", body: JSON.stringify({ agentId, label: "quickstart" }) }, session);
|
|
569
|
+
const mint = await api("/api/workspace/agent-token", { method: "POST", body: JSON.stringify({ agentId, label: "quickstart", expiresInDays: 7 }) }, session);
|
|
568
570
|
if (!(mint.status >= 200 && mint.status < 300) || !mint.body?.token) {
|
|
569
571
|
log(`✘ token mint failed (${mint.status}): ${JSON.stringify(mint.body)}`);
|
|
570
572
|
return 1;
|
|
@@ -574,9 +576,6 @@ async function cmdQuickstart() {
|
|
|
574
576
|
DECIDIO_AGENT_KEY: privateKeyBase64, DECIDIO_API_TOKEN: mint.body.token,
|
|
575
577
|
DECIDIO_WORKSPACE_ID: mint.body.workspaceId ?? "", DECIDIO_WEBHOOK_SECRET: readEnvFile().DECIDIO_WEBHOOK_SECRET || randomBytes(32).toString("hex"),
|
|
576
578
|
};
|
|
577
|
-
// NEVER clobber a real agent's key (guard catch: keys never leave this machine — overwriting
|
|
578
|
-
// .env's DECIDIO_AGENT_KEY orphans that agent irrecoverably). A directory that already holds
|
|
579
|
-
// one gets a TOUR-SCOPED env file instead.
|
|
580
579
|
let envFile = ENV_PATH;
|
|
581
580
|
if (readEnvFile().DECIDIO_AGENT_KEY) {
|
|
582
581
|
envFile = ".env.quickstart";
|
|
@@ -589,11 +588,11 @@ async function cmdQuickstart() {
|
|
|
589
588
|
ensureGitignore();
|
|
590
589
|
log(` ✔ key generated locally (public half registered) · floor-limited token minted · ${envFile} written`);
|
|
591
590
|
log("");
|
|
592
|
-
|
|
593
|
-
log(`② Gating a demo action with ONE line — guard.protect(payInvoice, …):`);
|
|
591
|
+
log(`② Gating a demo action — the tour runs this (blocking mode: watch it live):`);
|
|
594
592
|
log("");
|
|
595
593
|
log(` const payInvoice = guard.protect(payInvoiceRaw,`);
|
|
596
|
-
log(` (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }))
|
|
594
|
+
log(` (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }),`);
|
|
595
|
+
log(` { mode: "blocking" }); // your real agent typically omits this — durable + a worker`);
|
|
597
596
|
log("");
|
|
598
597
|
let decisionId = "";
|
|
599
598
|
const cfg = {
|
|
@@ -615,7 +614,6 @@ async function cmdQuickstart() {
|
|
|
615
614
|
log(` ✔ $86,000 payInvoice ROUTED to a human (decision ${decisionId}) — this agent has no auto-approve rule,`);
|
|
616
615
|
log(` so deny-by-default sends everything to a person. Nothing executes until someone decides.`);
|
|
617
616
|
log("");
|
|
618
|
-
// 3. The human moment — approve right here, or from the Decidio app.
|
|
619
617
|
if (process.stdin.isTTY) {
|
|
620
618
|
await ask(`③ YOU are the approver. Press Enter to approve it here (or approve it in your Decidio queue first, then Enter): `);
|
|
621
619
|
}
|
|
@@ -628,7 +626,6 @@ async function cmdQuickstart() {
|
|
|
628
626
|
return 1;
|
|
629
627
|
}
|
|
630
628
|
log(` ✔ approved${ap.body?.alreadyResolved ? " (it was already approved from your app — even better)" : ""}`);
|
|
631
|
-
// 4. The blocking wrapper wakes and runs the agent's own function.
|
|
632
629
|
let result;
|
|
633
630
|
try {
|
|
634
631
|
result = await running;
|
|
@@ -639,7 +636,6 @@ async function cmdQuickstart() {
|
|
|
639
636
|
}
|
|
640
637
|
log(` ✔ your function executed AFTER approval: ${JSON.stringify(result)}`);
|
|
641
638
|
log("");
|
|
642
|
-
// 5. The proof: download the receipt and verify it offline, all in-process.
|
|
643
639
|
log("④ Downloading the sealed Authority Receipt and verifying it OFFLINE…");
|
|
644
640
|
const rec = await api(`/api/records/${encodeURIComponent(decisionId)}/verify`, {}, session);
|
|
645
641
|
if (!(rec.status >= 200 && rec.status < 300) || !rec.body?.credential) {
|
|
@@ -648,26 +644,28 @@ async function cmdQuickstart() {
|
|
|
648
644
|
}
|
|
649
645
|
const fname = `decidio-receipt-${decisionId.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
|
|
650
646
|
writeFileSync(fname, JSON.stringify(rec.body.credential, null, 2) + "\n");
|
|
651
|
-
const
|
|
652
|
-
|
|
647
|
+
const issuer = rec.body?.issuer;
|
|
648
|
+
const v = verifyReceipt(rec.body.credential, issuer);
|
|
649
|
+
log(` ${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? `, PINNED to your workspace issuer ${issuer}` : "; issuer unpinned"})`);
|
|
650
|
+
if (issuer)
|
|
651
|
+
log(` Keep that DID: it is your trust anchor — npx @decidio/sdk verify --issuer ${issuer} <receipt.json>`);
|
|
653
652
|
const detail = await api(`/api/records/${encodeURIComponent(decisionId)}`, {}, session);
|
|
654
653
|
const tier = detail.body?.record?.confirmation?.status;
|
|
655
654
|
if (tier)
|
|
656
|
-
log(` ✔ execution evidence on the record: ${tier} — the wrapper's report
|
|
655
|
+
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.)`);
|
|
657
656
|
log("");
|
|
658
657
|
const secs = Math.round((Date.now() - t0) / 1000);
|
|
659
|
-
log(`Done in ${secs}s: gated → routed to a human → approved → executed once →
|
|
658
|
+
log(`Done in ${secs}s: gated → routed to a human → approved → your function executed once → an authority-decision receipt you can verify offline.`);
|
|
660
659
|
log("");
|
|
661
660
|
log("Take it to your real agent — the .env here already works:");
|
|
662
661
|
log(` import { guard } from "@decidio/sdk";`);
|
|
663
662
|
log(` const doIt = guard.protect(doItRaw, (a) => ({ action: "yourAction", amount: a.amount, scope: "YourScope" }));`);
|
|
663
|
+
log(` // production is DURABLE mode (default) + one worker: guard.worker(); // README has the full pattern`);
|
|
664
664
|
log("");
|
|
665
|
-
log("Next: the README's 7-step quickstart (durable
|
|
665
|
+
log(`Your workspace + queue: ${API.replace("decidio-api.onrender.com", "app.decidioai.com")} · Next: the README's 7-step quickstart (durable, workers, adapters).`);
|
|
666
666
|
return v.valid ? 0 : 1;
|
|
667
667
|
}
|
|
668
668
|
function cmdDev(target, port) {
|
|
669
|
-
// Signal-only relay: receive Decidio's webhook here (a stable URL) and forward it to
|
|
670
|
-
// your local agent's resume listener. Useful when the agent isn't directly reachable.
|
|
671
669
|
log(`decidio dev relay on http://localhost:${port}/decidio/resume → forwarding to ${target}`);
|
|
672
670
|
createServer((req, res) => {
|
|
673
671
|
if (req.method !== "POST") {
|
|
@@ -704,9 +702,12 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
704
702
|
|
|
705
703
|
npx @decidio/sdk quickstart the guided two-minute tour: gate → approve → execute → proof
|
|
706
704
|
npx @decidio/sdk init [agentId] sign in, register the agent, mint its API token, write .env
|
|
707
|
-
|
|
705
|
+
--unique append a random suffix (collision-proof samples)
|
|
706
|
+
--force replace a different agent already in this .env
|
|
707
|
+
(its .env is backed up first)
|
|
708
|
+
--expires-days N agent-token lifetime (default 90)
|
|
708
709
|
npx @decidio/sdk doctor check config, connectivity, and what your token can do
|
|
709
|
-
npx @decidio/sdk approvals [--json]
|
|
710
|
+
npx @decidio/sdk approvals [--json] [--mine] list pending agent approvals
|
|
710
711
|
npx @decidio/sdk approvals approve <id> [reason]
|
|
711
712
|
npx @decidio/sdk approvals reject <id> [reason]
|
|
712
713
|
npx @decidio/sdk receipt <id> download a sealed decision's Authority Receipt (.json)
|
|
@@ -736,14 +737,24 @@ async function main(argv) {
|
|
|
736
737
|
case "init": {
|
|
737
738
|
const argsIn = argv.slice(3);
|
|
738
739
|
const unique = argsIn.includes("--unique");
|
|
739
|
-
const
|
|
740
|
+
const expIdx = argsIn.indexOf("--expires-days");
|
|
741
|
+
const expRaw = expIdx >= 0 ? argsIn[expIdx + 1] : undefined;
|
|
742
|
+
if (expIdx >= 0 && (!expRaw || !/^\d+$/.test(expRaw) || Number(expRaw) < 1)) {
|
|
743
|
+
log("✘ --expires-days needs a positive whole number of days (e.g. --expires-days 30).");
|
|
744
|
+
return 2;
|
|
745
|
+
}
|
|
746
|
+
const nameArg = argsIn.find((x, i) => !x.startsWith("-") && !(expIdx >= 0 && i === expIdx + 1));
|
|
740
747
|
let name = nameArg ?? process.env.DECIDIO_AGENT_ID ?? "my-agent";
|
|
741
748
|
if (unique)
|
|
742
749
|
name = `${name}-${randomBytes(2).toString("hex")}`;
|
|
743
|
-
return cmdInit(name);
|
|
750
|
+
return cmdInit(name, { force: argsIn.includes("--force"), expiresDays: expRaw ? Number(expRaw) : undefined });
|
|
744
751
|
}
|
|
745
752
|
case "doctor": return cmdDoctor();
|
|
746
|
-
case "approvals":
|
|
753
|
+
case "approvals": {
|
|
754
|
+
const argsIn = argv.slice(3);
|
|
755
|
+
const positional = argsIn.filter((x) => !x.startsWith("-"));
|
|
756
|
+
return cmdApprovals(positional[0], positional[1], positional.slice(2).join(" ") || undefined, { json: argsIn.includes("--json"), mine: argsIn.includes("--mine") });
|
|
757
|
+
}
|
|
747
758
|
case "receipt": {
|
|
748
759
|
const argsIn = argv.slice(3);
|
|
749
760
|
return cmdReceipt(argsIn.find((x) => !x.startsWith("-")), argsIn.includes("--json"));
|
package/dist/guard.d.ts
CHANGED
|
@@ -8,36 +8,25 @@ 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";
|
|
20
|
+
action?: string;
|
|
28
21
|
}
|
|
29
22
|
export declare class Guard {
|
|
30
23
|
config: GuardFacadeConfig;
|
|
31
24
|
resume?: ResumeController;
|
|
32
25
|
constructor(config: GuardFacadeConfig, resume?: ResumeController);
|
|
33
26
|
static fromEnv(): Guard;
|
|
34
|
-
|
|
27
|
+
register(action: string, fn: (...a: any[]) => Promise<any>): void;
|
|
35
28
|
resumeHandler(): import("http").RequestListener;
|
|
36
|
-
/** Mount on a Next.js/Hono/CF/Bun route: `export const POST = (req) => guard.resumeFetchHandler()(req)`. */
|
|
37
29
|
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
30
|
worker(opts?: {
|
|
42
31
|
intervalMs?: number;
|
|
43
32
|
timeoutMs?: number;
|
|
@@ -48,5 +37,4 @@ export declare class Guard {
|
|
|
48
37
|
private ensureResume;
|
|
49
38
|
protect<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<R>;
|
|
50
39
|
}
|
|
51
|
-
/** Preconfigured singleton — `import { guard } from "@decidio/sdk"`. */
|
|
52
40
|
export declare const guard: Guard;
|