@decidio/sdk 0.6.3 → 0.6.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ the gate, signing, receipts and the verifier - is conformance-locked between the
5
5
  first-run surfaces (the fork, `--demo`, the code sign-in) are TypeScript-first and named as such in the parity
6
6
  allowlist. Each entry names what changed, what a reader must do on upgrade, and what proved it.
7
7
 
8
+ ## 0.6.4 — the request closes when the agent gives up
9
+
10
+ - **The README links the agent skill file** published on the site (https://decidioai.com/skills/decidio-guard.md, one page for both languages) and the site's llms.txt. The file is served by the site, not shipped in the package; an in-package copy is a one-line publish-boundary decision for this release if wanted.
11
+ - **A blocking wait that hits `pollTimeoutMs` now WITHDRAWS its request** (`POST /agent/withdraw/<id>`
12
+ `{reason:"timeout"}`) before throwing `DecidioTimeoutError`. Before, the decision stayed open in Decidio
13
+ forever after this process had stopped waiting, so the queue carried a request nobody could act on and a
14
+ later tap sealed "approved" for an action that never ran. Now it leaves the queue as *withdrawn* and a
15
+ later approval is a no-op (`alreadyResolved`). `DecidioTimeoutError.withdrawn` says whether the call
16
+ landed; a failed withdraw changes nothing else (nothing executes either way). Withdrawing needs the
17
+ bound agent token and the signing key `init` writes — the gate refuses anyone else with 404, so an
18
+ unbound or unsigned caller simply sees `withdrawn:false`. If a human decided in the last poll window,
19
+ the gate answers 409 with that decision and the SDK honours it: the action runs as approved (and
20
+ confirms), or `DecidioRejectedError` is thrown — never a timeout that hides a decision. The durable
21
+ path is untouched — its parked action is exactly what a later approval resumes. The CLI tour and the
22
+ `--demo` stand-in say which of these happened. Nothing to do on upgrade.
23
+ Proved by: `smoke.ts` (the timeout vector now asserts exactly one withdraw after the last poll, and a
24
+ failing withdraw still ends in the same error with `withdrawn:false`; a 409 carrying `approved` runs and
25
+ confirms, `rejected` rejects; a wait that ends in a decision never withdraws) and, server-side,
26
+ `integration.smoke.ts` #W2 (a lab-posture boot with real bound tokens: another agent's token, an
27
+ anonymous caller and an unsigned receipt all get 404; the owner's later approve is `alreadyResolved`
28
+ with no sealed record; a second withdraw → 409) and #W (IdP posture: anonymous and IdP users → 404).
29
+
8
30
  ## 0.6.3 — the two 0.6.2 reviews' small items
9
31
 
10
32
  - **`DECIDIO_DEBUG_INPUT=1` no longer prints what you type** (review: Medium). The 0.6.2 trace hex-dumped every stdin
package/README.md CHANGED
@@ -680,7 +680,7 @@ manager for production.
680
680
  - `DecidioBlockedError` — policy blocked the action: a named block rule, or the universal backstop that fires when NO active policy governs agents at all (fail closed, no human involved). Distinct from routing-by-default — with an active policy and no matching auto-approve rule, a request is not blocked, it is ROUTED to a person.
681
681
  - `DecidioRejectedError` — a human rejected the routed action.
682
682
  - `DecidioSuspendedError` — (durable mode) the action was routed and is parked for async approval; the process may exit and resume later. Not a failure.
683
- - `DecidioTimeoutError` — (blocking mode only) no human decided within `pollTimeoutMs` (default 10 min); the decision stays open in Decidio, nothing executed locally.
683
+ - `DecidioTimeoutError` — (blocking mode only) no human decided within `pollTimeoutMs` (default 10 min); nothing executed locally. Since 0.6.4 the SDK's last act before throwing is to tell Decidio it gave up, so the request is closed as **withdrawn** (it leaves the queue; a later approval does nothing) — `error.withdrawn` is `true` when that landed and `false` when the call failed and the request may still be open. Withdrawing needs the bound agent token and the signing key `init` writes (an unbound or unsigned caller cannot withdraw anything — it just gets `withdrawn:false`). If a human decided in the last poll window, the SDK honours that decision instead: the action runs as approved, or `DecidioRejectedError` is thrown. The durable path never withdraws: its parked action is what a later approval resumes.
684
684
  - `DecidioRateLimitError` — the workspace's governed-action limit was hit; carries the server's `limit` / `remaining` / `resetAt`.
685
685
  - `DecidioHttpError` — the gate refused the call. Carries `status`, `endpoint`, the parsed `body`,
