@decidio/sdk 0.1.8 → 0.1.10
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 +43 -16
- package/dist/adapters/_gate.d.ts +2 -0
- package/dist/adapters/_gate.js +14 -2
- package/dist/adapters/langgraph.d.ts +13 -0
- package/dist/adapters/openai.d.ts +24 -0
- package/dist/adapters/temporal.d.ts +16 -0
- package/dist/cli.js +149 -168
- package/dist/guard.d.ts +32 -1
- package/dist/guard.js +13 -2
- package/dist/index.d.ts +46 -2
- package/dist/index.js +21 -6
- package/dist/inngest.d.ts +20 -0
- package/dist/resume.d.ts +88 -0
- package/dist/resume.js +62 -10
- package/dist/signing.d.ts +30 -0
- package/dist/signing.js +56 -0
- package/openapi.yaml +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, appendFileSync, chmodSync, rmSync } from "node:fs";
|
|
3
|
-
import { randomBytes
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { userInfo } from "node:os";
|
|
4
5
|
import { createServer } from "node:http";
|
|
5
6
|
import { execFileSync } from "node:child_process";
|
|
6
7
|
import { createInterface } from "node:readline";
|
|
7
8
|
import { createRequire } from "node:module";
|
|
8
9
|
import { generateKeypair } from "./signing.js";
|
|
9
10
|
import { withApproval, DecidioRateLimitError } from "./index.js";
|
|
10
|
-
import { verifyReceipt } from "@decidio/verify";
|
|
11
|
+
import { verifyReceipt, runVerify } from "@decidio/verify";
|
|
11
12
|
const VERSION = (() => {
|
|
12
13
|
try {
|
|
13
14
|
return createRequire(import.meta.url)("../package.json").version;
|
|
@@ -16,24 +17,35 @@ const VERSION = (() => {
|
|
|
16
17
|
return "unknown";
|
|
17
18
|
}
|
|
18
19
|
})();
|
|
20
|
+
let envWarned = false;
|
|
19
21
|
function readEnvFile() {
|
|
20
22
|
const out = {};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(".env"))
|
|
25
|
+
for (const line of readFileSync(".env", "utf8").split(/\r?\n/)) {
|
|
26
|
+
const m = line.match(/^\s*([A-Z_]+)=(.*)$/);
|
|
27
|
+
if (m)
|
|
28
|
+
out[m[1]] = m[2];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
if (!envWarned) {
|
|
33
|
+
envWarned = true;
|
|
34
|
+
console.error(`⚠ could not read .env (${e?.code ?? e?.message ?? e}) — continuing without it. Fix its permissions if you meant it to load.`);
|
|
26
35
|
}
|
|
36
|
+
}
|
|
27
37
|
return out;
|
|
28
38
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
const OFFLINE_CMD = ["verify", "--version", "-v", "-V", "version", "--help", "-h", "help", undefined].includes(process.argv[2]);
|
|
40
|
+
if (!OFFLINE_CMD)
|
|
41
|
+
for (const [k, v] of Object.entries(readEnvFile())) {
|
|
42
|
+
if (k === "DECIDIO_SESSION_TOKEN") {
|
|
43
|
+
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.");
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (process.env[k] === undefined)
|
|
47
|
+
process.env[k] = v;
|
|
33
48
|
}
|
|
34
|
-
if (process.env[k] === undefined)
|
|
35
|
-
process.env[k] = v;
|
|
36
|
-
}
|
|
37
49
|
const API = (process.env.DECIDIO_API_URL ?? "https://decidio-api.onrender.com").replace(/\/$/, "");
|
|
38
50
|
const TOKEN = () => process.env.DECIDIO_API_TOKEN;
|
|
39
51
|
const ENV_PATH = ".env";
|
|
@@ -46,6 +58,10 @@ async function api(path, init = {}, bearer) {
|
|
|
46
58
|
err(`· decidio → ${API}${process.env.DECIDIO_API_URL ? "" : " (default: the hosted sandbox — set DECIDIO_API_URL to point elsewhere)"}`);
|
|
47
59
|
}
|
|
48
60
|
const auth = bearer ?? TOKEN();
|
|
61
|
+
if (auth && /^http:[/][/]/i.test(API) && !/^http:[/][/](localhost|127.0.0.1|.::1.)([:/]|$)/i.test(API)) {
|
|
62
|
+
err(`✘ refusing to send credentials over plaintext http to ${API} — use https, or localhost for local dev. Nothing was sent.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
49
65
|
let res;
|
|
50
66
|
try {
|
|
51
67
|
res = await fetch(API + path, { ...init, headers: { "content-type": "application/json", ...(auth ? { authorization: `Bearer ${auth}` } : {}), ...(init.headers ?? {}) } });
|
|
@@ -80,18 +96,46 @@ function writeSecretFile(path, body) {
|
|
|
80
96
|
catch { }
|
|
81
97
|
if (process.platform === "win32") {
|
|
82
98
|
try {
|
|
83
|
-
execFileSync("icacls", [path, "/inheritance:r", "/grant:r", `${
|
|
99
|
+
execFileSync("icacls", [path, "/inheritance:r", "/grant:r", `${effectiveUser()}:F`], { stdio: "ignore" });
|
|
84
100
|
}
|
|
85
101
|
catch {
|
|
86
102
|
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
103
|
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
106
|
+
function effectiveUser() {
|
|
107
|
+
try {
|
|
108
|
+
return userInfo().username;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return process.env.USERNAME ?? process.env.USER ?? "";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
90
114
|
function upsertEnv(updates) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
115
|
+
const remaining = { ...updates };
|
|
116
|
+
const updatedKeys = new Set();
|
|
117
|
+
const lines = [];
|
|
118
|
+
try {
|
|
119
|
+
if (existsSync(ENV_PATH))
|
|
120
|
+
for (const line of readFileSync(ENV_PATH, "utf8").split(/\r?\n/)) {
|
|
121
|
+
const m = line.match(/^\s*([A-Z_]+)=/);
|
|
122
|
+
if (m && updatedKeys.has(m[1]))
|
|
123
|
+
continue;
|
|
124
|
+
if (m && m[1] in remaining) {
|
|
125
|
+
lines.push(`${m[1]}=${remaining[m[1]]}`);
|
|
126
|
+
updatedKeys.add(m[1]);
|
|
127
|
+
delete remaining[m[1]];
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
lines.push(line);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
while (lines.length && lines[lines.length - 1] === "")
|
|
135
|
+
lines.pop();
|
|
136
|
+
for (const [k, v] of Object.entries(remaining))
|
|
137
|
+
lines.push(`${k}=${v}`);
|
|
138
|
+
writeSecretFile(ENV_PATH, lines.join("\n") + "\n");
|
|
95
139
|
}
|
|
96
140
|
function ensureGitignore() {
|
|
97
141
|
const entries = [".env", ".env.quickstart", ".env.*.bak", ".decidio-pending/"];
|
|
@@ -104,7 +148,7 @@ function ensureGitignore() {
|
|
|
104
148
|
}
|
|
105
149
|
function ask(question, mute = false) {
|
|
106
150
|
return new Promise((resolve) => {
|
|
107
|
-
const rl = createInterface({ input: process.stdin, output: process.
|
|
151
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
108
152
|
if (mute) {
|
|
109
153
|
const anyRl = rl;
|
|
110
154
|
const write = anyRl._writeToOutput?.bind(rl);
|
|
@@ -112,23 +156,23 @@ function ask(question, mute = false) {
|
|
|
112
156
|
write?.(s); };
|
|
113
157
|
}
|
|
114
158
|
rl.question(question, (answer) => { rl.close(); if (mute)
|
|
115
|
-
process.
|
|
159
|
+
process.stderr.write("\n"); resolve(answer.trim()); });
|
|
116
160
|
});
|
|
117
161
|
}
|
|
118
162
|
async function acquireSession(purpose) {
|
|
119
163
|
if (/^http:\/\//i.test(API) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])([:/]|$)/i.test(API)) {
|
|
120
164
|
err(`✘ refusing to send workspace credentials over plaintext http to ${API}.`);
|
|
121
|
-
|
|
165
|
+
err(` Use https (the hosted sandbox is https://decidio-api.onrender.com) or localhost for local dev.`);
|
|
122
166
|
return null;
|
|
123
167
|
}
|
|
124
168
|
if (process.env.DECIDIO_SESSION_TOKEN)
|
|
125
169
|
return process.env.DECIDIO_SESSION_TOKEN;
|
|
126
170
|
if (!process.stdin.isTTY) {
|
|
127
171
|
err(`✘ ${purpose} needs a Decidio workspace sign-in, and this terminal can't prompt for one.`);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
172
|
+
err(` Fix: set DECIDIO_SESSION_TOKEN and re-run. To mint one:`);
|
|
173
|
+
err(` 1. Request a sign-in link from your workspace's sign-in screen (or use your welcome email).`);
|
|
174
|
+
err(` 2. POST its ?login= value — the response's "token" is your session token:`);
|
|
175
|
+
err(` curl -X POST ${API}/demo/login-consume -H "content-type: application/json" -d '{"token":"<login value>"}'`);
|
|
132
176
|
return null;
|
|
133
177
|
}
|
|
134
178
|
log(`${purpose} needs your Decidio workspace sign-in (${API}).`);
|
|
@@ -160,17 +204,17 @@ async function acquireSession(purpose) {
|
|
|
160
204
|
return null;
|
|
161
205
|
}
|
|
162
206
|
async function cmdInit(agentId, opts = {}) {
|
|
163
|
-
|
|
207
|
+
err(`Registering agent "${agentId}" with Decidio at ${API}…`);
|
|
164
208
|
const existingEnv = readEnvFile();
|
|
165
209
|
const existingKey = existingEnv.DECIDIO_AGENT_KEY;
|
|
166
210
|
const sourceSystem = process.env.DECIDIO_SOURCE_SYSTEM || existingEnv.DECIDIO_SOURCE_SYSTEM;
|
|
167
211
|
if (existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId && !opts.force) {
|
|
168
212
|
err(`✘ this directory's .env already holds the key for a DIFFERENT agent ("${existingEnv.DECIDIO_AGENT_ID}").`);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
213
|
+
err(` Registering "${agentId}" here would overwrite that key — and a private key that is`);
|
|
214
|
+
err(` overwritten is gone for good (it never left this machine). Nothing was written.`);
|
|
215
|
+
err(` Pick one:`);
|
|
216
|
+
err(` 1. New directory (cleanest): mkdir ${agentId} && cd ${agentId} && npx @decidio/sdk init ${agentId}`);
|
|
217
|
+
err(` 2. Replace the old agent here, backing its .env up first: npx @decidio/sdk init ${agentId} --force`);
|
|
174
218
|
return 1;
|
|
175
219
|
}
|
|
176
220
|
const { did: localDid, privateKeyBase64: localKey } = generateKeypair();
|
|
@@ -194,10 +238,10 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
194
238
|
else {
|
|
195
239
|
const suggestion = `${agentId}-${randomBytes(2).toString("hex")}`;
|
|
196
240
|
err(`✘ "${agentId}" is already registered in this workspace, and its private key is not on this machine`);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
241
|
+
err(` (keys never leave the machine that generated them — likely a previous run, or another directory).`);
|
|
242
|
+
err(` Nothing was written. Two ways forward:`);
|
|
243
|
+
err(` 1. Fresh agent (most common): npx @decidio/sdk init ${suggestion}`);
|
|
244
|
+
err(` 2. Reuse "${agentId}": restore its original DECIDIO_AGENT_KEY into .env here, then re-run init.`);
|
|
201
245
|
return 1;
|
|
202
246
|
}
|
|
203
247
|
}
|
|
@@ -208,7 +252,7 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
208
252
|
}
|
|
209
253
|
else {
|
|
210
254
|
err(`✘ registration failed (${r.status}): ${JSON.stringify(r.body)}`);
|
|
211
|
-
|
|
255
|
+
err(` Fix the cause above and re-run — init is safe to repeat.`);
|
|
212
256
|
return 1;
|
|
213
257
|
}
|
|
214
258
|
let agentToken, workspaceId, tokenJti;
|
|
@@ -221,10 +265,13 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
221
265
|
workspaceId = mint.body.workspaceId;
|
|
222
266
|
tokenJti = mint.body.jti;
|
|
223
267
|
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)`}`);
|
|
268
|
+
const superseded = Array.isArray(mint.body.supersededJtis) ? mint.body.supersededJtis : [];
|
|
269
|
+
for (const s of superseded)
|
|
270
|
+
log(` • this replaces the agent's previous token (jti ${s}) — now REVOKED: the server keeps one active token per agent. Anything still using it will 401.`);
|
|
224
271
|
}
|
|
225
272
|
else if (session) {
|
|
226
273
|
err(`✘ could not mint the agent's API token (${mint.status}): ${JSON.stringify(mint.body)}`);
|
|
227
|
-
|
|
274
|
+
err(` Without it the SDK has no runtime credential. Fix the cause above and re-run init — it's safe to repeat.`);
|
|
228
275
|
return 1;
|
|
229
276
|
}
|
|
230
277
|
else {
|
|
@@ -286,7 +333,9 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
286
333
|
async function cmdDoctor() {
|
|
287
334
|
const env = readEnvFile();
|
|
288
335
|
let ok = true;
|
|
336
|
+
let warned = 0;
|
|
289
337
|
const line = (good, label) => { log(`${good ? "✔" : "✘"} ${label}`); ok = ok && good; };
|
|
338
|
+
const warn = (label) => { warned++; log(`⚠ ${label}`); };
|
|
290
339
|
log(`✔ API URL: ${API}${(process.env.DECIDIO_API_URL || env.DECIDIO_API_URL) ? "" : " (default: the hosted sandbox — set DECIDIO_API_URL to point elsewhere)"}`);
|
|
291
340
|
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>)"}`);
|
|
292
341
|
const secret = process.env.DECIDIO_WEBHOOK_SECRET || env.DECIDIO_WEBHOOK_SECRET;
|
|
@@ -296,21 +345,26 @@ async function cmdDoctor() {
|
|
|
296
345
|
if (signing)
|
|
297
346
|
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).");
|
|
298
347
|
else
|
|
299
|
-
|
|
348
|
+
warn("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.");
|
|
300
349
|
const h = await api("/api/health", {}, "");
|
|
301
350
|
line(h.status >= 200 && h.status < 300, `connectivity: API reachable (${h.status})`);
|
|
302
351
|
const tok = process.env.DECIDIO_API_TOKEN || env.DECIDIO_API_TOKEN;
|
|
352
|
+
if (!tok) {
|
|
353
|
+
line(false, "agent token: NOT CONFIGURED — no DECIDIO_API_TOKEN in this directory's .env or your environment. Run `npx @decidio/sdk init <agent-name>` to register an agent and mint one.");
|
|
354
|
+
log(warned ? `\ndoctor: fix the ✘ items above, then re-run \`npx @decidio/sdk doctor\`. (${warned} warning${warned === 1 ? "" : "s"})` : "\ndoctor: fix the ✘ items above, then re-run `npx @decidio/sdk doctor`.");
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
303
357
|
if (tok) {
|
|
304
358
|
try {
|
|
305
359
|
const payload = JSON.parse(Buffer.from(tok.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
306
360
|
if (typeof payload?.exp === "number") {
|
|
307
361
|
const msLeft = payload.exp * 1000 - Date.now();
|
|
308
|
-
const days = Math.floor(msLeft / 86_400_000);
|
|
362
|
+
const days = Math.floor((msLeft + 60_000) / 86_400_000);
|
|
309
363
|
const left = msLeft < 86_400_000 ? `${Math.max(1, Math.floor(msLeft / 3_600_000))} hour(s)` : `${days} day(s)`;
|
|
310
364
|
if (msLeft < 0)
|
|
311
365
|
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.`);
|
|
312
366
|
else if (days <= 14)
|
|
313
|
-
|
|
367
|
+
warn(`agent token: expires in ${left} — mint a fresh one before it does: npx @decidio/sdk init ${env.DECIDIO_AGENT_ID ?? "<agent>"}`);
|
|
314
368
|
else
|
|
315
369
|
log(`✔ agent token: valid for another ${days} days`);
|
|
316
370
|
}
|
|
@@ -319,7 +373,7 @@ async function cmdDoctor() {
|
|
|
319
373
|
}
|
|
320
374
|
const rec = await api("/api/records");
|
|
321
375
|
if (rec.status >= 200 && rec.status < 300)
|
|
322
|
-
|
|
376
|
+
warn("token scope: SESSION/ADMIN-capable — this credential can approve and administer, and it does not belong in an agent's environment (the agent runtime needs only the floor-limited token init mints). Move it to your shell/CI secret store.");
|
|
323
377
|
else if (rec.status === 401) {
|
|
324
378
|
const probe = await api("/agent/status/doctor-scope-probe");
|
|
325
379
|
if (probe.status === 404)
|
|
@@ -327,11 +381,12 @@ async function cmdDoctor() {
|
|
|
327
381
|
else if (probe.status === 401)
|
|
328
382
|
line(false, "token: REJECTED on the agent floor too — it's invalid or expired. Re-run `npx @decidio/sdk init` to mint a fresh one.");
|
|
329
383
|
else
|
|
330
|
-
|
|
384
|
+
warn(`token scope: agent-floor probe returned ${probe.status} — inconclusive`);
|
|
331
385
|
}
|
|
332
386
|
else
|
|
333
|
-
|
|
334
|
-
|
|
387
|
+
warn(`token scope: unexpected ${rec.status} from /api/records`);
|
|
388
|
+
const warnNote = warned ? ` (${warned} warning${warned === 1 ? "" : "s"} above — working, worth a look)` : "";
|
|
389
|
+
log(ok ? `\ndoctor: all green.${warnNote}` : "\ndoctor: fix the ✘ items above, then re-run `npx @decidio/sdk doctor`.");
|
|
335
390
|
return ok ? 0 : 1;
|
|
336
391
|
}
|
|
337
392
|
async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
@@ -387,6 +442,8 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
|
387
442
|
return 1;
|
|
388
443
|
}
|
|
389
444
|
const cards = r.body?.cards ?? [];
|
|
445
|
+
if (cards.length >= 50)
|
|
446
|
+
err('⚠ this view is capped at 50 items by the server — older pending approvals may exist beyond it.');
|
|
390
447
|
const pending = cards.filter((c) => c.isAgentRequest && c.status !== "sealed");
|
|
391
448
|
const myAgent = process.env.DECIDIO_AGENT_ID;
|
|
392
449
|
const isMine = (c) => !!myAgent && typeof c.title === "string" && c.title.startsWith(`${myAgent}:`);
|
|
@@ -472,85 +529,6 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
472
529
|
}
|
|
473
530
|
return 0;
|
|
474
531
|
}
|
|
475
|
-
function parseReceiptStrict(text, file) {
|
|
476
|
-
const value = JSON.parse(text);
|
|
477
|
-
const stack = [];
|
|
478
|
-
let i = 0;
|
|
479
|
-
const readString = () => {
|
|
480
|
-
let out = "";
|
|
481
|
-
i++;
|
|
482
|
-
while (i < text.length) {
|
|
483
|
-
const ch = text[i];
|
|
484
|
-
if (ch === "\\") {
|
|
485
|
-
out += text[i] + text[i + 1];
|
|
486
|
-
i += 2;
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
if (ch === '"') {
|
|
490
|
-
i++;
|
|
491
|
-
return out;
|
|
492
|
-
}
|
|
493
|
-
out += ch;
|
|
494
|
-
i++;
|
|
495
|
-
}
|
|
496
|
-
return out;
|
|
497
|
-
};
|
|
498
|
-
while (i < text.length) {
|
|
499
|
-
const ch = text[i];
|
|
500
|
-
if (ch === '"') {
|
|
501
|
-
const s = readString();
|
|
502
|
-
let j = i;
|
|
503
|
-
while (j < text.length && /\s/.test(text[j]))
|
|
504
|
-
j++;
|
|
505
|
-
if (text[j] === ":" && stack.length > 0) {
|
|
506
|
-
const key = JSON.parse(`"${s}"`);
|
|
507
|
-
const top = stack[stack.length - 1];
|
|
508
|
-
if (top.has(key))
|
|
509
|
-
throw new Error(`${file}: duplicate JSON key ${JSON.stringify(key)} — RFC 8785 requires duplicate-free input, and different parsers disagree on which value is real`);
|
|
510
|
-
top.add(key);
|
|
511
|
-
}
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (ch === "{")
|
|
515
|
-
stack.push(new Set());
|
|
516
|
-
else if (ch === "}")
|
|
517
|
-
stack.pop();
|
|
518
|
-
i++;
|
|
519
|
-
}
|
|
520
|
-
return value;
|
|
521
|
-
}
|
|
522
|
-
function chainOf(vcs, expectedIssuer) {
|
|
523
|
-
if (!vcs.length)
|
|
524
|
-
return { valid: false, length: 0, reason: "no receipts supplied" };
|
|
525
|
-
const issuers = [...new Set(vcs.map((v) => String(v?.issuer ?? "")))];
|
|
526
|
-
if (issuers.length > 1)
|
|
527
|
-
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` };
|
|
528
|
-
let prev = null;
|
|
529
|
-
let anchor = "segment";
|
|
530
|
-
for (let i = 0; i < vcs.length; i++) {
|
|
531
|
-
const r = verifyReceiptDoor(vcs[i], expectedIssuer);
|
|
532
|
-
if (!r.valid)
|
|
533
|
-
return { valid: false, length: i, reason: `receipt ${i} (${r.recordId ?? "?"}) invalid: ${r.reason}` };
|
|
534
|
-
const cs = vcs[i]?.credentialSubject ?? {};
|
|
535
|
-
if (i === 0)
|
|
536
|
-
anchor = (cs.prevHash ?? null) === null ? "genesis" : "segment";
|
|
537
|
-
else if ((cs.prevHash ?? null) !== prev)
|
|
538
|
-
return { valid: false, length: i, reason: `receipt ${i} does not follow receipt ${i - 1} (prevHash mismatch)` };
|
|
539
|
-
prev = cs.contentHash;
|
|
540
|
-
}
|
|
541
|
-
return { valid: true, length: vcs.length, anchor };
|
|
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
|
-
}
|
|
554
532
|
function cmdVerify(argsIn) {
|
|
555
533
|
let expectedIssuer;
|
|
556
534
|
const files = [];
|
|
@@ -582,38 +560,20 @@ function cmdVerify(argsIn) {
|
|
|
582
560
|
err(" Verifies Authority Receipts OFFLINE — signature, content binding, and (for several files) chain order.");
|
|
583
561
|
return 2;
|
|
584
562
|
}
|
|
585
|
-
const texts =
|
|
563
|
+
const texts = new Map();
|
|
586
564
|
for (const f of files) {
|
|
587
565
|
try {
|
|
588
|
-
texts.
|
|
566
|
+
texts.set(f, readFileSync(f, "utf8"));
|
|
589
567
|
}
|
|
590
568
|
catch (e) {
|
|
591
569
|
err(`✘ cannot read ${f}: ${e?.code === "ENOENT" ? "no such file" : e?.message ?? e} (nothing was verified)`);
|
|
592
570
|
return 1;
|
|
593
571
|
}
|
|
594
572
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
catch (e) {
|
|
600
|
-
log(`✘ INVALID ${String(e?.message ?? e)}`);
|
|
601
|
-
return 1;
|
|
602
|
-
}
|
|
603
|
-
let allOk = true;
|
|
604
|
-
vcs.forEach((vc, i) => {
|
|
605
|
-
const res = verifyReceiptDoor(vc, expectedIssuer);
|
|
606
|
-
allOk = allOk && res.valid;
|
|
607
|
-
log(`${res.valid ? "✔ VALID " : "✘ INVALID"} ${files[i]}`);
|
|
608
|
-
log(` record: ${res.recordId ?? "?"} issuer: ${res.issuer}${expectedIssuer ? "" : " (unpinned — pass --issuer to bind to your workspace's DID)"}`);
|
|
609
|
-
log(` content-bound: ${res.contentBound}${res.reason ? ` reason: ${res.reason}` : ""}`);
|
|
610
|
-
});
|
|
611
|
-
if (vcs.length > 1) {
|
|
612
|
-
const c = chainOf(vcs, expectedIssuer);
|
|
613
|
-
allOk = allOk && c.valid;
|
|
614
|
-
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}` : ""}`);
|
|
615
|
-
}
|
|
616
|
-
return allOk ? 0 : 1;
|
|
573
|
+
const { ok, lines } = runVerify(files, expectedIssuer, (f) => texts.get(f));
|
|
574
|
+
for (const l of lines)
|
|
575
|
+
log(l);
|
|
576
|
+
return ok ? 0 : 1;
|
|
617
577
|
}
|
|
618
578
|
async function cmdQuickstart(opts = {}) {
|
|
619
579
|
const t0 = Date.now();
|
|
@@ -654,8 +614,9 @@ async function cmdQuickstart(opts = {}) {
|
|
|
654
614
|
const prev = readFileSync(envFile, "utf8");
|
|
655
615
|
const prevId = prev.match(/^DECIDIO_AGENT_ID=(.+)$/m)?.[1];
|
|
656
616
|
if (prevId && prevId !== agentId) {
|
|
657
|
-
|
|
658
|
-
|
|
617
|
+
const bak = `.env.quickstart.${prevId.replace(/[^\w.-]/g, "_")}.bak`;
|
|
618
|
+
writeSecretFile(bak, prev);
|
|
619
|
+
log(` ✔ previous tour agent (${prevId}) backed up to ${bak} — its key would otherwise be unrecoverable (one backup PER agent: serial tours never clobber an earlier backup)`);
|
|
659
620
|
}
|
|
660
621
|
}
|
|
661
622
|
writeSecretFile(envFile, Object.entries(envOut).map(([k, v]) => `${k}=${v}`).join("\n") + "\n");
|
|
@@ -714,7 +675,10 @@ async function cmdQuickstart(opts = {}) {
|
|
|
714
675
|
}
|
|
715
676
|
else {
|
|
716
677
|
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
|
|
678
|
+
log(" stops here rather than approve its own request. The decision is routed and waiting —");
|
|
679
|
+
log(" but note: this BLOCKING demo process is about to exit, so approving later resolves the");
|
|
680
|
+
log(" RECORD only; the demo function itself cannot run afterwards (durable mode is what");
|
|
681
|
+
log(" survives a process exit — the README's worker section shows it):");
|
|
718
682
|
log("");
|
|
719
683
|
log(` npx @decidio/sdk approvals approve ${decisionId} (or approve it in your Decidio app)`);
|
|
720
684
|
log("");
|
|
@@ -794,7 +758,9 @@ async function quickstartCleanup(session, envFile) {
|
|
|
794
758
|
})();
|
|
795
759
|
if (env.DECIDIO_TOKEN_JTI) {
|
|
796
760
|
const rev = await api(`/api/workspace/agent-token/${encodeURIComponent(env.DECIDIO_TOKEN_JTI)}`, { method: "DELETE" }, session);
|
|
797
|
-
if (rev.status >= 200 && rev.status < 300)
|
|
761
|
+
if (rev.status >= 200 && rev.status < 300 && rev.body?.changed === false)
|
|
762
|
+
log(` • token was already revoked (jti ${env.DECIDIO_TOKEN_JTI}) — nothing to do.`);
|
|
763
|
+
else if (rev.status >= 200 && rev.status < 300)
|
|
798
764
|
log(` ✔ token revoked (jti ${env.DECIDIO_TOKEN_JTI})`);
|
|
799
765
|
else
|
|
800
766
|
log(` ⚠ token revoke returned ${rev.status} — it still expires on its own within 7 days. List tokens: npx @decidio/sdk tokens list`);
|
|
@@ -830,9 +796,9 @@ async function quickstartCleanup(session, envFile) {
|
|
|
830
796
|
log(` and the agent's registration row (inert without key or token; server-side deregistration is on the roadmap).`);
|
|
831
797
|
return 0;
|
|
832
798
|
}
|
|
833
|
-
async function cmdTokens(sub, jti) {
|
|
799
|
+
async function cmdTokens(sub, jti, mineOnly = false) {
|
|
834
800
|
if (sub !== "list" && sub !== "revoke") {
|
|
835
|
-
err("usage: npx @decidio/sdk tokens list");
|
|
801
|
+
err("usage: npx @decidio/sdk tokens list [--mine]");
|
|
836
802
|
err(" npx @decidio/sdk tokens revoke <jti>");
|
|
837
803
|
err(" Agent tokens are shown by jti (their revocation id) — the token VALUE is never shown again after minting.");
|
|
838
804
|
return 2;
|
|
@@ -841,6 +807,11 @@ async function cmdTokens(sub, jti) {
|
|
|
841
807
|
err("usage: npx @decidio/sdk tokens revoke <jti> (find the jti with: npx @decidio/sdk tokens list)");
|
|
842
808
|
return 2;
|
|
843
809
|
}
|
|
810
|
+
if (mineOnly && !process.env.DECIDIO_AGENT_ID) {
|
|
811
|
+
err(`✘ --mine needs an agent identity, and there is no DECIDIO_AGENT_ID in this directory's .env or your environment.`);
|
|
812
|
+
err(` Run this from your agent's directory (where init wrote .env), or drop --mine for the workspace-wide list.`);
|
|
813
|
+
return 2;
|
|
814
|
+
}
|
|
844
815
|
let bearer;
|
|
845
816
|
let r = await api("/api/workspace/agent-token");
|
|
846
817
|
if (r.status === 401 || r.status === 403) {
|
|
@@ -873,14 +844,16 @@ async function cmdTokens(sub, jti) {
|
|
|
873
844
|
err(`✘ could not list tokens (${r.status}): ${JSON.stringify(r.body).slice(0, 300)}`);
|
|
874
845
|
return 1;
|
|
875
846
|
}
|
|
876
|
-
const
|
|
847
|
+
const all = r.body?.tokens ?? r.body ?? [];
|
|
848
|
+
const myAgent = process.env.DECIDIO_AGENT_ID;
|
|
849
|
+
const tokens = mineOnly ? all.filter((t) => t.agentId === myAgent) : all;
|
|
877
850
|
if (!Array.isArray(tokens) || !tokens.length) {
|
|
878
|
-
log("No agent tokens in this workspace.");
|
|
851
|
+
log(mineOnly ? `No agent tokens for "${myAgent}"${all.length ? ` (${all.length} elsewhere in this workspace — drop --mine to see them)` : ""}.` : "No agent tokens in this workspace.");
|
|
879
852
|
return 0;
|
|
880
853
|
}
|
|
881
854
|
const status = (t) => t.revokedAt ? `REVOKED ${String(t.revokedAt).slice(0, 10)}`
|
|
882
855
|
: (t.expiresAt && Date.parse(t.expiresAt) < Date.now()) ? "expired" : "active";
|
|
883
|
-
log(`Agent tokens (${tokens.length}):`);
|
|
856
|
+
log(`Agent tokens (${tokens.length}${mineOnly ? ` for "${myAgent}"` : ""})${!mineOnly && myAgent ? ` — workspace-wide (a shared workspace shows every member's agents; --mine narrows to this directory's agent, ${myAgent})` : ""}:`);
|
|
884
857
|
for (const t of tokens)
|
|
885
858
|
log(` ${t.jti} ${status(t)} agent=${t.agentId ?? "?"} label=${t.label ?? ""} expires=${t.expiresAt ?? "?"}`);
|
|
886
859
|
const al = r.body?.allowance;
|
|
@@ -921,16 +894,18 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
921
894
|
npx @decidio/sdk quickstart
|
|
922
895
|
|
|
923
896
|
It gates a demo action, routes it to YOU for approval, executes on your yes, and hands you a
|
|
924
|
-
cryptographically verifiable receipt
|
|
925
|
-
|
|
897
|
+
cryptographically verifiable receipt of the DECISION (execution evidence then accrues on the
|
|
898
|
+
hosted record — the receipt seals who authorized what, not what ran afterward). Guided and
|
|
899
|
+
explicit — this CLI never acts, signs you in, or writes files unless you ask it to.
|
|
926
900
|
|
|
927
901
|
npx @decidio/sdk quickstart the guided two-minute tour: gate → approve → execute → proof
|
|
928
902
|
(interactive: YOUR Enter is the approval. Headless it stops
|
|
929
903
|
at the routed decision; --auto-approve-demo for CI, labeled
|
|
930
904
|
automated; ends with a keep-or-cleanup choice)
|
|
931
905
|
npx @decidio/sdk quickstart cleanup revoke the tour's token + delete its local credentials
|
|
932
|
-
npx @decidio/sdk tokens list
|
|
933
|
-
token values are never re-shown
|
|
906
|
+
npx @decidio/sdk tokens list [--mine] agent tokens with status (active/expired/REVOKED) by
|
|
907
|
+
jti — token values are never re-shown; the list is
|
|
908
|
+
workspace-wide, --mine narrows to this directory's agent
|
|
934
909
|
npx @decidio/sdk tokens revoke <jti> kill a token now instead of waiting out its lifetime
|
|
935
910
|
npx @decidio/sdk init [agentId] sign in, register the agent, mint its API token, write .env
|
|
936
911
|
--unique append a random suffix (collision-proof samples)
|
|
@@ -946,11 +921,14 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
946
921
|
npx @decidio/sdk dev [--target URL] [--port N] local relay for the resume webhook
|
|
947
922
|
npx @decidio/sdk --version print the CLI version
|
|
948
923
|
|
|
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.
|
|
924
|
+
Every NETWORK command talks to the hosted sandbox (https://decidio-api.onrender.com) unless
|
|
925
|
+
DECIDIO_API_URL says otherwise; the target host is announced on stderr each run. "verify" is
|
|
926
|
+
fully offline — it touches no network and reads no .env.
|
|
951
927
|
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
|
|
953
|
-
|
|
928
|
+
Errors, and usage shown on ERROR paths, go to stderr; stdout carries results only (safe to
|
|
929
|
+
pipe). Asked-for help — including this bare invocation — IS a result, so it prints here.
|
|
930
|
+
Docs: the README that shipped with this package — node_modules/@decidio/sdk/README.md
|
|
931
|
+
(quickstart, durable resume, workers, adapters).`;
|
|
954
932
|
async function main(argv) {
|
|
955
933
|
const [cmd, a, b, ...rest] = argv.slice(2);
|
|
956
934
|
const wantsHelp = (s) => s === "help" || s === "--help" || s === "-h";
|
|
@@ -974,7 +952,7 @@ async function main(argv) {
|
|
|
974
952
|
approvals: { flags: ["--json", "--mine"] },
|
|
975
953
|
receipt: { flags: ["--json"] },
|
|
976
954
|
verify: { flags: [], valued: ["--issuer"] },
|
|
977
|
-
tokens: { flags: [] },
|
|
955
|
+
tokens: { flags: ["--mine"] },
|
|
978
956
|
dev: { flags: [], valued: ["--target", "--port"] },
|
|
979
957
|
};
|
|
980
958
|
const spec = KNOWN[cmd];
|
|
@@ -1057,7 +1035,7 @@ async function main(argv) {
|
|
|
1057
1035
|
case "verify": return cmdVerify(argsIn);
|
|
1058
1036
|
case "tokens": {
|
|
1059
1037
|
const positional = argsIn.filter((x) => !x.startsWith("-"));
|
|
1060
|
-
return cmdTokens(positional[0], positional[1]);
|
|
1038
|
+
return cmdTokens(positional[0], positional[1], argsIn.includes("--mine"));
|
|
1061
1039
|
}
|
|
1062
1040
|
case "dev": {
|
|
1063
1041
|
const flag = (name) => {
|
|
@@ -1080,7 +1058,10 @@ async function main(argv) {
|
|
|
1080
1058
|
return cmdDev(target, Number(portRaw));
|
|
1081
1059
|
}
|
|
1082
1060
|
default:
|
|
1083
|
-
|
|
1061
|
+
if (cmd.startsWith("-"))
|
|
1062
|
+
err(`✘ unknown command "${cmd}" — flags go AFTER the command (e.g. npx @decidio/sdk approvals ${cmd}).\n\n${USAGE}`);
|
|
1063
|
+
else
|
|
1064
|
+
err(`✘ unknown command "${cmd}"\n\n${USAGE}`);
|
|
1084
1065
|
return 2;
|
|
1085
1066
|
}
|
|
1086
1067
|
}
|
package/dist/guard.d.ts
CHANGED
|
@@ -8,25 +8,52 @@ 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. */
|
|
11
13
|
agentKey?: string;
|
|
12
14
|
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. */
|
|
13
17
|
workspaceId?: string;
|
|
18
|
+
/** Base URL of the Decidio app (DECIDIO_APP_URL) — used to deep-link a routed decision. */
|
|
14
19
|
appUrl?: string;
|
|
15
20
|
fetchImpl?: typeof fetch;
|
|
16
21
|
}
|
|
17
22
|
export interface ProtectOptions {
|
|
18
|
-
|
|
23
|
+
/** null/"generic" → the core gate; engine adapters (langgraph/openai/temporal/inngest)
|
|
24
|
+
* live in @decidio/sdk subpaths and take the engine handle. */
|
|
25
|
+
adapter?: "generic" | "langgraph" | "openai" | "temporal" | "inngest";
|
|
26
|
+
/** "durable" (default) suspends for async approval; "blocking" polls (short approvals only). */
|
|
19
27
|
mode?: "durable" | "blocking";
|
|
28
|
+
/** The action name this wrapper produces, declared UP FRONT so the resume registry is populated
|
|
29
|
+
* at WRAP time rather than on first call. Required for the cold-worker pattern: after a
|
|
30
|
+
* restart, `guard.worker()` can only re-execute actions registered in THIS process, and
|
|
31
|
+
* `describe` (which names the action) cannot run without the original call arguments. Declare
|
|
32
|
+
* it and a restarted process resumes work it never originated. */
|
|
20
33
|
action?: string;
|
|
34
|
+
/** Blocking-mode poll ceiling in ms (default 600000 = 10 min). Set it here so a CI pipeline
|
|
35
|
+
* fails fast instead of blocking the full 10-minute default while a routed action waits.
|
|
36
|
+
* Accepted identically by the Python SDK (poll_timeout, seconds). */
|
|
37
|
+
pollTimeoutMs?: number;
|
|
38
|
+
/** Blocking-mode poll interval in ms (default 3000). */
|
|
39
|
+
pollIntervalMs?: number;
|
|
21
40
|
}
|
|
22
41
|
export declare class Guard {
|
|
23
42
|
config: GuardFacadeConfig;
|
|
24
43
|
resume?: ResumeController;
|
|
25
44
|
constructor(config: GuardFacadeConfig, resume?: ResumeController);
|
|
26
45
|
static fromEnv(): Guard;
|
|
46
|
+
/** Register a raw fn under its action name so a COLD process (one that never made the original
|
|
47
|
+
* call) can resume it. Call this at startup beside `guard.worker()` — or pass
|
|
48
|
+
* `{ action }` to `protect(...)`, which does it for you at wrap time. */
|
|
27
49
|
register(action: string, fn: (...a: any[]) => Promise<any>): void;
|
|
50
|
+
/** Mount on a node/Express/Fastify route: `app.post("/decidio/resume", guard.resumeHandler())`. */
|
|
28
51
|
resumeHandler(): import("http").RequestListener;
|
|
52
|
+
/** Mount on a Next.js/Hono/CF/Bun route: `export const POST = (req) => guard.resumeFetchHandler()(req)`. */
|
|
29
53
|
resumeFetchHandler(): (req: Request) => Promise<Response>;
|
|
54
|
+
/** Durable resume with NOTHING to host: a background watcher polls Decidio outward and finishes
|
|
55
|
+
* any parked approval — so a suspended action survives a process restart. One line at startup.
|
|
56
|
+
* Production may swap this for the webhook (resumeUrl); the suspend/resume shape is identical. */
|
|
30
57
|
worker(opts?: {
|
|
31
58
|
intervalMs?: number;
|
|
32
59
|
timeoutMs?: number;
|
|
@@ -35,6 +62,10 @@ export declare class Guard {
|
|
|
35
62
|
stop: () => void;
|
|
36
63
|
};
|
|
37
64
|
private ensureResume;
|
|
65
|
+
/** Throws when the facade has no gate URL — at USE time, not import time, so `import { guard }`
|
|
66
|
+
* stays safe in envless contexts. */
|
|
67
|
+
private assertConfigured;
|
|
38
68
|
protect<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<R>;
|
|
39
69
|
}
|
|
70
|
+
/** Preconfigured singleton — `import { guard } from "@decidio/sdk"`. */
|
|
40
71
|
export declare const guard: Guard;
|