@decidio/sdk 0.6.4 → 0.7.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 +103 -23
- package/dist/adapters/_gate.d.ts +27 -2
- package/dist/adapters/_gate.js +33 -2
- 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 +9 -0
- package/dist/cli.js +252 -37
- package/dist/demo-gate.js +6 -4
- package/dist/index.d.ts +36 -4
- package/dist/index.js +48 -5
- package/dist/inngest.d.ts +5 -2
- package/dist/inngest.js +23 -15
- package/dist/resume.d.ts +98 -5
- package/dist/resume.js +203 -109
- package/dist/signing.d.ts +35 -0
- package/dist/signing.js +40 -1
- package/openapi.yaml +65 -6
- package/package.json +2 -2
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, 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,38 @@ 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
|
+
default: return new DecidioBlockedError(check.reason ?? `not approved (Decidio answered ${JSON.stringify(check.state)})`, id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function coreRequest(ctx, args) {
|
|
62
|
+
return { action: ctx.action, amount: ctx.amount, ...(ctx.scope !== undefined && ctx.scope !== null ? { scope: ctx.scope } : {}), args };
|
|
63
|
+
}
|
|
32
64
|
export class DecidioTimeoutError extends Error {
|
|
33
65
|
decisionId;
|
|
34
66
|
withdrawn;
|
|
@@ -153,12 +185,16 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
153
185
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
154
186
|
: undefined;
|
|
155
187
|
assertDescribed(ctx);
|
|
188
|
+
const material = bindingMaterial(config);
|
|
189
|
+
const committed = coreRequest(ctx, args);
|
|
190
|
+
const sentCommitment = material ? requestCommitment(material, committed) : undefined;
|
|
156
191
|
const auth = await post(config, "/agent/authorize", {
|
|
157
192
|
requester: config.agentId, action: ctx.action, amount: ctx.amount,
|
|
158
193
|
scope: ctx.scope, sourceSystem: config.sourceSystem,
|
|
159
194
|
...(ctx.context ? { context: ctx.context } : {}),
|
|
160
195
|
...(requesterIdentity ? { requesterIdentity } : {}),
|
|
161
196
|
...(resumeUrl ? { resumeUrl } : {}),
|
|
197
|
+
...(sentCommitment ? { requestCommitment: sentCommitment } : {}),
|
|
162
198
|
});
|
|
163
199
|
if (auth.decision !== "proceed" && auth.decision !== "route" && auth.decision !== "block")
|
|
164
200
|
throw new DecidioBlockedError(`unrecognized gate verdict: ${String(auth.decision)}`, auth.decisionId);
|
|
@@ -181,14 +217,18 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
181
217
|
await config.resume.park({
|
|
182
218
|
decisionId: auth.decisionId, receiptId: auth.receiptId,
|
|
183
219
|
action: ctx.action, amount: ctx.amount, scope: ctx.scope, args,
|
|
220
|
+
...(sentCommitment ? { requestCommitment: sentCommitment } : {}),
|
|
184
221
|
});
|
|
185
222
|
throw new DecidioSuspendedError(auth.decisionId, ctx.action);
|
|
186
223
|
}
|
|
187
224
|
const deadline = Date.now() + pollTimeout;
|
|
225
|
+
let approvedStatus;
|
|
188
226
|
while (true) {
|
|
189
227
|
const st = await get(config, `/agent/status/${encodeURIComponent(auth.decisionId)}`);
|
|
190
|
-
if (st.state === "approved" || st.state === "auto_approved")
|
|
228
|
+
if (st.state === "approved" || st.state === "auto_approved") {
|
|
229
|
+
approvedStatus = st;
|
|
191
230
|
break;
|
|
231
|
+
}
|
|
192
232
|
if (st.state === "rejected")
|
|
193
233
|
throw new DecidioRejectedError(st.reason ?? "rejected by approver", auth.decisionId);
|
|
194
234
|
if (st.state === "blocked")
|
|
@@ -215,6 +255,9 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
215
255
|
}
|
|
216
256
|
await sleep(pollInterval);
|
|
217
257
|
}
|
|
258
|
+
const check = await checkApproval(config, auth.decisionId, committed, { sent: !!sentCommitment, ...(approvedStatus ? { status: approvedStatus } : {}) });
|
|
259
|
+
if (!check.ok)
|
|
260
|
+
throw approvalRefusal(check);
|
|
218
261
|
}
|
|
219
262
|
const result = opts?.withContext
|
|
220
263
|
? 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,65 @@ 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. */
|
|
705
|
+
export interface DecisionStatus {
|
|
706
|
+
state: string;
|
|
707
|
+
reason?: string | null;
|
|
708
|
+
requestCommitment?: string | null;
|
|
709
|
+
}
|
|
710
|
+
type StatusConfig = {
|
|
711
|
+
apiUrl: string;
|
|
712
|
+
apiToken?: string;
|
|
713
|
+
agentKey?: string;
|
|
714
|
+
agentDid?: string;
|
|
715
|
+
fetchImpl?: typeof fetch;
|
|
716
|
+
};
|
|
717
|
+
/** Why an approval check did not pass.
|
|
718
|
+
* pending / unrecognized_state — Decidio records no approval (yet): nothing may run.
|
|
719
|
+
* rejected / blocked — Decidio's final answer is no.
|
|
720
|
+
* request_changed — the approval is for a DIFFERENT request than the one about to run.
|
|
721
|
+
* commitment_missing — this SDK bound the request, the gate answered without a commitment.
|
|
722
|
+
* unverifiable — Decidio holds a commitment this process cannot recompute (no or
|
|
723
|
+
* unreadable key material), or answered a malformed one. */
|
|
724
|
+
export type ApprovalCheckCode = "pending" | "unrecognized_state" | "rejected" | "blocked" | "request_changed" | "commitment_missing" | "unverifiable";
|
|
725
|
+
/** The outcome of the one check. Plain JSON on purpose: an engine that runs it as an activity or a
|
|
726
|
+
* step (Temporal, Inngest) memoizes it across its serialization boundary. */
|
|
727
|
+
export interface ApprovalCheck {
|
|
728
|
+
ok: boolean;
|
|
729
|
+
decisionId: string;
|
|
730
|
+
/** Decidio's derived state, verbatim ("" when it answered none). */
|
|
731
|
+
state: string;
|
|
732
|
+
code?: ApprovalCheckCode;
|
|
733
|
+
/** The server's (tokenized) reason for a rejection, or this SDK's explanation of a refusal. */
|
|
734
|
+
reason?: string;
|
|
735
|
+
}
|
|
736
|
+
/** CONFIRM WITH DECIDIO THAT *THIS* REQUEST WAS APPROVED — the check every door runs right before it
|
|
737
|
+
* executes a routed approval: the inline blocking wait, the durable resume (webhook AND worker), and
|
|
738
|
+
* each engine adapter (LangGraph, Temporal, Inngest, OpenAI Agents). A resume signal is a wake-up; it
|
|
739
|
+
* is never the authority.
|
|
740
|
+
*
|
|
741
|
+
* Approved means: GET /agent/status/:decisionId answers approved/auto_approved AND the commitment it
|
|
742
|
+
* returns equals a FRESH recompute over `request` — the exact request about to run. The fail-closed
|
|
743
|
+
* rules:
|
|
744
|
+
* - Decidio holds a commitment → it must match; no key material to recompute it → refused.
|
|
745
|
+
* - Decidio holds none, but this SDK bound the request (`sent`, default: this config has key
|
|
746
|
+
* material, so it would have sent one) → refused: the gate predates request binding.
|
|
747
|
+
* - Decidio holds none and nothing was bound (a keyless agent, or an entry parked before 0.7.0) →
|
|
748
|
+
* the state alone decides — nothing on either side could say more.
|
|
749
|
+
* A transport failure THROWS (it is not an answer); every other outcome is a value.
|
|
750
|
+
*
|
|
751
|
+
* `status` lets a caller that has just read the status (the blocking poll, the worker) pass it in
|
|
752
|
+
* instead of reading it twice. */
|
|
753
|
+
export declare function checkApproval(cfg: StatusConfig, decisionId: string, request: CommittedRequest, opts?: {
|
|
754
|
+
sent?: boolean;
|
|
755
|
+
status?: DecisionStatus;
|
|
756
|
+
}): Promise<ApprovalCheck>;
|
|
757
|
+
/** The webhook handlers' refusal when no webhook secret is configured - with the remedy. */
|
|
758
|
+
/** The one remedy every webhook-secret refusal names (#4B review W3): the dedicated webhook secret - NEVER the
|
|
759
|
+
* server's API_AUTH_TOKEN, the admin bearer, which a refusal that said "the value your deployment signs
|
|
760
|
+
* with" used to steer operators into copying onto agent hosts. */
|
|
761
|
+
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
762
|
export declare function createDecidioResume(opts: ResumeConfig & {
|
|
670
763
|
store: PendingStore;
|
|
671
764
|
}): ResumeController;
|