@decidio/sdk 0.10.0 → 0.12.0
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 +113 -3
- package/README.md +18 -9
- package/dist/adapters/_gate.d.ts +18 -1
- package/dist/adapters/_gate.js +34 -27
- package/dist/adapters/langgraph.js +1 -1
- package/dist/adapters/openai.d.ts +11 -4
- package/dist/adapters/openai.js +12 -4
- package/dist/adapters/temporal.d.ts +29 -2
- package/dist/adapters/temporal.js +42 -6
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +623 -96
- package/dist/demo-gate.js +14 -0
- package/dist/errors.js +3 -1
- package/dist/guard.d.ts +5 -0
- package/dist/guard.js +3 -2
- package/dist/index.d.ts +39 -7
- package/dist/index.js +103 -20
- package/dist/inngest.d.ts +5 -0
- package/dist/inngest.js +2 -2
- package/dist/resume.d.ts +52 -2
- package/dist/resume.js +74 -5
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +6 -0
- package/openapi.yaml +158 -2
- package/package.json +2 -2
package/dist/resume.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHmac, timingSafeEqual, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, randomUUID } from "node:crypto";
|
|
1
|
+
import { createHmac, timingSafeEqual, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, randomUUID, randomBytes } from "node:crypto";
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, renameSync, openSync, closeSync, fsyncSync, linkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { userInfo } from "node:os";
|
|
@@ -7,7 +7,7 @@ import { signConfirm, canonicalize as jcs, assertSafeTransport, carriesCredentia
|
|
|
7
7
|
import { canonicalize as toCanonical, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
8
8
|
import { readConfirmResponse, buildConfirmBody, tierConfirms, tombstoneTier } from "./evidence.js";
|
|
9
9
|
import { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
10
|
-
const argsBindingBytes = (p) => Buffer.from(jcs({ decisionId: p.decisionId, action: p.action, amount: p.amount, ...(p.scope !== undefined ? { scope: p.scope } : {}), args: p.args, ...(p.requestCommitment !== undefined ? { requestCommitment: p.requestCommitment } : {}) }), "utf8");
|
|
10
|
+
const argsBindingBytes = (p) => Buffer.from(jcs({ decisionId: p.decisionId, action: p.action, amount: p.amount, ...(p.scope !== undefined ? { scope: p.scope } : {}), args: p.args, ...(p.requestCommitment !== undefined ? { requestCommitment: p.requestCommitment } : {}), ...(p.claimKey !== undefined ? { claimKey: p.claimKey } : {}) }), "utf8");
|
|
11
11
|
function signArgs(p, agentKey, webhookSecret) {
|
|
12
12
|
const bytes = argsBindingBytes(p);
|
|
13
13
|
if (agentKey) {
|
|
@@ -882,6 +882,55 @@ export async function checkApproval(cfg, decisionId, request, opts = {}) {
|
|
|
882
882
|
}
|
|
883
883
|
return { ok: true, decisionId, state };
|
|
884
884
|
}
|
|
885
|
+
export const RETRY_DELAYS_MS = [300, 1_000];
|
|
886
|
+
export const REQUEST_KEY_RE = /^[A-Za-z0-9._:-]{16,128}$/;
|
|
887
|
+
export function newClaimKey() {
|
|
888
|
+
return `run_${randomBytes(18).toString("base64url")}`;
|
|
889
|
+
}
|
|
890
|
+
export async function claimApproval(cfg, decisionId, claimKey) {
|
|
891
|
+
const no = (code, reason, status) => ({ ok: false, decisionId, code, final: code === "already_used" || code === "expired" || code === "not_claimable", reason, ...(status !== undefined ? { status } : {}) });
|
|
892
|
+
if (typeof claimKey !== "string" || !REQUEST_KEY_RE.test(claimKey))
|
|
893
|
+
return no("refused", "this run's claim key is not one Decidio accepts (16-128 letters, digits, dot, colon, underscore or dash). Nothing ran.");
|
|
894
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
895
|
+
assertSafeTransport(cfg.apiUrl, carriesCredential(cfg));
|
|
896
|
+
const url = cfg.apiUrl.replace(/\/$/, "") + "/agent/claim/" + encodeURIComponent(decisionId);
|
|
897
|
+
let last = "";
|
|
898
|
+
for (let attempt = 0;; attempt++) {
|
|
899
|
+
let res;
|
|
900
|
+
try {
|
|
901
|
+
res = await f(url, { method: "POST", headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) }, body: JSON.stringify({ claimKey }) });
|
|
902
|
+
}
|
|
903
|
+
catch (e) {
|
|
904
|
+
last = `could not reach Decidio: ${e?.cause?.code ?? e?.code ?? e?.message ?? e}`;
|
|
905
|
+
}
|
|
906
|
+
if (res) {
|
|
907
|
+
const body = await res.json().catch(() => null);
|
|
908
|
+
if (res.status === 200) {
|
|
909
|
+
if (body && body.ok === true && body.claimed === true)
|
|
910
|
+
return { ok: true, decisionId, again: body.again === true };
|
|
911
|
+
return no("unrecognized", "Decidio answered the claim with HTTP 200 but did not say this run collected the approval, and an answer this SDK does not understand is never permission. Nothing ran.", 200);
|
|
912
|
+
}
|
|
913
|
+
if (res.status === 409) {
|
|
914
|
+
const code = body?.code;
|
|
915
|
+
if (code === "ALREADY_USED")
|
|
916
|
+
return no("already_used", "this approval was already used by another run. Ask again for a new approval. Nothing ran.", 409);
|
|
917
|
+
if (code === "EXPIRED")
|
|
918
|
+
return no("expired", "this approval has expired. Ask again for a new approval. Nothing ran.", 409);
|
|
919
|
+
if (code === "NOT_CLAIMABLE")
|
|
920
|
+
return no("not_claimable", "Decidio says this approval can't be collected by a run (only a person's approval is, and a deploy approval belongs to its deploy job). Nothing ran.", 409);
|
|
921
|
+
return no("unrecognized", `Decidio refused the claim with a code this SDK does not know (${JSON.stringify(code ?? null)}). Nothing ran.`, 409);
|
|
922
|
+
}
|
|
923
|
+
if (res.status === 404)
|
|
924
|
+
return no("not_found", "Decidio did not let this agent collect the approval (HTTP 404: it does not know this decision for this credential, or it predates approval claims). Nothing ran.", 404);
|
|
925
|
+
if (!(res.status === 429 || (res.status >= 500 && res.status !== 501)))
|
|
926
|
+
return no("refused", `Decidio refused to let this run collect the approval (HTTP ${res.status}). Nothing ran.`, res.status);
|
|
927
|
+
last = `HTTP ${res.status}`;
|
|
928
|
+
}
|
|
929
|
+
if (attempt >= RETRY_DELAYS_MS.length)
|
|
930
|
+
return no("unreachable", `could not collect the approval from Decidio after ${attempt + 1} tries (${last}). Nothing ran.`);
|
|
931
|
+
await sleep(RETRY_DELAYS_MS[attempt]);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
885
934
|
const V2_STORE_METHODS = [
|
|
886
935
|
"put", "get", "delete", "list",
|
|
887
936
|
"claim", "markInvoking", "recordExecuted", "markIndeterminate", "markReported",
|
|
@@ -1125,6 +1174,25 @@ export function createDecidioResume(opts) {
|
|
|
1125
1174
|
refuseOnce(decisionId, `[decidio] REFUSING to execute ${decisionId}: ${check.reason}. Nothing ran; the parked entry is untouched for inspection. If the request really changed, reject this decision and re-run the action to request a fresh approval.`);
|
|
1126
1175
|
return finish({ status: "request_unverified", decisionId, action: pending.action, reason: check.reason });
|
|
1127
1176
|
}
|
|
1177
|
+
if (check.state === "approved") {
|
|
1178
|
+
let claimed;
|
|
1179
|
+
try {
|
|
1180
|
+
claimed = await claimApproval(ctrl, decisionId, pending.claimKey ?? newClaimKey());
|
|
1181
|
+
}
|
|
1182
|
+
catch (e) {
|
|
1183
|
+
await ctrl.store.releasePreinvoke(decisionId, attemptId).catch(() => { });
|
|
1184
|
+
throw e;
|
|
1185
|
+
}
|
|
1186
|
+
if (!claimed.ok) {
|
|
1187
|
+
if (claimed.final) {
|
|
1188
|
+
await ctrl.store.markDiscarded(decisionId, "blocked", claimed.reason);
|
|
1189
|
+
await ctrl.store.delete(decisionId);
|
|
1190
|
+
return finish({ status: "blocked", decisionId, action: pending.action, reason: claimed.reason });
|
|
1191
|
+
}
|
|
1192
|
+
await ctrl.store.releasePreinvoke(decisionId, attemptId).catch(() => { });
|
|
1193
|
+
throw new Error(`[decidio] ${decisionId}: ${claimed.reason} The entry stays parked; the next signal tries again with the same claim key.`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1128
1196
|
try {
|
|
1129
1197
|
await ctrl.store.markInvoking(decisionId, attemptId);
|
|
1130
1198
|
}
|
|
@@ -1194,10 +1262,11 @@ export function createDecidioResume(opts) {
|
|
|
1194
1262
|
catch (e) {
|
|
1195
1263
|
throw new DecidioUnsafeArgumentsError(`${e?.message ?? e} NOT parked and NOT executable — but decision ${p.decisionId} may already be routed; reject it in your queue: npx @decidio/sdk approvals reject ${p.decisionId}`, p.decisionId);
|
|
1196
1264
|
}
|
|
1197
|
-
const
|
|
1265
|
+
const parked = { ...p, claimKey: p.claimKey ?? newClaimKey() };
|
|
1266
|
+
const argsSig = signArgs(parked, ctrl.agentKey, integrityKey());
|
|
1198
1267
|
if (!argsSig)
|
|
1199
1268
|
console.warn(`[decidio] parking ${p.decisionId} UNBOUND — no agent key or webhook secret to sign the payload with, so a modified pending file could not be detected. Run \`npx @decidio/sdk init\` to configure keys.`);
|
|
1200
|
-
await ctrl.store.put({ ...
|
|
1269
|
+
await ctrl.store.put({ ...parked, createdAt: new Date().toISOString(), ...(argsSig ? { argsSig } : {}) });
|
|
1201
1270
|
},
|
|
1202
1271
|
async resolve(input) { return resolveCore(input); },
|
|
1203
1272
|
webhookHandler() {
|
|
@@ -1478,7 +1547,7 @@ export function createDecidioResume(opts) {
|
|
|
1478
1547
|
else if (outcome.status === "rejected")
|
|
1479
1548
|
console.error(`[decidio] resumed ${p.decisionId}: REJECTED — the parked "${p.action}" was discarded, nothing executed.`);
|
|
1480
1549
|
else if (outcome.status === "blocked")
|
|
1481
|
-
console.error(`[decidio] resumed ${p.decisionId}: BLOCKED by policy — the parked "${p.action}" was discarded, nothing executed.`);
|
|
1550
|
+
console.error(`[decidio] resumed ${p.decisionId}: BLOCKED ${outcome.reason ? `(${outcome.reason})` : "by policy"} — the parked "${p.action}" was discarded, nothing executed.`);
|
|
1482
1551
|
workerOpts?.onResolved?.(outcome);
|
|
1483
1552
|
}
|
|
1484
1553
|
catch (e) {
|
package/dist/signing.d.ts
CHANGED
|
@@ -126,6 +126,14 @@ export declare function requestCommitment(material: BindingMaterial, request: Co
|
|
|
126
126
|
/** lowercase-hex idempotency key over an execution identity (e.g. LangGraph's thread + task +
|
|
127
127
|
* commitment), keyed so the host's thread ids never reach Decidio in the clear. */
|
|
128
128
|
export declare function idempotencyKey(material: BindingMaterial, identity: Record<string, JsonValue>): string;
|
|
129
|
+
/** THE REQUEST KEY A CALL SENDS BY DEFAULT (11f, founder 2026-10-30): an idempotency key over a per-call random id and
|
|
130
|
+
* the request's commitment, so the SDK's own retry of one call is the same request (the gate answers the decision it
|
|
131
|
+
* already made) while two calls stay two requests. Python twin: invocation_request_key. */
|
|
132
|
+
export declare function invocationRequestKey(material: BindingMaterial, invocationId: string, commitment: string): string;
|
|
133
|
+
/** A CALLER'S OWN REQUEST KEY (11f), e.g. an order id: keyed like every other identity, so the caller's id never reaches
|
|
134
|
+
* Decidio in the clear. Deliberately NOT over the commitment: the same key for a changed request is the gate's 409
|
|
135
|
+
* IDEMPOTENCY_CONFLICT, never a silent second request. Python twin: own_request_key. */
|
|
136
|
+
export declare function ownRequestKey(material: BindingMaterial, key: string): string;
|
|
129
137
|
/** The Claude Code hook's request scope: `cmd-` + 16 hex of a KEYED digest over the command text and its subject
|
|
130
138
|
* (chat approvals follow-up, invariant-guard 2026-10-15). The text may carry a credential the display redacts; an
|
|
131
139
|
* unkeyed digest sealed beside the redacted text would be an offline oracle for a weak password. Keyed under its own
|
package/dist/signing.js
CHANGED
|
@@ -198,6 +198,12 @@ export function requestCommitment(material, request) {
|
|
|
198
198
|
export function idempotencyKey(material, identity) {
|
|
199
199
|
return createHmac("sha256", bindingSubkey(material, "decidio.idempotency-key.v1")).update(canonicalJson(identity, "idempotency identity"), "utf8").digest("hex");
|
|
200
200
|
}
|
|
201
|
+
export function invocationRequestKey(material, invocationId, commitment) {
|
|
202
|
+
return idempotencyKey(material, { invocation: invocationId, commitment });
|
|
203
|
+
}
|
|
204
|
+
export function ownRequestKey(material, key) {
|
|
205
|
+
return idempotencyKey(material, { requestKey: key });
|
|
206
|
+
}
|
|
201
207
|
export function boundScope(material, text) {
|
|
202
208
|
return "cmd-" + createHmac("sha256", bindingSubkey(material, "decidio.hook-scope.v1")).update(text, "utf8").digest("hex").slice(0, 16);
|
|
203
209
|
}
|
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.
|
|
8
|
+
version: 0.12.0 # 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
|
|
@@ -87,6 +87,15 @@ components:
|
|
|
87
87
|
same requestCommitment AND the same requester, action, amount and scope (the server binds that
|
|
88
88
|
tuple itself) - otherwise 409 IDEMPOTENCY_CONFLICT; an earlier decision is never handed back for a
|
|
89
89
|
changed request.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
SDK 0.12.0 sends one BY DEFAULT beside every requestCommitment: a keyed HMAC over a fresh id for the
|
|
93
|
+
call and the commitment (or over the caller's own key, or an engine's own id for the call - an
|
|
94
|
+
OpenAI tool call id, a Temporal workflow run and activity, an Inngest run), and retries a network
|
|
95
|
+
failure, a 5xx or a non-AGENT_RATE 429 with the SAME key: a retry of a request still waiting is
|
|
96
|
+
answered with the one already waiting, and no retry runs an action twice. (A request is not
|
|
97
|
+
guaranteed single: an auto-approval whose answer is lost on every try, or a keyless request a policy
|
|
98
|
+
blocks, can make a second decision.) Without key material there is no commitment and no key.
|
|
90
99
|
AuthorizeResponse:
|
|
91
100
|
type: object
|
|
92
101
|
required: [decision, decisionId, receiptId]
|
|
@@ -111,14 +120,27 @@ components:
|
|
|
111
120
|
and `receipt` carries only the fields fixed at create. A sealed auto-approval replays as `proceed`
|
|
112
121
|
only when THIS call could itself auto-approve (a verified, replay-protected identity or a trusted
|
|
113
122
|
transport); anything else answers `route` or `block` - ask GET /agent/status for the real state.
|
|
123
|
+
openTwin:
|
|
124
|
+
type: boolean
|
|
125
|
+
description: >
|
|
126
|
+
present (true, beside idempotentReplay) when this KEYLESS request joined the same request from the
|
|
127
|
+
same agent that is still waiting for a person: nothing new was created, counted or summoned, and the
|
|
128
|
+
caller waits on that decision. The request may be shared, so a caller that gives up waiting must not
|
|
129
|
+
withdraw it (the SDKs skip their timeout withdraw after either flag; the server refuses a shared
|
|
130
|
+
withdraw with 409 SHARED_REQUEST).
|
|
114
131
|
StatusResponse:
|
|
115
132
|
type: object
|
|
116
133
|
required: [state]
|
|
134
|
+
description: >
|
|
135
|
+
Reading the status claims NOTHING: a dashboard, the CLI or a second watcher can poll it freely. The run
|
|
136
|
+
that acts on a person's approval collects it with POST /agent/claim/{decisionId}.
|
|
117
137
|
properties:
|
|
118
138
|
state:
|
|
119
139
|
type: string
|
|
120
140
|
enum: [pending, approved, auto_approved, rejected, blocked]
|
|
121
|
-
|
|
141
|
+
decisionId: { type: string }
|
|
142
|
+
receiptId: { type: [string, "null"] }
|
|
143
|
+
reason: { type: [string, "null"], description: the approver's reason, tokenized; never a cleartext name }
|
|
122
144
|
requestCommitment:
|
|
123
145
|
type: [string, "null"]
|
|
124
146
|
description: >
|
|
@@ -126,6 +148,27 @@ components:
|
|
|
126
148
|
sent). The SDK treats an approval as covering the request it is about to run only when this
|
|
127
149
|
equals its own recompute; a gate that returns no commitment for a request the SDK bound is
|
|
128
150
|
refused (commitment_missing), never trusted.
|
|
151
|
+
spent:
|
|
152
|
+
type: boolean
|
|
153
|
+
description: >
|
|
154
|
+
SDK 0.8.0: true once an execution is recorded on this approval - one approval covers one action,
|
|
155
|
+
once. The SDKs refuse to run on a spent approval and ask again.
|
|
156
|
+
grantExpiresAt:
|
|
157
|
+
type: [string, "null"]
|
|
158
|
+
format: date-time
|
|
159
|
+
description: >
|
|
160
|
+
SDK 0.8.0: when a granted decision's approval lapses (its seal's time plus the gate's TTL); null
|
|
161
|
+
otherwise. A granted decision with no expiry has a grant of unknown age, which the SDKs treat as
|
|
162
|
+
expired.
|
|
163
|
+
handedBackAt:
|
|
164
|
+
type: [string, "null"]
|
|
165
|
+
format: date-time
|
|
166
|
+
description: when an idempotent replay handed this granted decision back to a retry (null otherwise)
|
|
167
|
+
claimed:
|
|
168
|
+
type: boolean
|
|
169
|
+
description: >
|
|
170
|
+
SDK 0.12.0: whether a run has collected this person's approval (POST /agent/claim) - never by whom.
|
|
171
|
+
Informational: it claims nothing, and a caller decides a claim only by claiming.
|
|
129
172
|
ConfirmRequest:
|
|
130
173
|
type: object
|
|
131
174
|
required: [decisionId, capturedResponse, source]
|
|
@@ -328,6 +371,119 @@ paths:
|
|
|
328
371
|
check runs BEFORE any lookup, so this response reveals nothing about existence.
|
|
329
372
|
"401": { description: missing/invalid agent token }
|
|
330
373
|
"404": { description: unknown decision }
|
|
374
|
+
/agent/claim/{decisionId}:
|
|
375
|
+
post:
|
|
376
|
+
summary: Collect a person's approval for ONE run, once, right before acting on it (SDK 0.12.0)
|
|
377
|
+
description: >
|
|
378
|
+
Several programs can wait on one approval (callers that asked the same request, a second poller); only
|
|
379
|
+
the run that collects it runs it. The first claim wins. The same claimKey claiming again is the same run
|
|
380
|
+
retrying (a lost answer) and is answered the same, with `again: true`. Any other key is told the
|
|
381
|
+
approval was already used, and asks again. Only a PERSON's approval (state `approved`) is claimed: an
|
|
382
|
+
auto-approval is never shared, so it is NOT_CLAIMABLE, and so are a refusal, a waiting request and a
|
|
383
|
+
GitHub deploy approval (its deploy job uses it). A spent or expired approval is refused before anything
|
|
384
|
+
is written. A person's sign-in cannot claim (people approve, runs claim): it gets the 404. A bound agent
|
|
385
|
+
token claims only its own signed decisions; an unsigned decision is claimable by any agent credential
|
|
386
|
+
of the workspace (founder call 29). The SDKs make the claimKey at random for each run (two workers with
|
|
387
|
+
one key would be one run), retry a network failure or a 5xx with the SAME key, and run only on a 200.
|
|
388
|
+
parameters:
|
|
389
|
+
- name: decisionId
|
|
390
|
+
in: path
|
|
391
|
+
required: true
|
|
392
|
+
schema: { type: string }
|
|
393
|
+
requestBody:
|
|
394
|
+
required: true
|
|
395
|
+
content:
|
|
396
|
+
application/json:
|
|
397
|
+
schema:
|
|
398
|
+
type: object
|
|
399
|
+
required: [claimKey]
|
|
400
|
+
properties:
|
|
401
|
+
claimKey:
|
|
402
|
+
type: string
|
|
403
|
+
pattern: "^[A-Za-z0-9._:-]{16,128}$"
|
|
404
|
+
description: the run's own key; stored only as a keyed digest scoped to this decision
|
|
405
|
+
responses:
|
|
406
|
+
"200":
|
|
407
|
+
description: this run collected the approval - run the action
|
|
408
|
+
content:
|
|
409
|
+
application/json:
|
|
410
|
+
schema:
|
|
411
|
+
type: object
|
|
412
|
+
required: [ok, claimed, decisionId, again]
|
|
413
|
+
properties:
|
|
414
|
+
ok: { type: boolean, enum: [true] }
|
|
415
|
+
claimed: { type: boolean, enum: [true] }
|
|
416
|
+
decisionId: { type: string }
|
|
417
|
+
again: { type: boolean, description: true when the same key claimed again (the same run retrying) }
|
|
418
|
+
"400": { description: "a malformed decision id (INVALID_DECISION_ID) or claimKey (VALIDATION)" }
|
|
419
|
+
"401": { description: missing/invalid agent token }
|
|
420
|
+
"404": { description: unknown decision, one this credential may not claim, or a person's sign-in }
|
|
421
|
+
"409":
|
|
422
|
+
description: >
|
|
423
|
+
Do NOT run. `ALREADY_USED` - another run collected this approval, or it was already used: ask again
|
|
424
|
+
for a new one. `EXPIRED` - the approval lapsed: ask again. `NOT_CLAIMABLE` - not a person's approval
|
|
425
|
+
(still waiting, refused, an auto-approval, or a deploy approval).
|
|
426
|
+
content:
|
|
427
|
+
application/json:
|
|
428
|
+
schema:
|
|
429
|
+
type: object
|
|
430
|
+
properties:
|
|
431
|
+
code: { type: string, enum: [ALREADY_USED, EXPIRED, NOT_CLAIMABLE] }
|
|
432
|
+
decisionId: { type: string }
|
|
433
|
+
state: { type: string, enum: [pending, approved, auto_approved, rejected, blocked] }
|
|
434
|
+
message: { type: string }
|
|
435
|
+
/agent/withdraw/{decisionId}:
|
|
436
|
+
post:
|
|
437
|
+
summary: The requester stopped waiting - close its OPEN routed request as withdrawn (SDK 0.6.4)
|
|
438
|
+
description: >
|
|
439
|
+
The blocking SDK wait calls this when it hits its ceiling, so the queue stops carrying a request nobody
|
|
440
|
+
will act on and a later tap cannot seal "approved" for an action that will never run. A withdrawal only
|
|
441
|
+
ever reduces authority. Only the requester may withdraw: a bound agent token whose agent signed the
|
|
442
|
+
request; everything else is the 404. A withdrawn request reads `rejected` on GET /agent/status. A SHARED
|
|
443
|
+
request is never withdrawn: once another caller joined it (openTwin), the answer is 409 SHARED_REQUEST and
|
|
444
|
+
nothing changes. The SDKs skip the withdraw after an `idempotentReplay` or `openTwin` answer.
|
|
445
|
+
parameters:
|
|
446
|
+
- name: decisionId
|
|
447
|
+
in: path
|
|
448
|
+
required: true
|
|
449
|
+
schema: { type: string }
|
|
450
|
+
requestBody:
|
|
451
|
+
content:
|
|
452
|
+
application/json:
|
|
453
|
+
schema:
|
|
454
|
+
type: object
|
|
455
|
+
properties:
|
|
456
|
+
reason: { type: string, enum: [timeout, cancelled], description: "default timeout" }
|
|
457
|
+
responses:
|
|
458
|
+
"200":
|
|
459
|
+
description: withdrawn
|
|
460
|
+
content:
|
|
461
|
+
application/json:
|
|
462
|
+
schema:
|
|
463
|
+
type: object
|
|
464
|
+
properties:
|
|
465
|
+
ok: { type: boolean }
|
|
466
|
+
withdrawn: { type: boolean, enum: [true] }
|
|
467
|
+
state: { type: string, enum: [rejected] }
|
|
468
|
+
decisionId: { type: string }
|
|
469
|
+
reason: { type: string }
|
|
470
|
+
"400": { description: "a malformed decision id (INVALID_DECISION_ID) or an unknown reason (INVALID_REASON)" }
|
|
471
|
+
"401": { description: missing/invalid agent token }
|
|
472
|
+
"404": { description: unknown decision, or not this requester's own signed request }
|
|
473
|
+
"409":
|
|
474
|
+
description: >
|
|
475
|
+
`NOT_OPEN` - a decision already stands (or it was already withdrawn); `state` is the derived state, so
|
|
476
|
+
an approval that landed in the last moment is honoured. `SHARED_REQUEST` - another caller is waiting on
|
|
477
|
+
this same request; nothing changed (no `state`), and a person still decides it.
|
|
478
|
+
content:
|
|
479
|
+
application/json:
|
|
480
|
+
schema:
|
|
481
|
+
type: object
|
|
482
|
+
properties:
|
|
483
|
+
code: { type: string, enum: [NOT_OPEN, SHARED_REQUEST] }
|
|
484
|
+
state: { type: string, enum: [pending, approved, auto_approved, rejected, blocked] }
|
|
485
|
+
decisionId: { type: string }
|
|
486
|
+
message: { type: string }
|
|
331
487
|
/agent/confirm:
|
|
332
488
|
post:
|
|
333
489
|
summary: >
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decidio/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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.
|
|
87
|
+
"@decidio/verify": "0.12.0"
|
|
88
88
|
},
|
|
89
89
|
"author": "OmniTwin Technologies Inc.",
|
|
90
90
|
"bugs": {
|