@decidio/sdk 0.1.12 → 0.2.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/README.md +52 -15
- package/dist/adapters/_gate.d.ts +8 -2
- package/dist/adapters/_gate.js +16 -7
- package/dist/adapters/langgraph.d.ts +3 -1
- package/dist/adapters/langgraph.js +2 -2
- package/dist/adapters/openai.d.ts +7 -2
- package/dist/adapters/openai.js +11 -3
- package/dist/adapters/temporal.d.ts +6 -2
- package/dist/adapters/temporal.js +3 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/canonical.js +8 -4
- package/dist/cli.js +68 -35
- package/dist/errors.d.ts +12 -5
- package/dist/errors.js +37 -5
- package/dist/evidence.d.ts +19 -0
- package/dist/evidence.js +7 -4
- package/dist/guard.d.ts +19 -2
- package/dist/guard.js +7 -5
- package/dist/index.d.ts +27 -2
- package/dist/index.js +32 -12
- package/dist/inngest.d.ts +4 -0
- package/dist/inngest.js +1 -1
- package/dist/resume.d.ts +331 -57
- package/dist/resume.js +678 -139
- package/dist/signing.js +19 -1
- package/openapi.yaml +89 -6
- package/package.json +13 -8
package/dist/errors.js
CHANGED
|
@@ -1,18 +1,50 @@
|
|
|
1
|
+
const RETRYABLE_STATUS = (status) => status === 408 || status === 429 || (status >= 500 && status !== 501);
|
|
2
|
+
function sideEffectNote(endpoint) {
|
|
3
|
+
if (endpoint.startsWith("/agent/confirm")) {
|
|
4
|
+
return " Your action ALREADY RAN — only its report failed. Do NOT re-run it.";
|
|
5
|
+
}
|
|
6
|
+
if (endpoint.startsWith("/agent/status")) {
|
|
7
|
+
return " This call is a read, so it changed nothing; the decision itself is unaffected.";
|
|
8
|
+
}
|
|
9
|
+
return " Nothing was routed or executed.";
|
|
10
|
+
}
|
|
1
11
|
export class DecidioHttpError extends Error {
|
|
2
12
|
status;
|
|
3
13
|
endpoint;
|
|
4
14
|
body;
|
|
5
15
|
retryable;
|
|
6
16
|
constructor(status, endpoint, body) {
|
|
7
|
-
const
|
|
17
|
+
const credential = status === 401 || status === 403;
|
|
18
|
+
const retryable = !credential && RETRYABLE_STATUS(status);
|
|
8
19
|
super(`[decidio] ${endpoint} refused this call (HTTP ${status})` +
|
|
9
|
-
(
|
|
10
|
-
? ` — the credential was rejected. The agent token may have expired or been revoked (re-running \`init\` for the same agent revokes the previous one). Mint a fresh one: npx @decidio/sdk init <agent-name>, then restart this process
|
|
11
|
-
:
|
|
20
|
+
(credential
|
|
21
|
+
? ` — the credential was rejected. The agent token may have expired or been revoked (re-running \`init\` for the same agent revokes the previous one). Mint a fresh one: npx @decidio/sdk init <agent-name>, then restart this process.`
|
|
22
|
+
: retryable
|
|
23
|
+
? ` — this looks transient; retrying is safe (a retry creates a NEW request and can never double-execute).`
|
|
24
|
+
: ` — the server refused the request itself, so retrying it unchanged will fail the same way. Fix the request (or the decision it names) first.`) +
|
|
25
|
+
sideEffectNote(endpoint));
|
|
12
26
|
this.status = status;
|
|
13
27
|
this.endpoint = endpoint;
|
|
14
28
|
this.body = body;
|
|
15
29
|
this.name = "DecidioHttpError";
|
|
16
|
-
this.retryable =
|
|
30
|
+
this.retryable = retryable;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function decisionIdProblem(raw) {
|
|
34
|
+
if (typeof raw !== "string")
|
|
35
|
+
return `it is ${raw === null ? "null" : typeof raw}, not a string`;
|
|
36
|
+
if (raw.length === 0)
|
|
37
|
+
return "it is empty";
|
|
38
|
+
if (raw.trim().length === 0)
|
|
39
|
+
return "it is only whitespace";
|
|
40
|
+
if (raw !== raw.trim())
|
|
41
|
+
return "it has leading or trailing whitespace (ids are used verbatim in URLs, filenames and audit lines — this SDK will not silently trim one)";
|
|
42
|
+
for (const ch of raw) {
|
|
43
|
+
const cp = ch.codePointAt(0);
|
|
44
|
+
if (cp < 0x20 || cp === 0x7f)
|
|
45
|
+
return "it contains control characters";
|
|
17
46
|
}
|
|
47
|
+
if (raw.length > 256)
|
|
48
|
+
return `it is ${raw.length} characters long (limit 256)`;
|
|
49
|
+
return null;
|
|
18
50
|
}
|
package/dist/evidence.d.ts
CHANGED
|
@@ -17,6 +17,25 @@ export interface EvidenceOutcome {
|
|
|
17
17
|
* proof VERIFIED (C3, 0.1.12 — this field used to be read off the wire and discarded, so a
|
|
18
18
|
* failed attestation was invisible to every client door). */
|
|
19
19
|
executionReportVerified?: boolean;
|
|
20
|
+
/** WHY the attestation did not verify, verbatim from the gate (unknown did, expired proof,
|
|
21
|
+
* burnt nonce, payload mismatch). It was folded into `diagnostic` prose and nowhere else, so a
|
|
22
|
+
* caller who wanted to BRANCH on the reason — retry vs re-register vs alert — had to regex an
|
|
23
|
+
* English sentence. Same rule as the typed HTTP errors: give callers the fact, not a string to
|
|
24
|
+
* parse. */
|
|
25
|
+
executionReportReason?: string;
|
|
26
|
+
/** Whether re-sending this report could succeed. False for a refusal the server will repeat
|
|
27
|
+
* (an unrecognised tier, a denied success flag); true for a transport or availability failure.
|
|
28
|
+
* Undefined when the report WAS recorded and there is nothing to retry. */
|
|
29
|
+
confirmationRetryable?: boolean;
|
|
30
|
+
/** Whether the reported value reached durable storage, so `reconfirm()` can re-send it. On the
|
|
31
|
+
* inline path this is false by construction — nothing is parked — which is exactly why an
|
|
32
|
+
* inline caller needs to see the outcome at the call site rather than in a log line. */
|
|
33
|
+
reportJournalled?: boolean;
|
|
34
|
+
/** Set when the caller's `evidence` mapper (or the canonical model) rejected the return value,
|
|
35
|
+
* so an EMPTY captured response was reported. The execution is recorded; its evidence is not.
|
|
36
|
+
* This was previously a console.warn and nothing else — invisible to the code that could
|
|
37
|
+
* actually fix it. */
|
|
38
|
+
evidenceError?: string;
|
|
20
39
|
}
|
|
21
40
|
/** The SERVER's rule for `application_confirmed`, restated here exactly as implemented so this
|
|
22
41
|
* explanation can never drift from it: the captured response must carry a RECORD ID
|
package/dist/evidence.js
CHANGED
|
@@ -20,19 +20,20 @@ export function describeTierGap(capturedResponse) {
|
|
|
20
20
|
return "the report was accepted but the gate recorded a weaker tier — check that this agent's registration and signing key still match (`npx @decidio/sdk doctor`).";
|
|
21
21
|
}
|
|
22
22
|
export function readConfirmResponse(body, opts) {
|
|
23
|
-
const
|
|
23
|
+
const journalled = opts.journalled ?? false;
|
|
24
|
+
const retry = journalled
|
|
24
25
|
? "re-send the report with `guard.resume.reconfirm(decisionId)` once connectivity or the service is healthy."
|
|
25
26
|
: "this path journalled no report, so there is nothing to re-send from here — record the outcome from your own system, or run the action through `guard.protect`'s durable mode, where reports ARE journalled and `reconfirm` can replay them.";
|
|
26
27
|
const b = body;
|
|
27
28
|
if (!b || b.ok !== true || typeof b.confirmation !== "string") {
|
|
28
|
-
return { reportRecorded: false, executionConfirmed: false, diagnostic: `the gate answered 2xx but named no evidence tier — the execution report is NOT recorded. Your action DID run; ${retry}` };
|
|
29
|
+
return { reportRecorded: false, executionConfirmed: false, confirmationRetryable: true, reportJournalled: journalled, diagnostic: `the gate answered 2xx but named no evidence tier — the execution report is NOT recorded. Your action DID run; ${retry}` };
|
|
29
30
|
}
|
|
30
31
|
const tier = b.confirmation;
|
|
31
32
|
if (!EVIDENCE_TIERS.includes(tier)) {
|
|
32
|
-
return { reportRecorded: false, executionConfirmed: false, diagnostic: `the gate reported an evidence tier this SDK does not recognise (${JSON.stringify(tier)}) — refusing to treat it as evidence. Upgrade @decidio/sdk; your action DID run, and ${retry}` };
|
|
33
|
+
return { reportRecorded: false, executionConfirmed: false, confirmationRetryable: false, reportJournalled: journalled, diagnostic: `the gate reported an evidence tier this SDK does not recognise (${JSON.stringify(tier)}) — refusing to treat it as evidence. Upgrade @decidio/sdk; your action DID run, and ${retry}` };
|
|
33
34
|
}
|
|
34
35
|
const executionConfirmed = CONFIRMING.includes(tier);
|
|
35
|
-
const outcome = { reportRecorded: true, evidenceTier: tier, executionConfirmed };
|
|
36
|
+
const outcome = { reportRecorded: true, evidenceTier: tier, executionConfirmed, reportJournalled: journalled };
|
|
36
37
|
if (!executionConfirmed && opts.signed)
|
|
37
38
|
outcome.diagnostic = describeTierGap(opts.capturedResponse);
|
|
38
39
|
else if (!executionConfirmed)
|
|
@@ -40,6 +41,8 @@ export function readConfirmResponse(body, opts) {
|
|
|
40
41
|
const rpt = body?.executionReport;
|
|
41
42
|
if (rpt && typeof rpt === "object" && typeof rpt.verified === "boolean") {
|
|
42
43
|
outcome.executionReportVerified = rpt.verified;
|
|
44
|
+
if (typeof rpt.reason === "string" && rpt.reason)
|
|
45
|
+
outcome.executionReportReason = rpt.reason;
|
|
43
46
|
if (!rpt.verified) {
|
|
44
47
|
const why = typeof rpt.reason === "string" && rpt.reason ? ` (${rpt.reason})` : "";
|
|
45
48
|
outcome.diagnostic = `${outcome.diagnostic ? outcome.diagnostic + " " : ""}The signed execution report did NOT verify${why} — the report was recorded without a proven attestation. Check the agent's key/DID/registration (npx @decidio/sdk doctor).`;
|
package/dist/guard.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ApprovalContext } from "./index.js";
|
|
1
|
+
import { type ApprovalContext, type ProtectedExecution } from "./index.js";
|
|
2
2
|
import { type ResumeController, type ResumeOutcome } from "./resume.js";
|
|
3
3
|
export interface GuardFacadeConfig {
|
|
4
4
|
apiUrl: string;
|
|
@@ -28,6 +28,12 @@ export interface ProtectOptions {
|
|
|
28
28
|
* across every door), but their ENGINE integration is verified against structural mocks, not
|
|
29
29
|
* live engines. Use the core path for production until that changes. */
|
|
30
30
|
adapter?: "generic" | "langgraph" | "openai" | "temporal" | "inngest";
|
|
31
|
+
/** Hand the action an `ExecutionContext` as its FIRST argument: `{ decisionId, attemptId,
|
|
32
|
+
* idempotencyKey }`. Pass `idempotencyKey` to your downstream API and the one window this SDK
|
|
33
|
+
* cannot close — the vendor committing twice — closes on their side. Opt-in by design: arity
|
|
34
|
+
* inspection would have to guess, and guessing wrong hands a payment call a context object
|
|
35
|
+
* where it expected an invoice. */
|
|
36
|
+
withContext?: boolean;
|
|
31
37
|
/** "durable" (default) suspends for async approval; "blocking" polls (short approvals only). */
|
|
32
38
|
mode?: "durable" | "blocking";
|
|
33
39
|
/** The action name this wrapper produces, declared UP FRONT so the resume registry is populated
|
|
@@ -84,7 +90,18 @@ export declare class Guard {
|
|
|
84
90
|
/** Throws when the facade has no gate URL — at USE time, not import time, so `import { guard }`
|
|
85
91
|
* stays safe in envless contexts. */
|
|
86
92
|
private assertConfigured;
|
|
87
|
-
|
|
93
|
+
/** Wrap a function so every call is authorized, recorded and — in 0.2.0 — REPORTED BACK.
|
|
94
|
+
*
|
|
95
|
+
* Returns `{ value, decisionId, confirmation }`, not the bare value. Through 0.1.x it returned
|
|
96
|
+
* `R` and the confirmation outcome went to a console warning, so the one thing a caller of an
|
|
97
|
+
* authority SDK could not do was find out what Decidio recorded. If you genuinely do not need
|
|
98
|
+
* it, `protectBestEffort` keeps the old shape. */
|
|
99
|
+
protect<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<ProtectedExecution<R>>;
|
|
100
|
+
/** @deprecated The 0.1.x shape: resolves to your function's value alone and DISCARDS what
|
|
101
|
+
* Decidio recorded. Kept because a caller who truly does not care should not have to
|
|
102
|
+
* destructure — but "the report was not recorded" and "the execution could not be attested"
|
|
103
|
+
* become invisible again, which is the gap 0.2.0 exists to close. Prefer `protect`. */
|
|
104
|
+
protectBestEffort<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<R>;
|
|
88
105
|
}
|
|
89
106
|
/** Preconfigured singleton — `import { guard } from "@decidio/sdk"`. */
|
|
90
107
|
export declare const guard: Guard;
|
package/dist/guard.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { withApproval } from "./index.js";
|
|
2
2
|
import { createDecidioResume, FilePendingStore } from "./resume.js";
|
|
3
|
-
import { join } from "node:path";
|
|
4
3
|
export class Guard {
|
|
5
4
|
config;
|
|
6
5
|
_resume;
|
|
@@ -38,7 +37,6 @@ export class Guard {
|
|
|
38
37
|
this._resume = createDecidioResume({
|
|
39
38
|
apiUrl: c.apiUrl, apiToken: c.apiToken, webhookSecret: c.webhookSecret,
|
|
40
39
|
resumeUrl: c.resumeUrl, store: new FilePendingStore(c.pendingDir ?? ".decidio-pending"),
|
|
41
|
-
reportDir: join(c.pendingDir ?? ".decidio-pending", "reports"),
|
|
42
40
|
agentId: c.agentId, agentKey: c.agentKey, agentDid: c.agentDid, workspaceId: c.workspaceId,
|
|
43
41
|
fetchImpl: c.fetchImpl,
|
|
44
42
|
});
|
|
@@ -70,12 +68,16 @@ export class Guard {
|
|
|
70
68
|
`(pollTimeoutMs), and when it does, the decision stays OPEN in Decidio while this process stops waiting: ` +
|
|
71
69
|
`a later approval cannot resume it. Only safe for short, watched approvals — omit mode:"blocking" for ` +
|
|
72
70
|
`durable async resume on real (minutes-to-days) approvals.`);
|
|
73
|
-
return withApproval(fn, describe, base, opts?.evidence ? { evidence: opts.evidence } :
|
|
71
|
+
return withApproval(fn, describe, base, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
|
|
74
72
|
}
|
|
75
73
|
const resume = this.ensureResume();
|
|
76
74
|
if (opts?.action)
|
|
77
|
-
resume.register(opts.action, fn, opts?.evidence);
|
|
78
|
-
return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume }, opts?.evidence ? { evidence: opts.evidence } :
|
|
75
|
+
resume.register(opts.action, fn, opts?.evidence, opts?.withContext);
|
|
76
|
+
return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume }, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
|
|
77
|
+
}
|
|
78
|
+
protectBestEffort(fn, describe, opts) {
|
|
79
|
+
const wrapped = this.protect(fn, describe, opts);
|
|
80
|
+
return async (...a) => (await wrapped(...a)).value;
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
export const guard = Guard.fromEnv();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ResumeController } from "./resume.js";
|
|
2
|
+
import { type EvidenceOutcome } from "./evidence.js";
|
|
2
3
|
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
3
4
|
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome } from "./resume.js";
|
|
4
5
|
export { guard, Guard } from "./guard.js";
|
|
5
6
|
export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
6
|
-
export { DecidioHttpError } from "./errors.js";
|
|
7
|
+
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
7
8
|
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
8
9
|
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
9
10
|
export type { JsonValue, JsonPrimitive } from "./canonical.js";
|
|
@@ -107,10 +108,34 @@ export declare class DecidioNetworkError extends Error {
|
|
|
107
108
|
/** `<appUrl>/d/<decisionId>` — the deep link that opens this decision directly, on the web app or
|
|
108
109
|
* the phone build. Null when DECIDIO_APP_URL is not configured, never a half-built URL. */
|
|
109
110
|
export declare function decisionUrl(appUrl: string | undefined, decisionId: string): string | null;
|
|
111
|
+
/** What a protected call returns in 0.2.0: your function's value, the decision it was authorized
|
|
112
|
+
* under, and what Decidio actually recorded about the execution.
|
|
113
|
+
*
|
|
114
|
+
* Through 0.1.x this was just `R`. The SDK computed the full confirmation outcome — tier, whether
|
|
115
|
+
* the report was recorded, whether the signed execution report verified, whether the evidence
|
|
116
|
+
* mapper failed — and then dropped it on the floor, surfacing it only as a console warning. For a
|
|
117
|
+
* product whose value IS the record, the one thing a caller could not do was find out what got
|
|
118
|
+
* recorded. Returning it makes "my action ran but was logged as an unproven assertion" something
|
|
119
|
+
* code can branch on instead of something a human might notice in a log.
|
|
120
|
+
*
|
|
121
|
+
* `protectBestEffort` keeps the old value-only shape for callers that genuinely do not care. */
|
|
122
|
+
export interface ProtectedExecution<R> {
|
|
123
|
+
/** Exactly what your function returned — unchanged, uncanonicalized, your object. */
|
|
124
|
+
value: R;
|
|
125
|
+
/** The decision this execution was authorized under. Use it as your downstream idempotency key. */
|
|
126
|
+
decisionId: string;
|
|
127
|
+
/** What Decidio recorded. `reportRecorded` false means the execution happened and Decidio does
|
|
128
|
+
* NOT know; `executionConfirmed` false means it knows but could not attest a commit. */
|
|
129
|
+
confirmation: EvidenceOutcome;
|
|
130
|
+
}
|
|
110
131
|
export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig, opts?: {
|
|
111
132
|
/** Map YOUR function's natural return value to the evidence Decidio records. The gate awards
|
|
112
133
|
* its strongest wrapper tier (application_confirmed) only for a committed record id plus a
|
|
113
134
|
* success indication — this lets a real API keep its own shape instead of being forced into
|
|
114
135
|
* magic field names: `evidence: (r) => ({ recordId: r.transactionId, success: r.status === "committed" })`. */
|
|
115
136
|
evidence?: (result: R) => unknown;
|
|
116
|
-
|
|
137
|
+
/** Hand the action an `ExecutionContext` as its FIRST argument: `{ decisionId, attemptId,
|
|
138
|
+
* idempotencyKey }`. Opt-in, never inferred from the function's arity — guessing wrong means
|
|
139
|
+
* passing a context object as someone's first business argument. */
|
|
140
|
+
withContext?: boolean;
|
|
141
|
+
}): (...args: A) => Promise<ProtectedExecution<R>>;
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import { canonicalize, canonicalizeArgs } from "./canonical.js";
|
|
|
4
4
|
import { readConfirmResponse, buildConfirmBody } from "./evidence.js";
|
|
5
5
|
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
6
6
|
export { guard, Guard } from "./guard.js";
|
|
7
|
-
export { DecidioHttpError } from "./errors.js";
|
|
8
|
-
import { DecidioHttpError } from "./errors.js";
|
|
7
|
+
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
8
|
+
import { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
9
9
|
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
10
10
|
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
11
11
|
export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
|
|
@@ -142,6 +142,12 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
142
142
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
143
143
|
: undefined;
|
|
144
144
|
canonicalize(ctx, "describe(...)");
|
|
145
|
+
if (typeof ctx?.action !== "string" || ctx.action.trim().length === 0) {
|
|
146
|
+
throw new Error(`[decidio] describe(...) must return an \`action\` string naming what is being asked (got ${JSON.stringify(ctx?.action ?? null)}). Nothing was routed or executed.`);
|
|
147
|
+
}
|
|
148
|
+
if (typeof ctx.amount !== "number" || !Number.isFinite(ctx.amount) || ctx.amount < 0) {
|
|
149
|
+
throw new Error(`[decidio] describe(...) must return a finite, non-negative \`amount\` — the policy decides on it, so a request without one is not a request Decidio can rule on (got ${JSON.stringify(ctx.amount ?? null)}). Use 0 for an action with no monetary value. Nothing was routed or executed.`);
|
|
150
|
+
}
|
|
145
151
|
const auth = await post(config, "/agent/authorize", {
|
|
146
152
|
requester: config.agentId, action: ctx.action, amount: ctx.amount,
|
|
147
153
|
scope: ctx.scope, sourceSystem: config.sourceSystem,
|
|
@@ -151,8 +157,9 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
151
157
|
});
|
|
152
158
|
if (auth.decision !== "proceed" && auth.decision !== "route" && auth.decision !== "block")
|
|
153
159
|
throw new DecidioBlockedError(`unrecognized gate verdict: ${String(auth.decision)}`, auth.decisionId);
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
const idProblem = decisionIdProblem(auth.decisionId);
|
|
161
|
+
if (idProblem)
|
|
162
|
+
throw new DecidioBlockedError(`the gate answered "${auth.decision}" with no usable decisionId (${JSON.stringify(auth.decisionId ?? null)} — ${idProblem}) — refusing to execute. Every authorized action must be addressable: without an id it cannot be polled, resumed, confirmed or audited.`, typeof auth.decisionId === "string" ? auth.decisionId : String(auth.decisionId ?? ""));
|
|
156
163
|
if (auth.decision === "block")
|
|
157
164
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
158
165
|
if (auth.decision === "route") {
|
|
@@ -188,7 +195,9 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
188
195
|
await sleep(pollInterval);
|
|
189
196
|
}
|
|
190
197
|
}
|
|
191
|
-
const result =
|
|
198
|
+
const result = opts?.withContext
|
|
199
|
+
? await fn({ decisionId: auth.decisionId, attemptId: null, idempotencyKey: auth.decisionId }, ...args)
|
|
200
|
+
: await fn(...args);
|
|
192
201
|
let reported = null;
|
|
193
202
|
let evidenceProblem;
|
|
194
203
|
try {
|
|
@@ -199,20 +208,31 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
199
208
|
evidenceProblem = String(e?.message ?? e);
|
|
200
209
|
console.warn(`[decidio] ${auth.decisionId}: the action SUCCEEDED but its result could not be reported as evidence (${evidenceProblem}). Reporting an empty captured response so the execution is still recorded — map the return value with the \`evidence\` option (e.g. evidence: (r) => ({ recordId: String(r.id), success: true })) to earn a confirming tier. The action was NOT re-run.`);
|
|
201
210
|
}
|
|
211
|
+
let confirmation;
|
|
202
212
|
try {
|
|
203
213
|
const confirmIdentity = signConfirm(auth.decisionId, config);
|
|
204
214
|
const confirmBody = await post(config, "/agent/confirm", buildConfirmBody(config, auth.decisionId, reported, confirmIdentity));
|
|
205
|
-
|
|
206
|
-
if (!
|
|
207
|
-
console.warn(`[decidio] ${auth.decisionId}: ${
|
|
208
|
-
else if (!
|
|
209
|
-
console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as ${
|
|
215
|
+
confirmation = readConfirmResponse(confirmBody, { signed: !!confirmIdentity, capturedResponse: reported, journalled: false });
|
|
216
|
+
if (!confirmation.reportRecorded)
|
|
217
|
+
console.warn(`[decidio] ${auth.decisionId}: ${confirmation.diagnostic}`);
|
|
218
|
+
else if (!confirmation.executionConfirmed || confirmation.executionReportVerified === false)
|
|
219
|
+
console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as ${confirmation.evidenceTier} — ${confirmation.diagnostic}`);
|
|
210
220
|
if (evidenceProblem)
|
|
211
221
|
console.warn(`[decidio] ${auth.decisionId}: the execution IS recorded, but with an empty captured response — see the evidence note above.`);
|
|
212
222
|
}
|
|
213
223
|
catch (e) {
|
|
214
|
-
|
|
224
|
+
const isHttp = e instanceof DecidioHttpError;
|
|
225
|
+
console.warn(`[decidio] confirmation post failed (action succeeded): ${e?.message ?? e}`);
|
|
226
|
+
confirmation = {
|
|
227
|
+
reportRecorded: false,
|
|
228
|
+
executionConfirmed: false,
|
|
229
|
+
confirmationRetryable: isHttp ? e.retryable : true,
|
|
230
|
+
reportJournalled: false,
|
|
231
|
+
diagnostic: `your action RAN and Decidio does NOT have its execution report (${e?.message ?? e}). This path journals nothing, so there is no local copy to re-send: record the outcome from your own system. Durable mode DOES journal reports, and its recovery path can re-send them — that is the mode to use if this matters to you. Do NOT re-run the action.`,
|
|
232
|
+
};
|
|
215
233
|
}
|
|
216
|
-
|
|
234
|
+
if (evidenceProblem)
|
|
235
|
+
confirmation.evidenceError = evidenceProblem;
|
|
236
|
+
return { value: result, decisionId: auth.decisionId, confirmation };
|
|
217
237
|
};
|
|
218
238
|
}
|
package/dist/inngest.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export declare function decidioGateStep<R>(args: {
|
|
|
45
45
|
ctx: ApprovalContext;
|
|
46
46
|
run: () => Promise<R>;
|
|
47
47
|
timeout?: string;
|
|
48
|
+
/** Map your action's return value to the evidence Decidio records — the same option the core
|
|
49
|
+
* path takes. This door's own confirm chokepoint tells callers to use it; until 0.2.0 no
|
|
50
|
+
* adapter door accepted one, so the advice was unfollowable everywhere it was printed. */
|
|
51
|
+
evidence?: (result: R) => unknown;
|
|
48
52
|
}): Promise<GateStepOutcome<R>>;
|
|
49
53
|
/**
|
|
50
54
|
* Decidio → Inngest bridge. Mount as an HTTP endpoint and register it as the agent's
|
package/dist/inngest.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function decidioGateStep(args) {
|
|
|
22
22
|
return { status: "blocked", decisionId: auth.decisionId };
|
|
23
23
|
}
|
|
24
24
|
const result = await step.run("agent-action", args.run);
|
|
25
|
-
await step.run("decidio-confirm", async () => { await gateConfirm(config, auth.decisionId, result); return true; });
|
|
25
|
+
await step.run("decidio-confirm", async () => { await gateConfirm(config, auth.decisionId, result, args.evidence); return true; });
|
|
26
26
|
return { status: "executed", decisionId: auth.decisionId, result };
|
|
27
27
|
}
|
|
28
28
|
export function createInngestResumeBridge(opts) {
|