@decidio/sdk 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,16 @@ is sent), registers the agent, mints its **floor-limited API token** (it can req
29
29
  and nothing else — your workspace session never touches the agent's disk), and writes `.env`.
30
30
  Every later command reads `.env` from this directory automatically.
31
31
 
32
+ What `.env` contains after a successful init: `DECIDIO_API_URL`, `DECIDIO_AGENT_ID`,
33
+ `DECIDIO_AGENT_DID` + `DECIDIO_AGENT_KEY` (the local keypair — the key is what earns
34
+ `application_confirmed` evidence later), `DECIDIO_API_TOKEN` (the floor-limited agent token),
35
+ `DECIDIO_WORKSPACE_ID`, and `DECIDIO_WEBHOOK_SECRET` (operator deployments only). Pick any
36
+ agent name — if it's already taken in your workspace, `init` stops cleanly and suggests a
37
+ fresh one. Headless terminal (no prompts possible)? Export `DECIDIO_SESSION_TOKEN` first:
38
+ request a sign-in link, then exchange its `?login=` value at `POST /demo/login-consume` — the
39
+ response's `token` is your session token (the CLI prints these exact instructions when it
40
+ can't prompt).
41
+
32
42
  **3. Protect one function** — save this as `quickstart.mjs`:
33
43
 
34
44
  ```js
@@ -77,7 +87,7 @@ becomes the record's execution evidence.
77
87
 
78
88
  ```bash
79
89
  npx @decidio/sdk receipt <decisionId>
80
- npx @decidio/verify decidio-receipt-<decisionId>.json
90
+ npx --yes @decidio/verify decidio-receipt-<decisionId>.json
81
91
  ```
82
92
 
83
93
  The receipt is a W3C Verifiable Credential: the signature and content binding verify with no
@@ -88,12 +98,15 @@ record verify panel). One receipt proves itself; chain order across receipts is
88
98
  you pass several files at once.
89
99
 
90
100
  What the credential contains, honestly: the sealed **authority decision** — who decided, what
91
- was authorized, when, under which policy. It seals at decision time and is immutable, so
92
- execution evidence that arrives *after* the seal (your function's captured result) lives on the
93
- **record**, not inside the credential bytes: your Decidio record page shows the evidence tier
94
- (`application_confirmed` once the wrapper's signed report lands). Immutable seal + accruing
95
- evidence is the design, not an omission — corrections and later facts supersede, they never
96
- rewrite.
101
+ was authorized, when, under which policy. It seals at decision time and is immutable — which is
102
+ why the credential's own `confirmation.status` field reads `"pending"` forever. Execution
103
+ evidence that arrives *after* the seal (your function's captured result) lives on the
104
+ **record**, not inside the credential bytes: the `receipt` command prints the record's current
105
+ tier next to the file, and with the signing key `init` wrote you'll see `application_confirmed`
106
+ — the wrapper's report was cryptographically bound to your agent. (No signing key configured?
107
+ The server caps the evidence at the weaker `agent_asserted`, and the SDK warns at confirm
108
+ time.) Immutable seal + accruing evidence is the design, not an omission — corrections and
109
+ later facts supersede, they never rewrite.
97
110
 
98
111
  **7. Prove a replay is rejected:**
99
112
 
@@ -32,6 +32,9 @@ export async function gateConfirm(config, decisionId, result) {
32
32
  // Sign the confirm too (bound to {action:"confirm", scope:decisionId}) so the adapter path
33
33
  // earns application_confirmed; without a key the server caps it at agent_asserted.
34
34
  const requesterIdentity = signConfirm(decisionId, config);
35
+ // Same no-silent-downgrade rule as the inline wrap (guard equivalent-path catch).
36
+ if (!requesterIdentity)
37
+ console.warn(`[decidio] execution evidence for ${decisionId} recorded as agent_asserted — no signing key configured, so the report can't be proven to come from this agent. Set DECIDIO_AGENT_KEY/DID to earn application_confirmed.`);
35
38
  const res = await f(config.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
36
39
  method: "POST",
37
40
  headers: { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) },
package/dist/cli.js CHANGED
@@ -56,12 +56,15 @@ for (const [k, v] of Object.entries(readEnvFile())) {
56
56
  process.env[k] = v;
57
57
  }
58
58
  const API = (process.env.DECIDIO_API_URL ?? "http://localhost:4000").replace(/\/$/, "");
59
- const TOKEN = process.env.DECIDIO_API_TOKEN;
59
+ // Token is read AT CALL TIME, not captured at import: init mints a fresh agent token mid-run and
60
+ // its closing doctor must probe with THAT, not the pre-init env (round-3 Codex catch — the stale
61
+ // capture made init's own doctor print "token: REJECTED" seconds after a successful mint).
62
+ const TOKEN = () => process.env.DECIDIO_API_TOKEN;
60
63
  const ENV_PATH = ".env";
61
64
  const log = (...a) => console.log(...a);
62
65
  /** Fetch wrapper that turns transport failures into guidance instead of a stack trace. */
63
66
  async function api(path, init = {}, bearer) {
64
- const auth = bearer ?? TOKEN;
67
+ const auth = bearer ?? TOKEN();
65
68
  let res;
66
69
  try {
67
70
  res = await fetch(API + path, { ...init, headers: { "content-type": "application/json", ...(auth ? { authorization: `Bearer ${auth}` } : {}), ...(init.headers ?? {}) } });
@@ -189,18 +192,24 @@ async function cmdInit(agentId) {
189
192
  }
190
193
  let did, privateKey;
191
194
  if (r.status === 409) {
192
- // Already registered to a different (server-held) did. Reuse it; the local key we just
193
- // minted does NOT match, so don't write it — the operator must supply the existing key.
195
+ // Already registered to a server-held did. WITHOUT the original private key on this machine
196
+ // this is a HARD STOP, not a degraded continue (round-3 Codex catch: init used to write a
197
+ // keyless .env and print happy-path next steps — every request then routed unproven and
198
+ // execution evidence silently capped at agent_asserted).
194
199
  did = r.body?.did;
195
- log(`• "${agentId}" is already registered in this workspace (did ${did}) — reusing it.`);
196
- if (sourceSystem)
197
- log(` Note: sourceSystem can't change on an existing agent — the registered lane stands. To use the "${sourceSystem}" lane, register a NEW name: npx @decidio/sdk init ${agentId}-${sourceSystem}`);
198
- if (existingKey)
199
- log(` Your .env already carries a DECIDIO_AGENT_KEY; if it's this agent's original key, signing keeps working.`);
200
+ if (existingKey) {
201
+ log(`• "${agentId}" is already registered in this workspace (did ${did}) — reusing it with the DECIDIO_AGENT_KEY already in .env.`);
202
+ if (sourceSystem)
203
+ log(` Note: sourceSystem can't change on an existing agent — the registered lane stands. To use the "${sourceSystem}" lane, register a NEW name.`);
204
+ }
200
205
  else {
201
- log(` ⚠ Its private key lives wherever this agent was FIRST initialized — keys never leave the machine that made them.`);
202
- log(` Either restore that DECIDIO_AGENT_KEY into .env, or start fresh under a new name:`);
203
- log(` npx @decidio/sdk init ${agentId}-2`);
206
+ const suggestion = `${agentId}-${randomBytes(2).toString("hex")}`;
207
+ log(`✘ "${agentId}" is already registered in this workspace, and its private key is not on this machine`);
208
+ log(` (keys never leave the machine that generated them — likely a previous run, or another directory).`);
209
+ log(` Nothing was written. Two ways forward:`);
210
+ log(` 1. Fresh agent (most common): npx @decidio/sdk init ${suggestion}`);
211
+ log(` 2. Reuse "${agentId}": restore its original DECIDIO_AGENT_KEY into .env here, then re-run init.`);
212
+ return 1;
204
213
  }
205
214
  }
206
215
  else if (r.status >= 200 && r.status < 300) {
@@ -218,7 +227,7 @@ async function cmdInit(agentId) {
218
227
  // without the tester-lab fence (plain local dev) the mint endpoint refuses; that's fine —
219
228
  // local dev auths with its existing env and we say so instead of failing the init.
220
229
  let agentToken, workspaceId;
221
- const mintBearer = session ?? TOKEN;
230
+ const mintBearer = session ?? TOKEN();
222
231
  if (mintBearer) {
223
232
  const mint = await api("/api/workspace/agent-token", { method: "POST", body: JSON.stringify({ agentId, label: "sdk-cli" }) }, mintBearer);
224
233
  if (mint.status >= 200 && mint.status < 300 && mint.body?.token) {
@@ -248,6 +257,11 @@ async function cmdInit(agentId) {
248
257
  if (workspaceId)
249
258
  envOut.DECIDIO_WORKSPACE_ID = workspaceId;
250
259
  upsertEnv(envOut);
260
+ // The closing doctor (and anything else this process does) must see what init just wrote —
261
+ // OVERWRITE process.env with the fresh values (round-3 Codex catch: doctor probed with the
262
+ // stale pre-init token and reported the mint it just watched as REJECTED).
263
+ for (const [k, v] of Object.entries(envOut))
264
+ process.env[k] = v;
251
265
  log(`✔ wrote ${ENV_PATH} — every decidio command reads it from this directory (your script loads it with node --env-file=.env)`);
252
266
  ensureGitignore();
253
267
  if (privateKey)
@@ -274,7 +288,7 @@ async function cmdInit(agentId) {
274
288
  log("");
275
289
  log(` node --env-file=.env quickstart.mjs # routes to a human, prints the decision id, waits`);
276
290
  log(` npx @decidio/sdk approvals # (second terminal) see it pending`);
277
- log(` npx @decidio/sdk approvals approve <id> # approve -> your function runs -> sealed`);
291
+ log(` npx @decidio/sdk approvals approve <id> # approve -> your function runs -> evidence recorded`);
278
292
  log("");
279
293
  return cmdDoctor();
280
294
  }
@@ -305,12 +319,13 @@ async function cmdDoctor() {
305
319
  else if (rec.status === 401) {
306
320
  // A 401 off the agent floor could also mean an INVALID token (Codex catch: don't
307
321
  // false-green expiry). Disambiguate with a floor probe: a valid agent token gets 404 for an
308
- // unknown decision (authorized, not found); an invalid one gets 401 again.
322
+ // unknown decision (authorized, not found); an invalid one gets 401 again. A rejection here
323
+ // flips the overall verdict — "all green" after a red line was round 3's other doctor bug.
309
324
  const probe = await api("/agent/status/doctor-scope-probe");
310
325
  if (probe.status === 404)
311
326
  log("✔ token scope: agent floor — correct for the runtime SDK (approvals/receipt will ask you to sign in)");
312
327
  else if (probe.status === 401)
313
- log("✘ token: REJECTED on the agent floor too — it's invalid or expired. Re-run `npx @decidio/sdk init` to mint a fresh one.");
328
+ 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.");
314
329
  else
315
330
  log(`⚠ token scope: agent-floor probe returned ${probe.status} — inconclusive`);
316
331
  }
@@ -322,6 +337,9 @@ async function cmdDoctor() {
322
337
  async function cmdApprovals(sub, id, reason) {
323
338
  // Approvals are a HUMAN act on the workspace — session scope, never the agent token. If the
324
339
  // env's token is agent-floor (401 on /api/*), sign in interactively; memory-only as always.
340
+ const jsonMode = sub === "--json";
341
+ if (jsonMode)
342
+ sub = undefined;
325
343
  let bearer;
326
344
  const probe = await api("/api/decisions/cards");
327
345
  if (probe.status === 401 || probe.status === 403) {
@@ -368,6 +386,11 @@ async function cmdApprovals(sub, id, reason) {
368
386
  }
369
387
  const cards = r.body?.cards ?? [];
370
388
  const pending = cards.filter((c) => c.isAgentRequest && c.status !== "sealed");
389
+ if (jsonMode) {
390
+ // Stable machine output (round-3 ask): agents scripting the queue parse this, not prose.
391
+ 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));
392
+ return 0;
393
+ }
371
394
  if (!pending.length) {
372
395
  log("No pending agent approvals. Trigger one from your protected function, then re-run this.");
373
396
  return 0;
package/dist/index.js CHANGED
@@ -190,6 +190,10 @@ export function withApproval(fn, describe, config) {
190
190
  source: "wrapper", // the SDK captured the genuine response, not an agent self-report
191
191
  ...(confirmIdentity ? { requesterIdentity: confirmIdentity } : {}),
192
192
  });
193
+ // An UNSIGNED confirm is server-capped at the weaker agent_asserted tier. Say so at the
194
+ // moment it happens — a silent evidence downgrade was round 3's acceptance FAIL.
195
+ if (!confirmIdentity)
196
+ console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as agent_asserted — no signing key configured, so the report can't be proven to come from this agent. Set DECIDIO_AGENT_KEY/DID (npx @decidio/sdk init writes them) to earn application_confirmed.`);
193
197
  }
194
198
  catch (e) {
195
199
  // surface but don't throw — the action already happened
package/dist/resume.js CHANGED
@@ -110,6 +110,10 @@ async function confirm(cfg, decisionId, capturedResponse) {
110
110
  // Sign the confirm so the DURABLE/resume path can earn application_confirmed too (bound to
111
111
  // {action:"confirm", scope:decisionId}); without a key the server caps it at agent_asserted.
112
112
  const requesterIdentity = signConfirm(decisionId, cfg);
113
+ // Same no-silent-downgrade rule as the inline wrap (guard equivalent-path catch): the durable
114
+ // path is what production agents run, so it must disclose an agent_asserted cap too.
115
+ if (!requesterIdentity)
116
+ console.warn(`[decidio] execution evidence for ${decisionId} recorded as agent_asserted — no signing key configured, so the report can't be proven to come from this agent. Set DECIDIO_AGENT_KEY/DID to earn application_confirmed.`);
113
117
  await f(cfg.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
114
118
  method: "POST",
115
119
  headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
package/openapi.yaml CHANGED
@@ -5,7 +5,7 @@
5
5
  openapi: 3.1.0
6
6
  info:
7
7
  title: Decidio Agent Gate
8
- version: 0.1.2
8
+ version: 0.1.3
9
9
  description: >
10
10
  Authorize an AI-agent action (proceed | route | block), confirm the captured outcome, and
11
11
  receive the signed resume webhook when a routed action is decided. Every outcome seals a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decidio/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "One-line approval gate for AI-agent actions. Wrap a risky tool call with guard.protect(); Decidio's policy engine decides proceed | route | block, a human approves routed actions, and every outcome is sealed as a verifiable Authority Receipt.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",