@decidio/sdk 0.6.4 → 0.8.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 +167 -0
- package/README.md +104 -24
- package/dist/adapters/_gate.d.ts +47 -2
- package/dist/adapters/_gate.js +49 -3
- package/dist/adapters/langgraph.d.ts +5 -11
- package/dist/adapters/langgraph.js +57 -6
- package/dist/adapters/openai.d.ts +40 -10
- package/dist/adapters/openai.js +19 -11
- package/dist/adapters/temporal.d.ts +21 -2
- package/dist/adapters/temporal.js +18 -5
- package/dist/cli.d.ts +59 -0
- package/dist/cli.js +2095 -194
- package/dist/demo-gate.js +6 -4
- package/dist/index.d.ts +36 -4
- package/dist/index.js +50 -5
- package/dist/inngest.d.ts +5 -2
- package/dist/inngest.js +23 -15
- package/dist/resume.d.ts +108 -5
- package/dist/resume.js +215 -109
- package/dist/signing.d.ts +40 -0
- package/dist/signing.js +43 -1
- package/openapi.yaml +65 -6
- package/package.json +2 -2
package/dist/demo-gate.js
CHANGED
|
@@ -40,6 +40,7 @@ export async function startDemoGate(opts = {}) {
|
|
|
40
40
|
let lastReason = "quickstart";
|
|
41
41
|
let relayCode = null;
|
|
42
42
|
let relayOk = null;
|
|
43
|
+
let committed = null;
|
|
43
44
|
const relayFetch = async (path, init = {}) => {
|
|
44
45
|
if (!relayBase)
|
|
45
46
|
return null;
|
|
@@ -111,7 +112,8 @@ export async function startDemoGate(opts = {}) {
|
|
|
111
112
|
return send({ ok: true, changed: true });
|
|
112
113
|
if (path === "/agent/authorize") {
|
|
113
114
|
verdict = null;
|
|
114
|
-
|
|
115
|
+
committed = typeof body?.requestCommitment === "string" && /^[0-9a-f]{64}$/.test(body.requestCommitment) ? body.requestCommitment : null;
|
|
116
|
+
return send({ decision: "route", decisionId: DECISION_ID, receiptId: "R-DEMO-1", requestCommitment: committed });
|
|
115
117
|
}
|
|
116
118
|
if (path.startsWith("/agent/status/")) {
|
|
117
119
|
if (!verdict && relayCode) {
|
|
@@ -123,8 +125,8 @@ export async function startDemoGate(opts = {}) {
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
if (withdrawnDemo && !verdict)
|
|
126
|
-
return send({ state: "rejected", reason: "quickstart: withdrawn — the tour stopped waiting" });
|
|
127
|
-
return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver" } : { state: verdict === "approve" ? "approved" : "pending" });
|
|
128
|
+
return send({ state: "rejected", reason: "quickstart: withdrawn — the tour stopped waiting", requestCommitment: committed });
|
|
129
|
+
return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver", requestCommitment: committed } : { state: verdict === "approve" ? "approved" : "pending", requestCommitment: committed });
|
|
128
130
|
}
|
|
129
131
|
if (path.startsWith("/agent/withdraw/")) {
|
|
130
132
|
if (verdict)
|
|
@@ -157,7 +159,7 @@ export async function startDemoGate(opts = {}) {
|
|
|
157
159
|
}
|
|
158
160
|
if (/^\/api\/records\/[^/]+\/verify$/.test(path)) {
|
|
159
161
|
const cred = issueReceipt(verdict === "reject" ? "denial" : "approval");
|
|
160
|
-
return send({ dNumber: D_NUMBER, issuer: issuerDid, alg: "Ed25519", valid: true, signatureValid: true, contentBound: true, matchesRecord: true, payloadMatches: true, hash: cred.credentialSubject.contentHash, prevHash: null, credential: cred });
|
|
162
|
+
return send({ dNumber: D_NUMBER, issuer: issuerDid, expectedIssuer: issuerDid, alg: "Ed25519", valid: true, signatureValid: true, contentBound: true, matchesRecord: true, payloadMatches: true, hash: cred.credentialSubject.contentHash, prevHash: null, credential: cred });
|
|
161
163
|
}
|
|
162
164
|
if (/^\/api\/records\/[^/]+$/.test(path))
|
|
163
165
|
return send({ record: { confirmation: verdict === "approve" ? { status: "application_confirmed", confirmed: true } : { status: "pending", confirmed: false } } });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { ResumeController } from "./resume.js";
|
|
1
|
+
import type { ResumeController, ApprovalCheck } from "./resume.js";
|
|
2
|
+
import { type CommittedRequest } from "./signing.js";
|
|
2
3
|
import { type EvidenceOutcome } from "./evidence.js";
|
|
3
|
-
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
4
|
-
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome } from "./resume.js";
|
|
4
|
+
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded, checkApproval } from "./resume.js";
|
|
5
|
+
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome, ApprovalCheck, ApprovalCheckCode, DecisionStatus } from "./resume.js";
|
|
5
6
|
export { guard, Guard } from "./guard.js";
|
|
6
7
|
export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
7
8
|
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
8
|
-
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
9
|
+
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential, bindingMaterial, boundScope, requestCommitment, idempotencyKey, committedRequestJson } from "./signing.js";
|
|
10
|
+
export type { BindingMaterial, CommittedRequest } from "./signing.js";
|
|
9
11
|
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
10
12
|
export type { JsonValue, JsonPrimitive } from "./canonical.js";
|
|
11
13
|
export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
|
|
@@ -80,6 +82,36 @@ export declare class DecidioRejectedError extends Error {
|
|
|
80
82
|
decisionId: string;
|
|
81
83
|
constructor(reason: string, decisionId: string);
|
|
82
84
|
}
|
|
85
|
+
/** THE REQUEST CHANGED AFTER APPROVAL (0.7.0). Decidio records an approval for `decisionId`, but it is
|
|
86
|
+
* bound (by a keyed commitment) to a DIFFERENT request than the one about to run — the arguments, the
|
|
87
|
+
* amount or the described context changed after a human approved, or this agent's key material changed
|
|
88
|
+
* since it asked. Nothing was executed. A new request needs a new approval: reject this decision and
|
|
89
|
+
* run the action again. Extends DecidioBlockedError, so code that already treats a block as "did not
|
|
90
|
+
* run" keeps doing so. */
|
|
91
|
+
export declare class DecidioRequestChangedError extends DecidioBlockedError {
|
|
92
|
+
readonly code: "request_changed";
|
|
93
|
+
constructor(decisionId: string, detail?: string);
|
|
94
|
+
}
|
|
95
|
+
/** An approval that cannot be TIED to this request (0.7.0): Decidio answered without the request
|
|
96
|
+
* commitment this SDK sent (`commitment_missing` — the gate predates request binding), or holds one
|
|
97
|
+
* this process cannot recompute (`unverifiable` — no or unreadable key material, or a malformed
|
|
98
|
+
* answer). Fail closed: nothing was executed. Extends DecidioBlockedError. */
|
|
99
|
+
export declare class DecidioApprovalUnverifiedError extends DecidioBlockedError {
|
|
100
|
+
readonly code: "commitment_missing" | "unverifiable";
|
|
101
|
+
constructor(decisionId: string, code: "commitment_missing" | "unverifiable", detail: string);
|
|
102
|
+
}
|
|
103
|
+
/** The typed refusal for an approval check that did not pass — ONE mapping for every door that throws
|
|
104
|
+
* (inline, LangGraph, Temporal, Inngest, OpenAI Agents). `pending` is included for the doors that
|
|
105
|
+
* cannot wait again; the doors that can (LangGraph, Temporal, Inngest) wait instead of calling this. */
|
|
106
|
+
export declare function approvalRefusal(check: ApprovalCheck): Error;
|
|
107
|
+
/** What a routed approval must be bound to on the CORE door: exactly what executes — the canonical
|
|
108
|
+
* argument snapshot plus the described action/amount/scope. The durable resume recomputes the same
|
|
109
|
+
* shape over the parked payload. */
|
|
110
|
+
export declare function coreRequest(ctx: {
|
|
111
|
+
action: string;
|
|
112
|
+
amount: number;
|
|
113
|
+
scope?: string | null;
|
|
114
|
+
}, args: readonly unknown[]): CommittedRequest;
|
|
83
115
|
export declare class DecidioTimeoutError extends Error {
|
|
84
116
|
decisionId: string;
|
|
85
117
|
withdrawn: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { DecidioSuspendedError } from "./resume.js";
|
|
2
|
-
import { signBoundProof, signConfirm, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
1
|
+
import { DecidioSuspendedError, checkApproval } from "./resume.js";
|
|
2
|
+
import { signBoundProof, signConfirm, assertSafeTransport, carriesCredential, bindingMaterial, requestCommitment } from "./signing.js";
|
|
3
3
|
import { canonicalize, canonicalizeArgs } from "./canonical.js";
|
|
4
4
|
import { readConfirmResponse, buildConfirmBody } from "./evidence.js";
|
|
5
|
-
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
5
|
+
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded, checkApproval } from "./resume.js";
|
|
6
6
|
export { guard, Guard } from "./guard.js";
|
|
7
7
|
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
8
8
|
import { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
9
|
-
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
9
|
+
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential, bindingMaterial, boundScope, requestCommitment, idempotencyKey, committedRequestJson } from "./signing.js";
|
|
10
10
|
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
11
11
|
export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
|
|
12
12
|
export class DecidioBlockedError extends Error {
|
|
@@ -29,6 +29,40 @@ export class DecidioRejectedError extends Error {
|
|
|
29
29
|
this.name = "DecidioRejectedError";
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
export class DecidioRequestChangedError extends DecidioBlockedError {
|
|
33
|
+
code = "request_changed";
|
|
34
|
+
constructor(decisionId, detail) {
|
|
35
|
+
super(detail ?? `the request changed after approval (decision ${decisionId})`, decisionId);
|
|
36
|
+
this.name = "DecidioRequestChangedError";
|
|
37
|
+
this.message = `[decidio] the request changed after approval: decision ${decisionId} was approved for a different request than the one about to run. Nothing was executed. ${detail && !/^the request changed after approval/.test(detail) ? `(${detail}) ` : ""}A changed request needs its own approval — reject ${decisionId} and run the action again.`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class DecidioApprovalUnverifiedError extends DecidioBlockedError {
|
|
41
|
+
code;
|
|
42
|
+
constructor(decisionId, code, detail) {
|
|
43
|
+
super(detail, decisionId);
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.name = "DecidioApprovalUnverifiedError";
|
|
46
|
+
this.message = `[decidio] cannot confirm that decision ${decisionId} approved THIS request: ${detail}. Nothing was executed.`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function approvalRefusal(check) {
|
|
50
|
+
const id = check.decisionId;
|
|
51
|
+
switch (check.code) {
|
|
52
|
+
case "rejected": return new DecidioRejectedError(check.reason ?? "rejected by approver", id);
|
|
53
|
+
case "blocked": return new DecidioBlockedError(check.reason ?? "blocked by policy", id);
|
|
54
|
+
case "request_changed": return new DecidioRequestChangedError(id, check.reason);
|
|
55
|
+
case "commitment_missing": return new DecidioApprovalUnverifiedError(id, "commitment_missing", check.reason ?? "Decidio answered without the request commitment this SDK sent");
|
|
56
|
+
case "unverifiable": return new DecidioApprovalUnverifiedError(id, "unverifiable", check.reason ?? "the approval could not be tied to this request");
|
|
57
|
+
case "pending": return new DecidioBlockedError(`not approved - ${check.reason ?? `Decidio records ${id} as pending`}`, id);
|
|
58
|
+
case "spent": return new DecidioBlockedError(`not executed - ${check.reason ?? `the approval for ${id} is already spent`}`, id);
|
|
59
|
+
case "expired": return new DecidioBlockedError(`not executed - ${check.reason ?? `the approval for ${id} has expired`}`, id);
|
|
60
|
+
default: return new DecidioBlockedError(check.reason ?? `not approved (Decidio answered ${JSON.stringify(check.state)})`, id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function coreRequest(ctx, args) {
|
|
64
|
+
return { action: ctx.action, amount: ctx.amount, ...(ctx.scope !== undefined && ctx.scope !== null ? { scope: ctx.scope } : {}), args };
|
|
65
|
+
}
|
|
32
66
|
export class DecidioTimeoutError extends Error {
|
|
33
67
|
decisionId;
|
|
34
68
|
withdrawn;
|
|
@@ -153,12 +187,16 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
153
187
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
154
188
|
: undefined;
|
|
155
189
|
assertDescribed(ctx);
|
|
190
|
+
const material = bindingMaterial(config);
|
|
191
|
+
const committed = coreRequest(ctx, args);
|
|
192
|
+
const sentCommitment = material ? requestCommitment(material, committed) : undefined;
|
|
156
193
|
const auth = await post(config, "/agent/authorize", {
|
|
157
194
|
requester: config.agentId, action: ctx.action, amount: ctx.amount,
|
|
158
195
|
scope: ctx.scope, sourceSystem: config.sourceSystem,
|
|
159
196
|
...(ctx.context ? { context: ctx.context } : {}),
|
|
160
197
|
...(requesterIdentity ? { requesterIdentity } : {}),
|
|
161
198
|
...(resumeUrl ? { resumeUrl } : {}),
|
|
199
|
+
...(sentCommitment ? { requestCommitment: sentCommitment } : {}),
|
|
162
200
|
});
|
|
163
201
|
if (auth.decision !== "proceed" && auth.decision !== "route" && auth.decision !== "block")
|
|
164
202
|
throw new DecidioBlockedError(`unrecognized gate verdict: ${String(auth.decision)}`, auth.decisionId);
|
|
@@ -181,14 +219,18 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
181
219
|
await config.resume.park({
|
|
182
220
|
decisionId: auth.decisionId, receiptId: auth.receiptId,
|
|
183
221
|
action: ctx.action, amount: ctx.amount, scope: ctx.scope, args,
|
|
222
|
+
...(sentCommitment ? { requestCommitment: sentCommitment } : {}),
|
|
184
223
|
});
|
|
185
224
|
throw new DecidioSuspendedError(auth.decisionId, ctx.action);
|
|
186
225
|
}
|
|
187
226
|
const deadline = Date.now() + pollTimeout;
|
|
227
|
+
let approvedStatus;
|
|
188
228
|
while (true) {
|
|
189
229
|
const st = await get(config, `/agent/status/${encodeURIComponent(auth.decisionId)}`);
|
|
190
|
-
if (st.state === "approved" || st.state === "auto_approved")
|
|
230
|
+
if (st.state === "approved" || st.state === "auto_approved") {
|
|
231
|
+
approvedStatus = st;
|
|
191
232
|
break;
|
|
233
|
+
}
|
|
192
234
|
if (st.state === "rejected")
|
|
193
235
|
throw new DecidioRejectedError(st.reason ?? "rejected by approver", auth.decisionId);
|
|
194
236
|
if (st.state === "blocked")
|
|
@@ -215,6 +257,9 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
215
257
|
}
|
|
216
258
|
await sleep(pollInterval);
|
|
217
259
|
}
|
|
260
|
+
const check = await checkApproval(config, auth.decisionId, committed, { sent: !!sentCommitment, ...(approvedStatus ? { status: approvedStatus } : {}) });
|
|
261
|
+
if (!check.ok)
|
|
262
|
+
throw approvalRefusal(check);
|
|
218
263
|
}
|
|
219
264
|
const result = opts?.withContext
|
|
220
265
|
? await fn({ decisionId: auth.decisionId, attemptId: null, idempotencyKey: auth.decisionId }, ...args)
|
package/dist/inngest.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RequestListener } from "node:http";
|
|
2
2
|
import { verifyResumeSignature } from "./resume.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type GuardConfig, type ApprovalContext, type EvidenceOutcome } from "./index.js";
|
|
4
4
|
/** The Inngest event Decidio's verdict is translated into. The waiting function
|
|
5
5
|
* correlates on `data.decisionId`. */
|
|
6
6
|
export declare const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
|
|
@@ -51,7 +51,10 @@ export type GateStepOutcome<R> =
|
|
|
51
51
|
* Gate an action INSIDE an Inngest function, with durable suspend on routing.
|
|
52
52
|
* • proceed → run the action (as a step) + confirm.
|
|
53
53
|
* • route → `step.waitForEvent` durably suspends until Decidio's resume event for
|
|
54
|
-
* THIS decision arrives
|
|
54
|
+
* THIS decision arrives - a WAKE-UP (0.7.0): a memoized step then confirms with
|
|
55
|
+
* Decidio that THIS request was approved; approved → run + confirm; rejected /
|
|
56
|
+
* blocked → status; still pending → wait for the next event; the request changed
|
|
57
|
+
* after approval → DecidioRequestChangedError.
|
|
55
58
|
* • block → return blocked (the action never runs).
|
|
56
59
|
* The action runs in YOUR process with YOUR credentials — Decidio only signals.
|
|
57
60
|
* `config.resumeUrl` must point at the bridge endpoint (createInngestResumeBridge),
|
package/dist/inngest.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { verifyResumeSignature } from "./resume.js";
|
|
2
|
-
import { gateAuthorize, gateConfirm, assertAuthorizeVerdict,
|
|
1
|
+
import { verifyResumeSignature, WEBHOOK_SECRET_REMEDY } from "./resume.js";
|
|
2
|
+
import { gateAuthorize, gateConfirm, assertAuthorizeVerdict, gateCheckApproval, assertApprovalCheckShape, checkEngineVersion } from "./adapters/_gate.js";
|
|
3
|
+
import { approvalRefusal } from "./index.js";
|
|
3
4
|
export const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
|
|
4
5
|
export async function decidioGateStep(args) {
|
|
5
6
|
const { step, config, ctx } = args;
|
|
@@ -8,18 +9,25 @@ export async function decidioGateStep(args) {
|
|
|
8
9
|
if (auth.decision === "block")
|
|
9
10
|
return { status: "blocked", decisionId: auth.decisionId };
|
|
10
11
|
if (auth.decision === "route") {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
for (let n = 0;; n++) {
|
|
13
|
+
const suffix = n === 0 ? "" : `-${n}`;
|
|
14
|
+
const ev = await step.waitForEvent(`decidio-await-approval${suffix}`, {
|
|
15
|
+
event: DECIDIO_RESUME_EVENT,
|
|
16
|
+
timeout: args.timeout ?? "30d",
|
|
17
|
+
if: `async.data.decisionId == "${auth.decisionId}"`,
|
|
18
|
+
});
|
|
19
|
+
if (!ev)
|
|
20
|
+
throw new Error(`decidio approval timed out (${auth.decisionId})`);
|
|
21
|
+
const check = assertApprovalCheckShape(await step.run(`decidio-verify-approval${suffix}`, async () => gateCheckApproval(config, auth.decisionId, ctx)), auth.decisionId);
|
|
22
|
+
if (check.ok)
|
|
23
|
+
break;
|
|
24
|
+
if (check.code === "rejected")
|
|
25
|
+
return { status: "rejected", decisionId: auth.decisionId };
|
|
26
|
+
if (check.code === "blocked")
|
|
27
|
+
return { status: "blocked", decisionId: auth.decisionId };
|
|
28
|
+
if (check.code !== "pending")
|
|
29
|
+
throw approvalRefusal(check);
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
const result = await step.run("agent-action", args.run);
|
|
25
33
|
const confirmation = await step.run("decidio-confirm", async () => gateConfirm(config, auth.decisionId, result, args.evidence));
|
|
@@ -42,7 +50,7 @@ export function createInngestResumeBridge(opts) {
|
|
|
42
50
|
if (!secret) {
|
|
43
51
|
if (!opts.allowUnsigned) {
|
|
44
52
|
res.statusCode = 401;
|
|
45
|
-
return res.end(JSON.stringify({ error:
|
|
53
|
+
return res.end(JSON.stringify({ error: `resume webhook unsigned and no secret configured. ${WEBHOOK_SECRET_REMEDY}` }));
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
56
|
else if (!verifyResumeSignature(secret, raw, req.headers["x-decidio-signature"])) {
|
package/dist/resume.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RequestListener } from "node:http";
|
|
2
|
+
import { type CommittedRequest } from "./signing.js";
|
|
2
3
|
import { type EvidenceOutcome, type EvidenceTier } from "./evidence.js";
|
|
3
4
|
export declare function signEvidence(decisionId: string, attemptId: string | null, evidence: unknown, agentKey?: string, webhookSecret?: string): {
|
|
4
5
|
kind: "ed25519" | "hmac";
|
|
@@ -30,6 +31,13 @@ export interface PendingAction {
|
|
|
30
31
|
kind: "ed25519" | "hmac";
|
|
31
32
|
sig: string;
|
|
32
33
|
};
|
|
34
|
+
/** The request commitment this SDK SENT with /agent/authorize (0.7.0) — see `requestCommitment`
|
|
35
|
+
* in signing.ts. Recorded so `resolve()` can apply the one fail-closed rule that needs to know
|
|
36
|
+
* what was sent: a gate that answers with no commitment for a request this SDK bound is refused.
|
|
37
|
+
* The value that decides execution is always a FRESH recompute over these parked arguments,
|
|
38
|
+
* compared with the commitment Decidio returns. Absent on entries parked before 0.7.0 (and by
|
|
39
|
+
* an agent with no key material), which are checked on Decidio's state alone. */
|
|
40
|
+
requestCommitment?: string;
|
|
33
41
|
}
|
|
34
42
|
/** Where suspended actions live between request and approval. The default is a
|
|
35
43
|
* file store; swap in Redis/Postgres/your queue for production. AGENT-SIDE only —
|
|
@@ -462,7 +470,17 @@ export declare class DecidioSuspendedError extends Error {
|
|
|
462
470
|
* parked for a later signal. It used to read `unknown` (the word for "not ours") for the claim loser and
|
|
463
471
|
* `no_handler` for the boundary loser although a handler was registered (prodsec V040-01). A loss to a FINAL
|
|
464
472
|
* answer (a denial, an execution) stays `already_resolved`. Additive, like the two above. */
|
|
465
|
-
|
|
473
|
+
/** `not_approved` (0.7.0): the signal said "approved" but DECIDIO does not record an approval for this
|
|
474
|
+
* decision (it is still pending, or answered a state this SDK does not recognise). Every resume signal
|
|
475
|
+
* is a wake-up now, never the authority: the approval is confirmed with GET /agent/status right before
|
|
476
|
+
* the action runs. Nothing ran; the entry stays parked and a later signal resolves it. Not a resolution,
|
|
477
|
+
* so `onResolved` does not fire (like `unknown`).
|
|
478
|
+
* `request_unverified` (0.7.0): Decidio records an approval, but it cannot be tied to THIS parked
|
|
479
|
+
* request — the request changed after approval (the commitment Decidio holds does not match a fresh
|
|
480
|
+
* recompute over the parked arguments), the gate kept no commitment for a request this SDK bound, or
|
|
481
|
+
* this process lacks the key material to recompute it. `reason` says which. Nothing ran; the entry
|
|
482
|
+
* stays parked for inspection. Both members are additive, announced in the changelog. */
|
|
483
|
+
export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown" | "indeterminate" | "already_resolved" | "lost_race" | "not_approved" | "request_unverified";
|
|
466
484
|
/** The states a durable action can actually be in — every one of them nameable, including the
|
|
467
485
|
* one nobody likes. */
|
|
468
486
|
export type RecoveryState =
|
|
@@ -558,8 +576,15 @@ export interface ResumeConfig {
|
|
|
558
576
|
/** The workspace the agent acts in (DECIDIO_WORKSPACE_ID) — bound into the cold-resume confirm
|
|
559
577
|
* proof (D-B3.6) so it's tenant-scoped too. Default from env when unset. */
|
|
560
578
|
workspaceId?: string;
|
|
561
|
-
/** Shared secret Decidio signs the resume webhook with (HMAC-SHA256)
|
|
562
|
-
*
|
|
579
|
+
/** Shared secret Decidio signs the resume webhook with (HMAC-SHA256) — DECIDIO_WEBHOOK_SECRET, the
|
|
580
|
+
* dedicated secret your Decidio deployment signs resume webhooks with (never its API_AUTH_TOKEN, the
|
|
581
|
+
* admin bearer). NO FALLBACK since 0.7.0: it used to default to
|
|
582
|
+
* the API token, so anyone holding a leaked agent token could sign an "approved" callback. With
|
|
583
|
+
* no secret (and no `allowUnsigned`) the webhook handlers refuse every call. The poll worker
|
|
584
|
+
* (`worker()`) needs no inbound secret. The webhook is only a WAKE-UP either way: the approval
|
|
585
|
+
* is confirmed with Decidio (GET /agent/status + the request commitment) before anything runs.
|
|
586
|
+
* (The parked-payload integrity HMAC still falls back to the API token when this is unset — that
|
|
587
|
+
* key never authenticates a signal.) */
|
|
563
588
|
webhookSecret?: string;
|
|
564
589
|
/** @deprecated Ignored since 0.2.0 and accepted only so existing callers keep compiling.
|
|
565
590
|
* Execution evidence now lives in the PendingStore's own execution record, beside the state
|
|
@@ -604,13 +629,22 @@ export interface ResumeController extends ResumeConfig {
|
|
|
604
629
|
park(p: Omit<PendingAction, "createdAt" | "argsSig">): Promise<void>;
|
|
605
630
|
/** Drive a parked decision to completion once a verdict is known. Idempotent: a second call
|
|
606
631
|
* after success answers `already_resolved` (no double-write); an id this SDK never parked
|
|
607
|
-
* answers `unknown`; a call that loses the pre-invocation claim answers `lost_race`.
|
|
632
|
+
* answers `unknown`; a call that loses the pre-invocation claim answers `lost_race`.
|
|
633
|
+
*
|
|
634
|
+
* `verdict: "approved"` is a REQUEST to execute, never the authority (0.7.0): right before the
|
|
635
|
+
* action runs, resolve() confirms with Decidio that THIS parked request was approved (GET
|
|
636
|
+
* /agent/status + the request commitment — `checkApproval`). Decidio still pending → `not_approved`;
|
|
637
|
+
* Decidio says rejected/blocked → recorded as that denial; an approval that cannot be tied to these
|
|
638
|
+
* arguments → `request_unverified`. A transport failure during that check throws (nothing ran). A
|
|
639
|
+
* `rejected`/`blocked` verdict is applied as given — it can only ever prevent execution. */
|
|
608
640
|
resolve(input: {
|
|
609
641
|
decisionId: string;
|
|
610
642
|
verdict: "approved" | "rejected" | "blocked";
|
|
611
643
|
reason?: string;
|
|
612
644
|
}): Promise<ResumeOutcome>;
|
|
613
|
-
/** node:http listener for Decidio's signed approval webhook (Express/Fastify/node).
|
|
645
|
+
/** node:http listener for Decidio's signed approval webhook (Express/Fastify/node). The webhook is a
|
|
646
|
+
* WAKE-UP (0.7.0): after its signature is verified — with `webhookSecret` only, never the API token —
|
|
647
|
+
* only its decision id is read; the outcome follows Decidio's own state for that decision. */
|
|
614
648
|
webhookHandler(): RequestListener;
|
|
615
649
|
/** Web-Fetch handler (Request → Response) for Next.js route handlers, Hono,
|
|
616
650
|
* Cloudflare Workers, Bun, Deno. Same fail-closed signature check. */
|
|
@@ -666,6 +700,75 @@ export declare function verifyResumeSignature(secret: string, body: string, head
|
|
|
666
700
|
* is never evidence). Its own comment claimed it was "shared by every confirm door", and no door
|
|
667
701
|
* called it: the claim was what kept anyone from noticing the disagreement. */
|
|
668
702
|
export declare function assertConfirmRecorded(body: any, decisionId: string, signed: boolean): string;
|
|
703
|
+
/** What GET /agent/status answers. `requestCommitment` (0.7.0) is the commitment the decision was
|
|
704
|
+
* authorized with, or null for a decision whose request was never bound. `spent` and `grantExpiresAt` (0.8.0,
|
|
705
|
+
* mini-approvals): the grant is ONE-TIME - `spent` once an execution was recorded on it, `grantExpiresAt` when
|
|
706
|
+
* it lapses (the seal's time plus the gate's TTL) for a granted decision, null otherwise. A gate that predates
|
|
707
|
+
* them answers neither. */
|
|
708
|
+
export interface DecisionStatus {
|
|
709
|
+
state: string;
|
|
710
|
+
decisionId?: string;
|
|
711
|
+
receiptId?: string | null;
|
|
712
|
+
reason?: string | null;
|
|
713
|
+
requestCommitment?: string | null;
|
|
714
|
+
spent?: boolean;
|
|
715
|
+
grantExpiresAt?: string | null;
|
|
716
|
+
}
|
|
717
|
+
type StatusConfig = {
|
|
718
|
+
apiUrl: string;
|
|
719
|
+
apiToken?: string;
|
|
720
|
+
agentKey?: string;
|
|
721
|
+
agentDid?: string;
|
|
722
|
+
fetchImpl?: typeof fetch;
|
|
723
|
+
};
|
|
724
|
+
/** Why an approval check did not pass.
|
|
725
|
+
* pending / unrecognized_state — Decidio records no approval (yet): nothing may run.
|
|
726
|
+
* rejected / blocked — Decidio's final answer is no.
|
|
727
|
+
* spent — the approval was already used: an execution is recorded on it (one approval
|
|
728
|
+
* covers one action, once) - ask again.
|
|
729
|
+
* expired — the approval lapsed (its grant's time passed) before anything ran - ask again.
|
|
730
|
+
* request_changed — the approval is for a DIFFERENT request than the one about to run.
|
|
731
|
+
* commitment_missing — this SDK bound the request, the gate answered without a commitment.
|
|
732
|
+
* unverifiable — Decidio holds a commitment this process cannot recompute (no or
|
|
733
|
+
* unreadable key material), or answered a malformed one. */
|
|
734
|
+
export type ApprovalCheckCode = "pending" | "unrecognized_state" | "rejected" | "blocked" | "spent" | "expired" | "request_changed" | "commitment_missing" | "unverifiable";
|
|
735
|
+
/** The outcome of the one check. Plain JSON on purpose: an engine that runs it as an activity or a
|
|
736
|
+
* step (Temporal, Inngest) memoizes it across its serialization boundary. */
|
|
737
|
+
export interface ApprovalCheck {
|
|
738
|
+
ok: boolean;
|
|
739
|
+
decisionId: string;
|
|
740
|
+
/** Decidio's derived state, verbatim ("" when it answered none). */
|
|
741
|
+
state: string;
|
|
742
|
+
code?: ApprovalCheckCode;
|
|
743
|
+
/** The server's (tokenized) reason for a rejection, or this SDK's explanation of a refusal. */
|
|
744
|
+
reason?: string;
|
|
745
|
+
}
|
|
746
|
+
/** CONFIRM WITH DECIDIO THAT *THIS* REQUEST WAS APPROVED — the check every door runs right before it
|
|
747
|
+
* executes a routed approval: the inline blocking wait, the durable resume (webhook AND worker), and
|
|
748
|
+
* each engine adapter (LangGraph, Temporal, Inngest, OpenAI Agents). A resume signal is a wake-up; it
|
|
749
|
+
* is never the authority.
|
|
750
|
+
*
|
|
751
|
+
* Approved means: GET /agent/status/:decisionId answers approved/auto_approved AND the commitment it
|
|
752
|
+
* returns equals a FRESH recompute over `request` — the exact request about to run. The fail-closed
|
|
753
|
+
* rules:
|
|
754
|
+
* - Decidio holds a commitment → it must match; no key material to recompute it → refused.
|
|
755
|
+
* - Decidio holds none, but this SDK bound the request (`sent`, default: this config has key
|
|
756
|
+
* material, so it would have sent one) → refused: the gate predates request binding.
|
|
757
|
+
* - Decidio holds none and nothing was bound (a keyless agent, or an entry parked before 0.7.0) →
|
|
758
|
+
* the state alone decides — nothing on either side could say more.
|
|
759
|
+
* A transport failure THROWS (it is not an answer); every other outcome is a value.
|
|
760
|
+
*
|
|
761
|
+
* `status` lets a caller that has just read the status (the blocking poll, the worker) pass it in
|
|
762
|
+
* instead of reading it twice. */
|
|
763
|
+
export declare function checkApproval(cfg: StatusConfig, decisionId: string, request: CommittedRequest, opts?: {
|
|
764
|
+
sent?: boolean;
|
|
765
|
+
status?: DecisionStatus;
|
|
766
|
+
}): Promise<ApprovalCheck>;
|
|
767
|
+
/** The webhook handlers' refusal when no webhook secret is configured - with the remedy. */
|
|
768
|
+
/** The one remedy every webhook-secret refusal names (#4B review W3): the dedicated webhook secret - NEVER the
|
|
769
|
+
* server's API_AUTH_TOKEN, the admin bearer, which a refusal that said "the value your deployment signs
|
|
770
|
+
* with" used to steer operators into copying onto agent hosts. */
|
|
771
|
+
export declare const WEBHOOK_SECRET_REMEDY = "Set webhookSecret (DECIDIO_WEBHOOK_SECRET) to the dedicated secret your Decidio deployment signs resume webhooks with - never the server's API_AUTH_TOKEN (its admin bearer, which must not be copied onto an agent host) - or use the poll worker (guard.worker()), which needs no inbound secret.";
|
|
669
772
|
export declare function createDecidioResume(opts: ResumeConfig & {
|
|
670
773
|
store: PendingStore;
|
|
671
774
|
}): ResumeController;
|