686
686
  and **`retryable`** — the field to branch on. A 401/403 is terminal (the agent token expired, or
@@ -744,6 +744,7 @@ the next reader grades a promise, not a moving target:
744
744
 
745
745
  ## Notes
746
746
 
747
+ - **Coding agents and assistants:** a one-page skill file for adding the gate to an agent — TypeScript and Python on the same page, what the gate does and does not do, the wrapper, first run vs production, the typed errors, how to verify a receipt — is published at https://decidioai.com/skills/decidio-guard.md (the site summary for machines is https://decidioai.com/llms.txt).
747
748
  - **Framework-agnostic:** it wraps at the tool-call boundary, so it works with any agent runtime (LangGraph, OpenAI Agents SDK, CrewAI, a raw tool loop) and any action (Salesforce, M365, SAP, a DB write).
748
749
  - **Confirmation integrity:** the wrapper runs in your process, so it captures the genuine API response — a materially stronger signal than an agent self-report, but it trusts the host. For high-assurance, Decidio can *independently* read the source back (`independently_reconciled`) where it holds a read connection.
749
750
  - **Zero-code alternative:** an agent that speaks MCP (e.g. Claude on the web) can reach the same gate via Decidio's `request_approval` MCP tool with no code at all.
package/dist/cli.js CHANGED
@@ -1610,8 +1610,12 @@ async function cmdQuickstartInner(opts = {}) {
1610
1610
  log(`③ the tour's blocking call gave up after 5 minutes while waiting for your answer, so your answer is NOT posted:`);
1611
1611
  if (DEMO)
1612
1612
  log(` the demo ends here; nothing is kept (the demo's agent, token and folder go now). Re-run and answer within 5 minutes.`);
1613
+ else if (earlyError.withdrawn) {
1614
+ log(` the request was WITHDRAWN from your queue (it no longer waits for an answer; deciding it there does nothing). Re-run the tour and answer within 5 minutes.`);
1615
+ log(` NOTE: this tour's credentials are still live in ${envFile} - remove them: npx @decidio/sdk quickstart cleanup`);
1616
+ }
1613
1617
  else {
1614
- log(` the request is still ROUTED in your queue - decide it there, or re-run the tour and answer within 5 minutes.`);
1618
+ log(` the request is still ROUTED in your queue (the withdraw call did not land) - decide it there, or re-run the tour and answer within 5 minutes.`);
1615
1619
  log(` NOTE: this tour's credentials are still live in ${envFile} - remove them: npx @decidio/sdk quickstart cleanup`);
1616
1620
  }
1617
1621
  log("");
package/dist/demo-gate.js CHANGED
@@ -35,6 +35,7 @@ export async function startDemoGate(opts = {}) {
35
35
  const issuerDid = "did:key:z" + b58(Buffer.concat([Buffer.from([0xed, 0x01]), issuerRaw]));
36
36
  const sealedAt = new Date().toISOString();
37
37
  let verdict = null;
38
+ let withdrawnDemo = false;
38
39
  let decidedBy = "the human at the terminal";
39
40
  let lastReason = "quickstart";
40
41
  let relayCode = null;
@@ -121,8 +122,16 @@ export async function startDemoGate(opts = {}) {
121
122
  lastReason = "decided on the demo page";
122
123
  }
123
124
  }
125
+ if (withdrawnDemo && !verdict)
126
+ return send({ state: "rejected", reason: "quickstart: withdrawn — the tour stopped waiting" });
124
127
  return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver" } : { state: verdict === "approve" ? "approved" : "pending" });
125
128
  }
129
+ if (path.startsWith("/agent/withdraw/")) {
130
+ if (verdict)
131
+ return send({ code: "NOT_OPEN", state: verdict === "approve" ? "approved" : "rejected", decisionId: DECISION_ID }, 409);
132
+ withdrawnDemo = true;
133
+ return send({ ok: true, withdrawn: true, state: "rejected", decisionId: DECISION_ID, reason: "requester_stopped_waiting" });
134
+ }
126
135
  if (/^\/api\/decisions\/[^/]+\/(approve|reject)$/.test(path)) {
127
136
  const already = verdict !== null;
128
137
  verdict = path.endsWith("/reject") ? "reject" : "approve";
package/dist/guard.js CHANGED
@@ -65,8 +65,8 @@ export class Guard {
65
65
  if ((opts?.mode ?? "durable") === "blocking") {
66
66
  const ceilingMs = opts?.pollTimeoutMs ?? base.pollTimeoutMs ?? 600_000;
67
67
  console.warn(`[decidio] WARNING: blocking wait — this call gives up after ${Math.round(ceilingMs / 1000)}s ` +
68
- `(pollTimeoutMs), and when it does, the decision stays OPEN in Decidio while this process stops waiting: ` +
69
- `a later approval cannot resume it. Only safe for short, watched approvals — omit mode:"blocking" for ` +
68
+ `(pollTimeoutMs), and when it does, this process stops waiting and the request is withdrawn in Decidio: ` +
69
+ `a later approval does nothing. Only safe for short, watched approvals — omit mode:"blocking" for ` +
70
70
  `durable async resume on real (minutes-to-days) approvals.`);
71
71
  return withApproval(fn, describe, base, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
72
72
  }
package/dist/index.d.ts CHANGED
@@ -82,7 +82,12 @@ export declare class DecidioRejectedError extends Error {
82
82
  }
83
83
  export declare class DecidioTimeoutError extends Error {
84
84
  decisionId: string;
85
- constructor(decisionId: string);
85
+ withdrawn: boolean;
86
+ /** `withdrawn` (0.6.4+): true when the SDK told Decidio it gave up and the request was closed as
87
+ * WITHDRAWN (the queue no longer carries it; a later approval does nothing); false when that
88
+ * best-effort call failed, so the request may still be open in Decidio. Nothing was executed
89
+ * locally either way. */
90
+ constructor(decisionId: string, withdrawn?: boolean);
86
91
  }
87
92
  /** A transport failure with GUIDANCE instead of a raw fetch stack. Carries the endpoint, the
88
93
  * operation, the underlying code, and the safety state a caller cares about most: whether
package/dist/index.js CHANGED
@@ -31,9 +31,11 @@ export class DecidioRejectedError extends Error {
31
31
  }
32
32
  export class DecidioTimeoutError extends Error {
33
33
  decisionId;
34
- constructor(decisionId) {
34
+ withdrawn;
35
+ constructor(decisionId, withdrawn = false) {
35
36
  super(`Timed out waiting for human approval (${decisionId})`);
36
37
  this.decisionId = decisionId;
38
+ this.withdrawn = withdrawn;
37
39
  this.name = "DecidioTimeoutError";
38
40
  }
39
41
  }
@@ -192,8 +194,24 @@ export function withApproval(fn, describe, config, opts) {
192
194
  if (st.state === "blocked")
193
195
  throw new DecidioBlockedError(st.reason ?? "blocked", auth.decisionId);
194
196
  if (Date.now() > deadline) {
195
- console.warn(`[decidio] blocking wait hit its ${Math.round(pollTimeout / 1000)}s limit — decision ${auth.decisionId} is still open in Decidio, but this process stopped waiting and nothing was executed. Re-run to request again, or use durable resume (guard.worker()) so an approval is never lost.`);
196
- throw new DecidioTimeoutError(auth.decisionId);
197
+ let withdrawn = false;
198
+ let lateState = null;
199
+ try {
200
+ await post(config, `/agent/withdraw/${encodeURIComponent(auth.decisionId)}`, { reason: "timeout" });
201
+ withdrawn = true;
202
+ }
203
+ catch (e) {
204
+ if (e instanceof DecidioHttpError && e.status === 409 && typeof e.body?.state === "string")
205
+ lateState = e.body.state;
206
+ }
207
+ if (lateState === "approved" || lateState === "auto_approved")
208
+ break;
209
+ if (lateState === "rejected")
210
+ throw new DecidioRejectedError("rejected by approver", auth.decisionId);
211
+ if (lateState === "blocked")
212
+ throw new DecidioBlockedError("blocked", auth.decisionId);
213
+ console.warn(`[decidio] blocking wait hit its ${Math.round(pollTimeout / 1000)}s limit — decision ${auth.decisionId} ${withdrawn ? "was withdrawn in Decidio (the queue no longer shows it; a later approval does nothing)" : "is still open in Decidio (the withdraw call failed), but this process stopped waiting"} and nothing was executed. Re-run to request again, or use durable resume (guard.worker()) so an approval is never lost.`);
214
+ throw new DecidioTimeoutError(auth.decisionId, withdrawn);
197
215
  }
198
216
  await sleep(pollInterval);
199
217
  }
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.6.3 # tracks the SDK release - checked by packages/conformance/manifests.mjs
8
+ version: 0.6.4 # tracks the SDK release - checked by packages/conformance/manifests.mjs
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.6.3",
3
+ "version": "0.6.4",
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",
@@ -84,7 +84,7 @@
84
84
  "prepublishOnly": "npm run typecheck && npm run build && node ../publish-boundary/check.mjs --only @decidio/sdk"
85
85
  },
86
86
  "dependencies": {
87
- "@decidio/verify": "0.6.3"
87
+ "@decidio/verify": "0.6.4"
88
88
  },
89
89
  "author": "OmniTwin Technologies Inc.",
90
90
  "bugs": {