@decidio/sdk 0.1.10 → 0.1.12
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 +126 -25
- package/dist/adapters/_gate.d.ts +25 -1
- package/dist/adapters/_gate.js +76 -9
- package/dist/adapters/langgraph.js +2 -1
- package/dist/adapters/openai.js +2 -1
- package/dist/adapters/temporal.js +17 -3
- package/dist/canonical.d.ts +18 -0
- package/dist/canonical.js +128 -0
- package/dist/cli.js +182 -30
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +18 -0
- package/dist/evidence.d.ts +45 -0
- package/dist/evidence.js +65 -0
- package/dist/guard.d.ts +22 -3
- package/dist/guard.js +18 -11
- package/dist/index.d.ts +13 -2
- package/dist/index.js +58 -29
- package/dist/inngest.js +6 -30
- package/dist/resume.d.ts +78 -17
- package/dist/resume.js +226 -55
- package/dist/signing.d.ts +64 -0
- package/dist/signing.js +102 -3
- package/openapi.yaml +47 -1
- package/package.json +2 -2
package/dist/guard.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { withApproval } from "./index.js";
|
|
2
2
|
import { createDecidioResume, FilePendingStore } from "./resume.js";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
export class Guard {
|
|
4
5
|
config;
|
|
5
|
-
|
|
6
|
+
_resume;
|
|
6
7
|
constructor(config, resume) {
|
|
7
8
|
this.config = config;
|
|
8
|
-
this.
|
|
9
|
+
this._resume = resume;
|
|
9
10
|
}
|
|
11
|
+
get resume() { return this.ensureResume(); }
|
|
12
|
+
set resume(r) { this._resume = r; }
|
|
10
13
|
static fromEnv() {
|
|
11
14
|
return new Guard({
|
|
12
15
|
apiUrl: process.env.DECIDIO_API_URL ?? "",
|
|
@@ -22,7 +25,7 @@ export class Guard {
|
|
|
22
25
|
appUrl: process.env.DECIDIO_APP_URL,
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
|
-
register(action, fn) { this.ensureResume().register(action, fn); }
|
|
28
|
+
register(action, fn, evidence) { this.ensureResume().register(action, fn, evidence); }
|
|
26
29
|
resumeHandler() { return this.ensureResume().webhookHandler(); }
|
|
27
30
|
resumeFetchHandler() { return this.ensureResume().fetchHandler(); }
|
|
28
31
|
worker(opts) {
|
|
@@ -30,16 +33,17 @@ export class Guard {
|
|
|
30
33
|
}
|
|
31
34
|
ensureResume() {
|
|
32
35
|
this.assertConfigured();
|
|
33
|
-
if (!this.
|
|
36
|
+
if (!this._resume) {
|
|
34
37
|
const c = this.config;
|
|
35
|
-
this.
|
|
38
|
+
this._resume = createDecidioResume({
|
|
36
39
|
apiUrl: c.apiUrl, apiToken: c.apiToken, webhookSecret: c.webhookSecret,
|
|
37
40
|
resumeUrl: c.resumeUrl, store: new FilePendingStore(c.pendingDir ?? ".decidio-pending"),
|
|
41
|
+
reportDir: join(c.pendingDir ?? ".decidio-pending", "reports"),
|
|
38
42
|
agentId: c.agentId, agentKey: c.agentKey, agentDid: c.agentDid, workspaceId: c.workspaceId,
|
|
39
43
|
fetchImpl: c.fetchImpl,
|
|
40
44
|
});
|
|
41
45
|
}
|
|
42
|
-
return this.
|
|
46
|
+
return this._resume;
|
|
43
47
|
}
|
|
44
48
|
assertConfigured() {
|
|
45
49
|
if (!this.config.apiUrl)
|
|
@@ -61,14 +65,17 @@ export class Guard {
|
|
|
61
65
|
pollTimeoutMs: opts?.pollTimeoutMs, pollIntervalMs: opts?.pollIntervalMs,
|
|
62
66
|
};
|
|
63
67
|
if ((opts?.mode ?? "durable") === "blocking") {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const ceilingMs = opts?.pollTimeoutMs ?? base.pollTimeoutMs ?? 600_000;
|
|
69
|
+
console.warn(`[decidio] WARNING: blocking wait — this call gives up after ${Math.round(ceilingMs / 1000)}s ` +
|
|
70
|
+
`(pollTimeoutMs), and when it does, the decision stays OPEN in Decidio while this process stops waiting: ` +
|
|
71
|
+
`a later approval cannot resume it. Only safe for short, watched approvals — omit mode:"blocking" for ` +
|
|
72
|
+
`durable async resume on real (minutes-to-days) approvals.`);
|
|
73
|
+
return withApproval(fn, describe, base, opts?.evidence ? { evidence: opts.evidence } : undefined);
|
|
67
74
|
}
|
|
68
75
|
const resume = this.ensureResume();
|
|
69
76
|
if (opts?.action)
|
|
70
|
-
resume.register(opts.action, fn);
|
|
71
|
-
return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume });
|
|
77
|
+
resume.register(opts.action, fn, opts?.evidence);
|
|
78
|
+
return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume }, opts?.evidence ? { evidence: opts.evidence } : undefined);
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
81
|
export const guard = Guard.fromEnv();
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,12 @@ export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDeci
|
|
|
3
3
|
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome } from "./resume.js";
|
|
4
4
|
export { guard, Guard } from "./guard.js";
|
|
5
5
|
export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
6
|
-
export {
|
|
6
|
+
export { DecidioHttpError } from "./errors.js";
|
|
7
|
+
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
8
|
+
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
9
|
+
export type { JsonValue, JsonPrimitive } from "./canonical.js";
|
|
10
|
+
export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
|
|
11
|
+
export type { EvidenceOutcome, EvidenceTier } from "./evidence.js";
|
|
7
12
|
export type { RequesterIdentity } from "./signing.js";
|
|
8
13
|
export interface GuardConfig {
|
|
9
14
|
/** Base URL of the Decidio gate, e.g. "https://api.decidio.io". */
|
|
@@ -102,4 +107,10 @@ export declare class DecidioNetworkError extends Error {
|
|
|
102
107
|
/** `<appUrl>/d/<decisionId>` — the deep link that opens this decision directly, on the web app or
|
|
103
108
|
* the phone build. Null when DECIDIO_APP_URL is not configured, never a half-built URL. */
|
|
104
109
|
export declare function decisionUrl(appUrl: string | undefined, decisionId: string): string | null;
|
|
105
|
-
export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig
|
|
110
|
+
export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig, opts?: {
|
|
111
|
+
/** Map YOUR function's natural return value to the evidence Decidio records. The gate awards
|
|
112
|
+
* its strongest wrapper tier (application_confirmed) only for a committed record id plus a
|
|
113
|
+
* success indication — this lets a real API keep its own shape instead of being forced into
|
|
114
|
+
* magic field names: `evidence: (r) => ({ recordId: r.transactionId, success: r.status === "committed" })`. */
|
|
115
|
+
evidence?: (result: R) => unknown;
|
|
116
|
+
}): (...args: A) => Promise<R>;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { DecidioSuspendedError
|
|
2
|
-
import { signBoundProof, signConfirm,
|
|
1
|
+
import { DecidioSuspendedError } from "./resume.js";
|
|
2
|
+
import { signBoundProof, signConfirm, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
3
|
+
import { canonicalize, canonicalizeArgs } from "./canonical.js";
|
|
4
|
+
import { readConfirmResponse, buildConfirmBody } from "./evidence.js";
|
|
3
5
|
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
4
6
|
export { guard, Guard } from "./guard.js";
|
|
5
|
-
export {
|
|
7
|
+
export { DecidioHttpError } from "./errors.js";
|
|
8
|
+
import { DecidioHttpError } from "./errors.js";
|
|
9
|
+
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
10
|
+
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
11
|
+
export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
|
|
6
12
|
export class DecidioBlockedError extends Error {
|
|
7
13
|
reason;
|
|
8
14
|
decisionId;
|
|
@@ -74,7 +80,7 @@ async function reach(f, url, op, init) {
|
|
|
74
80
|
}
|
|
75
81
|
async function post(cfg, path, body) {
|
|
76
82
|
const f = cfg.fetchImpl ?? fetch;
|
|
77
|
-
assertSafeTransport(cfg.apiUrl,
|
|
83
|
+
assertSafeTransport(cfg.apiUrl, carriesCredential(cfg));
|
|
78
84
|
const res = await reach(f, cfg.apiUrl.replace(/\/$/, "") + path, `POST ${path}`, {
|
|
79
85
|
method: "POST",
|
|
80
86
|
headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
@@ -84,18 +90,25 @@ async function post(cfg, path, body) {
|
|
|
84
90
|
const b = await res.json().catch(() => null);
|
|
85
91
|
throw new DecidioRateLimitError(b?.limit, b?.remaining, b?.resetAt, b?.message);
|
|
86
92
|
}
|
|
87
|
-
if (!res.ok)
|
|
88
|
-
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const raw = await res.text().catch(() => "");
|
|
95
|
+
let parsed = raw;
|
|
96
|
+
try {
|
|
97
|
+
parsed = raw ? JSON.parse(raw) : undefined;
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
throw new DecidioHttpError(res.status, path, parsed);
|
|
101
|
+
}
|
|
89
102
|
return res.json();
|
|
90
103
|
}
|
|
91
104
|
async function get(cfg, path) {
|
|
92
105
|
const f = cfg.fetchImpl ?? fetch;
|
|
93
|
-
assertSafeTransport(cfg.apiUrl,
|
|
106
|
+
assertSafeTransport(cfg.apiUrl, carriesCredential(cfg));
|
|
94
107
|
const res = await reach(f, cfg.apiUrl.replace(/\/$/, "") + path, `GET ${path}`, {
|
|
95
108
|
headers: { ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
96
109
|
});
|
|
97
110
|
if (!res.ok)
|
|
98
|
-
throw new
|
|
111
|
+
throw new DecidioHttpError(res.status, path);
|
|
99
112
|
return res.json();
|
|
100
113
|
}
|
|
101
114
|
export function decisionUrl(appUrl, decisionId) {
|
|
@@ -103,27 +116,32 @@ export function decisionUrl(appUrl, decisionId) {
|
|
|
103
116
|
return null;
|
|
104
117
|
return `${appUrl.replace(/\/$/, "")}/d/${encodeURIComponent(decisionId)}`;
|
|
105
118
|
}
|
|
106
|
-
export function withApproval(fn, describe, config) {
|
|
119
|
+
export function withApproval(fn, describe, config, opts) {
|
|
107
120
|
const pollInterval = config.pollIntervalMs ?? 2_000;
|
|
108
121
|
const pollTimeout = config.pollTimeoutMs ?? 600_000;
|
|
109
122
|
const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
|
|
110
|
-
return async (...
|
|
123
|
+
return async (...rawArgs) => {
|
|
124
|
+
const args = canonicalizeArgs(rawArgs);
|
|
125
|
+
let ctx;
|
|
111
126
|
try {
|
|
112
|
-
|
|
127
|
+
ctx = describe(...args);
|
|
113
128
|
}
|
|
114
129
|
catch (e) {
|
|
115
|
-
|
|
130
|
+
if (e instanceof TypeError && /read only|not extensible|object is not extensible|frozen/i.test(e.message ?? "")) {
|
|
131
|
+
throw new TypeError(`[decidio] your describe() callback tried to MODIFY the call arguments (${e.message}). ` +
|
|
132
|
+
`Those arguments are the approved value: they are what a human sees, what the signature covers, ` +
|
|
133
|
+
`what is parked for a durable resume, and what your function ultimately runs on — so changing them ` +
|
|
134
|
+
`here would mean the action that executes is not the action that was approved. Read from the ` +
|
|
135
|
+
`arguments and RETURN the context instead: (inv) => ({ action: "pay", amount: inv.amount }). ` +
|
|
136
|
+
`If you need a modified value at execution time, compute it inside your function. ` +
|
|
137
|
+
`Nothing was routed or executed.`);
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
116
140
|
}
|
|
117
|
-
const ctx = describe(...args);
|
|
118
141
|
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
119
142
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
120
143
|
: undefined;
|
|
121
|
-
|
|
122
|
-
assertJsonSafe(ctx, "describe(...)");
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
throw new DecidioUnsafeArgumentsError(`${e?.message ?? e} The decision was NOT routed; nothing reached Decidio.`);
|
|
126
|
-
}
|
|
144
|
+
canonicalize(ctx, "describe(...)");
|
|
127
145
|
const auth = await post(config, "/agent/authorize", {
|
|
128
146
|
requester: config.agentId, action: ctx.action, amount: ctx.amount,
|
|
129
147
|
scope: ctx.scope, sourceSystem: config.sourceSystem,
|
|
@@ -133,6 +151,8 @@ export function withApproval(fn, describe, config) {
|
|
|
133
151
|
});
|
|
134
152
|
if (auth.decision !== "proceed" && auth.decision !== "route" && auth.decision !== "block")
|
|
135
153
|
throw new DecidioBlockedError(`unrecognized gate verdict: ${String(auth.decision)}`, auth.decisionId);
|
|
154
|
+
if (typeof auth.decisionId !== "string" || !auth.decisionId)
|
|
155
|
+
throw new DecidioBlockedError(`the gate answered "${auth.decision}" with no usable decisionId (${JSON.stringify(auth.decisionId ?? null)}) — refusing to execute. Every authorized action must be addressable: without an id it cannot be polled, resumed, confirmed or audited.`, auth.decisionId);
|
|
136
156
|
if (auth.decision === "block")
|
|
137
157
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
138
158
|
if (auth.decision === "route") {
|
|
@@ -145,7 +165,7 @@ export function withApproval(fn, describe, config) {
|
|
|
145
165
|
config.resume.agentId ??= config.agentId;
|
|
146
166
|
config.resume.agentKey ??= config.agentKey;
|
|
147
167
|
config.resume.agentDid ??= config.agentDid;
|
|
148
|
-
config.resume.register(ctx.action, fn);
|
|
168
|
+
config.resume.register(ctx.action, fn, opts?.evidence);
|
|
149
169
|
await config.resume.park({
|
|
150
170
|
decisionId: auth.decisionId, receiptId: auth.receiptId,
|
|
151
171
|
action: ctx.action, amount: ctx.amount, scope: ctx.scope, args,
|
|
@@ -169,17 +189,26 @@ export function withApproval(fn, describe, config) {
|
|
|
169
189
|
}
|
|
170
190
|
}
|
|
171
191
|
const result = await fn(...args);
|
|
192
|
+
let reported = null;
|
|
193
|
+
let evidenceProblem;
|
|
194
|
+
try {
|
|
195
|
+
const evidenceOf = (opts?.evidence ?? ((r) => r));
|
|
196
|
+
reported = canonicalize(evidenceOf(result) ?? null, "evidence(result)");
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
evidenceProblem = String(e?.message ?? e);
|
|
200
|
+
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
|
+
}
|
|
172
202
|
try {
|
|
173
203
|
const confirmIdentity = signConfirm(auth.decisionId, config);
|
|
174
|
-
const confirmBody = await post(config, "/agent/confirm",
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as agent_asserted — no signing key configured, so the report can't be proven to come from this agent. Set DECIDIO_AGENT_KEY/DID (npx @decidio/sdk init writes them) to earn application_confirmed.`);
|
|
204
|
+
const confirmBody = await post(config, "/agent/confirm", buildConfirmBody(config, auth.decisionId, reported, confirmIdentity));
|
|
205
|
+
const ev = readConfirmResponse(confirmBody, { signed: !!confirmIdentity, capturedResponse: reported });
|
|
206
|
+
if (!ev.reportRecorded)
|
|
207
|
+
console.warn(`[decidio] ${auth.decisionId}: ${ev.diagnostic}`);
|
|
208
|
+
else if (!ev.executionConfirmed || ev.executionReportVerified === false)
|
|
209
|
+
console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as ${ev.evidenceTier} — ${ev.diagnostic}`);
|
|
210
|
+
if (evidenceProblem)
|
|
211
|
+
console.warn(`[decidio] ${auth.decisionId}: the execution IS recorded, but with an empty captured response — see the evidence note above.`);
|
|
183
212
|
}
|
|
184
213
|
catch (e) {
|
|
185
214
|
console.warn(`[decidio] confirmation post failed (action succeeded): ${e.message}`);
|
package/dist/inngest.js
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import { verifyResumeSignature } from "./resume.js";
|
|
2
|
-
import {
|
|
2
|
+
import { gateAuthorize, gateConfirm, assertAuthorizeVerdict, verdictFrom, checkEngineVersion } from "./adapters/_gate.js";
|
|
3
3
|
export const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
|
|
4
4
|
export async function decidioGateStep(args) {
|
|
5
5
|
const { step, config, ctx } = args;
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const authHeaders = { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) };
|
|
9
|
-
const auth = await step.run("decidio-authorize", async () => {
|
|
10
|
-
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
11
|
-
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
12
|
-
: undefined;
|
|
13
|
-
const res = await f(base + "/agent/authorize", {
|
|
14
|
-
method: "POST", headers: authHeaders,
|
|
15
|
-
body: JSON.stringify({
|
|
16
|
-
requester: config.agentId, action: ctx.action, amount: ctx.amount, scope: ctx.scope,
|
|
17
|
-
sourceSystem: config.sourceSystem,
|
|
18
|
-
...(requesterIdentity ? { requesterIdentity } : {}),
|
|
19
|
-
...(config.resumeUrl ? { resumeUrl: config.resumeUrl } : {}),
|
|
20
|
-
}),
|
|
21
|
-
});
|
|
22
|
-
if (!res.ok)
|
|
23
|
-
throw new Error(`decidio authorize ${res.status}`);
|
|
24
|
-
return res.json();
|
|
25
|
-
});
|
|
6
|
+
const auth = await step.run("decidio-authorize", async () => gateAuthorize(config, ctx));
|
|
7
|
+
assertAuthorizeVerdict(auth, auth?.decisionId);
|
|
26
8
|
if (auth.decision === "block")
|
|
27
9
|
return { status: "blocked", decisionId: auth.decisionId };
|
|
28
10
|
if (auth.decision === "route") {
|
|
@@ -33,21 +15,14 @@ export async function decidioGateStep(args) {
|
|
|
33
15
|
});
|
|
34
16
|
if (!ev)
|
|
35
17
|
throw new Error(`decidio approval timed out (${auth.decisionId})`);
|
|
36
|
-
const verdict = ev.data
|
|
18
|
+
const verdict = verdictFrom(ev.data);
|
|
37
19
|
if (verdict === "rejected")
|
|
38
20
|
return { status: "rejected", decisionId: auth.decisionId };
|
|
39
21
|
if (verdict !== "approved" && verdict !== "auto_approved")
|
|
40
22
|
return { status: "blocked", decisionId: auth.decisionId };
|
|
41
23
|
}
|
|
42
24
|
const result = await step.run("agent-action", args.run);
|
|
43
|
-
await step.run("decidio-confirm", async () => {
|
|
44
|
-
const requesterIdentity = signConfirm(auth.decisionId, config);
|
|
45
|
-
await f(base + "/agent/confirm", {
|
|
46
|
-
method: "POST", headers: authHeaders,
|
|
47
|
-
body: JSON.stringify({ decisionId: auth.decisionId, capturedResponse: result ?? null, source: "wrapper", ...(requesterIdentity ? { requesterIdentity } : {}) }),
|
|
48
|
-
});
|
|
49
|
-
return true;
|
|
50
|
-
});
|
|
25
|
+
await step.run("decidio-confirm", async () => { await gateConfirm(config, auth.decisionId, result); return true; });
|
|
51
26
|
return { status: "executed", decisionId: auth.decisionId, result };
|
|
52
27
|
}
|
|
53
28
|
export function createInngestResumeBridge(opts) {
|
|
@@ -86,3 +61,4 @@ export function createInngestResumeBridge(opts) {
|
|
|
86
61
|
};
|
|
87
62
|
}
|
|
88
63
|
export { verifyResumeSignature };
|
|
64
|
+
checkEngineVersion("inngest");
|
package/dist/resume.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RequestListener } from "node:http";
|
|
2
|
+
import { type EvidenceOutcome, type EvidenceTier } from "./evidence.js";
|
|
2
3
|
/** A routed action parked on the agent side, awaiting human approval. The `args`
|
|
3
4
|
* are the original call arguments, replayed verbatim when the action resumes. */
|
|
4
5
|
export interface PendingAction {
|
|
@@ -41,6 +42,9 @@ export interface PendingStore {
|
|
|
41
42
|
claim?(decisionId: string): Promise<boolean>;
|
|
42
43
|
/** Undo a claim so a failed execution is retryable. */
|
|
43
44
|
release?(decisionId: string): Promise<void>;
|
|
45
|
+
/** Entries currently CLAIMED (excluded from list()). Needed for honest recovery reporting —
|
|
46
|
+
* a claimed entry with no journalled result is the indeterminate case. */
|
|
47
|
+
listClaimed?(): Promise<PendingAction[]>;
|
|
44
48
|
}
|
|
45
49
|
export declare class MemoryPendingStore implements PendingStore {
|
|
46
50
|
private m;
|
|
@@ -51,7 +55,13 @@ export declare class MemoryPendingStore implements PendingStore {
|
|
|
51
55
|
list(): Promise<PendingAction[]>;
|
|
52
56
|
claim(id: string): Promise<boolean>;
|
|
53
57
|
release(id: string): Promise<void>;
|
|
58
|
+
listClaimed(): Promise<PendingAction[]>;
|
|
54
59
|
}
|
|
60
|
+
/** Restrict a directory to its owner. On Windows POSIX modes are a no-op, so strip inheritance
|
|
61
|
+
* and grant only the effective user. A FAILURE IS SAID, never swallowed: these files hold the
|
|
62
|
+
* arguments of actions awaiting approval, and "silently kept the directory's inherited
|
|
63
|
+
* permissions" is exactly the outcome an operator must not discover from an auditor. */
|
|
64
|
+
export declare function hardenDir(dir: string): void;
|
|
55
65
|
/** File-backed store so a COLD process (the original requester long gone) can still
|
|
56
66
|
* resume — the proof that nothing is "kept alive". One JSON file per decision. A claim
|
|
57
67
|
* renames `<id>.json` → `<id>.claimed` atomically (same-volume rename), so two processes
|
|
@@ -69,6 +79,9 @@ export declare class FilePendingStore implements PendingStore {
|
|
|
69
79
|
list(): Promise<PendingAction[]>;
|
|
70
80
|
claim(id: string): Promise<boolean>;
|
|
71
81
|
release(id: string): Promise<void>;
|
|
82
|
+
/** Claimed entries (`.claimed` files) — invisible to list() by design, but a stranded one is
|
|
83
|
+
* exactly what recovery must surface. */
|
|
84
|
+
listClaimed(): Promise<PendingAction[]>;
|
|
72
85
|
}
|
|
73
86
|
/** Thrown by a `withApproval`-wrapped call when the action is routed and configured
|
|
74
87
|
* for async resume: the action is now SUSPENDED (parked in the store), not failed.
|
|
@@ -80,18 +93,60 @@ export declare class DecidioSuspendedError extends Error {
|
|
|
80
93
|
constructor(decisionId: string, action: string);
|
|
81
94
|
}
|
|
82
95
|
export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown";
|
|
96
|
+
/** A durable note that an action ALREADY RAN and what must still be reported. It lives OUTSIDE
|
|
97
|
+
* the pending store on purpose: a journal entry carries no handler binding, so replaying it can
|
|
98
|
+
* only ever re-send a report — it can never re-run the business action. That is what makes
|
|
99
|
+
* recovery safe on stores with and without claim(). */
|
|
100
|
+
export interface ExecutionJournalEntry {
|
|
101
|
+
decisionId: string;
|
|
102
|
+
action: string;
|
|
103
|
+
/** The canonical value that was reported — the durable FACT, journalled before the first
|
|
104
|
+
* attempt. Deliberately NOT the signed request: both proofs on a confirm are short-lived (the
|
|
105
|
+
* identity proof lives 120s, the execution report 5 minutes), so a replayed envelope is
|
|
106
|
+
* refused by the server for any outage longer than that — which is every outage worth
|
|
107
|
+
* recovering from. `reconfirm` re-signs at SEND time over these same bytes, so the report
|
|
108
|
+
* stays replay-safe (it still cannot re-run anything: no handler is journalled) while staying
|
|
109
|
+
* usable a week later. */
|
|
110
|
+
reported: unknown;
|
|
111
|
+
at: string;
|
|
112
|
+
}
|
|
113
|
+
/** The states a durable action can actually be in — every one of them nameable, including the
|
|
114
|
+
* one nobody likes. */
|
|
115
|
+
export type RecoveryState =
|
|
116
|
+
/** Parked, waiting on a human. Nothing has executed. */
|
|
117
|
+
"parked"
|
|
118
|
+
/** Claimed for execution and NOT journalled: the process died somewhere between claiming and
|
|
119
|
+
* recording the result. The action MAY OR MAY NOT have run — this cannot be resolved locally
|
|
120
|
+
* and must never be retried automatically. */
|
|
121
|
+
| "indeterminate"
|
|
122
|
+
/** Executed, result captured, report not yet accepted by Decidio. Safe to `reconfirm`. */
|
|
123
|
+
| "executed_unreported";
|
|
124
|
+
export interface RecoveryItem {
|
|
125
|
+
decisionId: string;
|
|
126
|
+
action?: string;
|
|
127
|
+
state: RecoveryState;
|
|
128
|
+
/** What a human should do about it, in words. */
|
|
129
|
+
guidance: string;
|
|
130
|
+
}
|
|
83
131
|
export interface ResumeOutcome {
|
|
84
132
|
status: ResumeStatus;
|
|
85
133
|
decisionId: string;
|
|
86
134
|
action?: string;
|
|
87
135
|
result?: unknown;
|
|
88
136
|
reason?: string;
|
|
89
|
-
/** executed only:
|
|
90
|
-
*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
137
|
+
/** executed only: Decidio ACCEPTED the execution report and named the tier it recorded.
|
|
138
|
+
* `status: "executed"` describes the LOCAL run; this describes the record. */
|
|
139
|
+
reportRecorded?: boolean;
|
|
140
|
+
/** executed only: the recorded tier attests a COMMITTED execution. Deliberately a different
|
|
141
|
+
* question — and a different name — from `reportRecorded`. */
|
|
142
|
+
executionConfirmed?: boolean;
|
|
143
|
+
/** executed only: the evidence tier Decidio recorded, verbatim. */
|
|
144
|
+
evidenceTier?: EvidenceTier;
|
|
145
|
+
/** executed only, when the gate reported on the SIGNED execution report: whether the
|
|
146
|
+
* attestation proof VERIFIED (C3, 0.1.12). */
|
|
147
|
+
executionReportVerified?: boolean;
|
|
148
|
+
/** executed only: why the evidence landed where it did, when that is worth explaining. */
|
|
149
|
+
diagnostic?: string;
|
|
95
150
|
}
|
|
96
151
|
/** Minimal config the controller needs to confirm outcomes + poll status. Shared
|
|
97
152
|
* with GuardConfig (apiUrl/apiToken). */
|
|
@@ -110,6 +165,12 @@ export interface ResumeConfig {
|
|
|
110
165
|
/** Shared secret Decidio signs the resume webhook with (HMAC-SHA256). Defaults to
|
|
111
166
|
* the API token if unset. Verified before any re-execution. */
|
|
112
167
|
webhookSecret?: string;
|
|
168
|
+
/** Where the execution journal lives (default `<pending dir>/reports`). Separate from the
|
|
169
|
+
* pending store by design: journal entries can only re-send a report, never re-run an action. */
|
|
170
|
+
reportDir?: string;
|
|
171
|
+
/** Map the action's natural return value to the evidence Decidio records — same option, same
|
|
172
|
+
* meaning as `protect`'s, so a durable resume reports exactly what the inline path would. */
|
|
173
|
+
evidence?: (result: any) => unknown;
|
|
113
174
|
/** Escape hatch to accept UNSIGNED resume webhooks (no secret). Off by default:
|
|
114
175
|
* with no secret and this unset, the webhook handler rejects every call (fail
|
|
115
176
|
* closed) so a misconfigured agent can't be driven by forged callbacks. Setting it
|
|
@@ -133,7 +194,7 @@ export interface ResumeController extends ResumeConfig {
|
|
|
133
194
|
/** Register a RAW fn under its action name so a (possibly cold) resume can replay
|
|
134
195
|
* it. `withApproval` auto-registers on the hot path; a cold worker/service should
|
|
135
196
|
* register at startup so it can resume actions it never originated. */
|
|
136
|
-
register(action: string, fn: (...args: any[]) => Promise<any
|
|
197
|
+
register(action: string, fn: (...args: any[]) => Promise<any>, evidence?: (result: any) => unknown): void;
|
|
137
198
|
/** Park a routed action WITH its integrity binding (prefer this over store.put — the
|
|
138
199
|
* binding is what lets resolve() refuse a tampered payload). */
|
|
139
200
|
park(p: Omit<PendingAction, "createdAt" | "argsSig">): Promise<void>;
|
|
@@ -158,6 +219,16 @@ export interface ResumeController extends ResumeConfig {
|
|
|
158
219
|
}): {
|
|
159
220
|
stop: () => void;
|
|
160
221
|
};
|
|
222
|
+
/** Re-send a journalled execution report, IDEMPOTENTLY. Replays the exact request built before
|
|
223
|
+
* the first attempt — it can never re-run the business action, because the journal holds no
|
|
224
|
+
* handler binding. Returns the evidence outcome; a decision with nothing journalled returns
|
|
225
|
+
* `reportRecorded: false` with guidance rather than pretending. */
|
|
226
|
+
reconfirm(decisionId: string): Promise<EvidenceOutcome>;
|
|
227
|
+
/** Every durable item that needs a human's attention, with its state named — including the one
|
|
228
|
+
* state that CANNOT be resolved automatically: `indeterminate`, where a process died between
|
|
229
|
+
* claiming an action and journalling its result, so whether the action ran is unknown locally.
|
|
230
|
+
* This never executes or retries anything; it reports. */
|
|
231
|
+
recover(): Promise<RecoveryItem[]>;
|
|
161
232
|
}
|
|
162
233
|
declare function sign(secret: string, body: string): string;
|
|
163
234
|
/** Verify Decidio's `x-decidio-signature` over the raw webhook body (HMAC-SHA256).
|
|
@@ -168,16 +239,6 @@ export declare function verifyResumeSignature(secret: string, body: string, head
|
|
|
168
239
|
* tier is a FAILED confirm; a SIGNED confirm coming back below application_confirmed is
|
|
169
240
|
* disclosed, never silent. Returns the recorded tier or throws. */
|
|
170
241
|
export declare function assertConfirmRecorded(body: any, decisionId: string, signed: boolean): string;
|
|
171
|
-
/**
|
|
172
|
-
* Build the resume controller. Spread it into your GuardConfig as `resume`, then
|
|
173
|
-
* either mount `webhookHandler()` (production) or run `worker()` (CLI/laptop).
|
|
174
|
-
*
|
|
175
|
-
* const resume = createDecidioResume({ apiUrl, apiToken, store: new FilePendingStore(".decidio-pending"), resumeUrl });
|
|
176
|
-
* const guard = { apiUrl, agentId, apiToken, resume };
|
|
177
|
-
* const createOpportunity = withApproval(createOpportunityRaw, describe, guard);
|
|
178
|
-
* // production: http.createServer(resume.webhookHandler()).listen(4100)
|
|
179
|
-
* // or CLI: resume.worker()
|
|
180
|
-
*/
|
|
181
242
|
export declare function createDecidioResume(opts: ResumeConfig & {
|
|
182
243
|
store: PendingStore;
|
|
183
244
|
}): ResumeController;
|