@decidio/sdk 0.1.11 → 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 +53 -15
- package/dist/adapters/_gate.d.ts +8 -2
- package/dist/adapters/_gate.js +30 -8
- 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 +34 -6
- package/dist/cli.js +141 -52
- package/dist/errors.d.ts +12 -5
- package/dist/errors.js +37 -5
- package/dist/evidence.d.ts +23 -0
- package/dist/evidence.js +15 -4
- package/dist/guard.d.ts +20 -3
- package/dist/guard.js +8 -6
- package/dist/index.d.ts +28 -3
- package/dist/index.js +49 -17
- package/dist/inngest.d.ts +4 -0
- package/dist/inngest.js +1 -1
- package/dist/resume.d.ts +334 -57
- package/dist/resume.js +705 -145
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +36 -2
- package/openapi.yaml +89 -6
- package/package.json +13 -8
package/dist/resume.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { RequestListener } from "node:http";
|
|
2
2
|
import { type EvidenceOutcome, type EvidenceTier } from "./evidence.js";
|
|
3
|
+
export declare function signEvidence(decisionId: string, attemptId: string | null, evidence: unknown, agentKey?: string, webhookSecret?: string): {
|
|
4
|
+
kind: "ed25519" | "hmac";
|
|
5
|
+
sig: string;
|
|
6
|
+
} | undefined;
|
|
7
|
+
export declare function verifyEvidence(rec: ExecutionRecord, agentKey?: string, webhookSecret?: string): "ok" | "mismatch" | "unbound" | "unverifiable";
|
|
3
8
|
/** A routed action parked on the agent side, awaiting human approval. The `args`
|
|
4
9
|
* are the original call arguments, replayed verbatim when the action resumes. */
|
|
5
10
|
export interface PendingAction {
|
|
@@ -29,59 +34,278 @@ export interface PendingAction {
|
|
|
29
34
|
/** Where suspended actions live between request and approval. The default is a
|
|
30
35
|
* file store; swap in Redis/Postgres/your queue for production. AGENT-SIDE only —
|
|
31
36
|
* Decidio never sees this. */
|
|
37
|
+
/** The durable execution state machine (v2).
|
|
38
|
+
*
|
|
39
|
+
* parked → claimed_preinvoke → invoking → (executed_unreported | indeterminate) → reported
|
|
40
|
+
*
|
|
41
|
+
* THE ONE RULE: nothing transitions BACK to parked once `invoking` has been written. Deletion
|
|
42
|
+
* is cleanup, never the single-use guarantee — that was the 0.1.x model, and a store whose
|
|
43
|
+
* delete() failed (or silently kept the row) let a fresh process execute an already-committed
|
|
44
|
+
* action a second time. The state IS the guarantee now; a failed delete is a tidiness problem.
|
|
45
|
+
*
|
|
46
|
+
* `indeterminate` is the honest answer to the case an SDK cannot decide: the handler was
|
|
47
|
+
* invoked and then threw. Whether the downstream write committed is not knowable from here, so
|
|
48
|
+
* the action is never automatically retried — it waits for `reconcile()`. */
|
|
49
|
+
export type ExecutionState = "parked" | "claimed_preinvoke" | "invoking" | "executed_unreported" | "indeterminate" | "reported";
|
|
50
|
+
/** Ordered weakest→strongest. Resolution ALWAYS takes the strongest state present for a
|
|
51
|
+
* decision, which is what makes a partially-failed transition safe: if a crash leaves both the
|
|
52
|
+
* old and new markers on disk, the stronger one wins and an executed action can never read
|
|
53
|
+
* back as parked. */
|
|
54
|
+
export declare const EXECUTION_STATES: readonly ExecutionState[];
|
|
55
|
+
/** States from which the handler must NEVER be invoked again automatically. */
|
|
56
|
+
export declare const NON_REPLAYABLE: readonly ExecutionState[];
|
|
57
|
+
export interface ExecutionRecord {
|
|
58
|
+
decisionId: string;
|
|
59
|
+
state: ExecutionState;
|
|
60
|
+
/** The attempt that owns (or last owned) this decision. A fencing token: a write from a
|
|
61
|
+
* stale attempt is refused. */
|
|
62
|
+
attemptId: string | null;
|
|
63
|
+
action?: string;
|
|
64
|
+
/** The canonical reported value — present from `executed_unreported` onward. This is what
|
|
65
|
+
* `reconfirm()` re-sends; it carries no handler binding, so replaying it can only ever
|
|
66
|
+
* re-send a report, never re-run the action. */
|
|
67
|
+
evidence?: unknown;
|
|
68
|
+
/** Whether `evidence` was actually recorded. Separate from `evidence` because `null` is a
|
|
69
|
+
* legitimate reported value (an action whose return could not be mapped still reports a null
|
|
70
|
+
* captured response), so presence cannot be inferred from the value. Python has carried this
|
|
71
|
+
* flag from the start; TypeScript inferring presence from `!== undefined` was a twin
|
|
72
|
+
* divergence waiting to disagree about a null-evidence decision. */
|
|
73
|
+
hasEvidence?: boolean;
|
|
74
|
+
/** Set when the evidence MAPPER failed, so `evidence` is null because nothing could be captured
|
|
75
|
+
* — not because the action returned null. The ledger could not previously tell those apart, and
|
|
76
|
+
* the difference decides whether an operator may fill the value: a null the SDK OBSERVED is a
|
|
77
|
+
* measurement, a null it failed to produce is a gap. */
|
|
78
|
+
evidenceError?: string;
|
|
79
|
+
/** Integrity binding over {decisionId, attemptId, evidence}, written when the evidence is
|
|
80
|
+
* recorded. `reconfirm()` verifies it before re-signing, so a record altered in a shared store
|
|
81
|
+
* is refused rather than attested. Absent when the agent has no key material — the same
|
|
82
|
+
* "unbound" state the parked args can be in, reported the same way. */
|
|
83
|
+
evidenceSig?: {
|
|
84
|
+
kind: "ed25519" | "hmac";
|
|
85
|
+
sig: string;
|
|
86
|
+
};
|
|
87
|
+
/** Set when this row was reconstructed by an upgrade rather than written by a live execution —
|
|
88
|
+
* currently only `"0.1.x report journal"`. Such a row predates evidence binding and can never
|
|
89
|
+
* carry a valid signature, so the strict check exempts it EXPLICITLY. The exemption must be a
|
|
90
|
+
* positive flag: exempting "anything with no signature" would hand an attacker the bypass. */
|
|
91
|
+
migratedFrom?: string;
|
|
92
|
+
/** Why the outcome is unknown — present on `indeterminate`. */
|
|
93
|
+
error?: string;
|
|
94
|
+
/** The tier Decidio recorded — present on `reported`. */
|
|
95
|
+
confirmation?: string;
|
|
96
|
+
/** Set when an OPERATOR settled this decision rather than the SDK observing it. Kept so an
|
|
97
|
+
* audit of the agent-side ledger can tell an observed execution from an asserted one. */
|
|
98
|
+
reconciled?: {
|
|
99
|
+
outcome: SettleOutcome;
|
|
100
|
+
reason?: string;
|
|
101
|
+
at: string;
|
|
102
|
+
};
|
|
103
|
+
at: string;
|
|
104
|
+
}
|
|
105
|
+
export type SettleOutcome = "committed" | "not_committed";
|
|
106
|
+
/** Refuse a reconciliation that would be a fabrication. Shared by every store so the rule cannot
|
|
107
|
+
* drift between implementations — a store that allowed `committed` from `parked` would let an
|
|
108
|
+
* operator mint an execution report for an action nothing ever invoked. */
|
|
109
|
+
/** A decision an operator SETTLED as committed is finished: its report is recorded and sent. A
|
|
110
|
+
* late write from the attempt that was still running would produce a second authority report for
|
|
111
|
+
* one decision, so it is refused here rather than in each store. `not_committed` settlements are
|
|
112
|
+
* deliberately not covered — that outcome exists so a new attempt can run and record. */
|
|
113
|
+
/** The ledger row is owned by a NEWER attempt. Typed for the same reason `DecidioAlreadySettled`
|
|
114
|
+
* is: `reportAfterExecution` must keep going when the ledger fails, so a bare Error here read as
|
|
115
|
+
* "the disk write failed" and the stale attempt went on to POST — a second authority report for
|
|
116
|
+
* one decision. Reachable through the SDK's own documented flow: `reconcile(id, not_committed)`
|
|
117
|
+
* releases a decision whose original attempt is still alive (recover() invites exactly that, since
|
|
118
|
+
* it says the process "died" when it may not have), the release is legitimate and the new attempt
|
|
119
|
+
* SHOULD run — but the old attempt's report must not be sent. */
|
|
120
|
+
export declare class DecidioStaleAttemptError extends Error {
|
|
121
|
+
decisionId: string;
|
|
122
|
+
owner: string | null;
|
|
123
|
+
state: ExecutionState;
|
|
124
|
+
constructor(decisionId: string, owner: string | null, state: ExecutionState);
|
|
125
|
+
}
|
|
126
|
+
export declare class DecidioAlreadySettledError extends Error {
|
|
127
|
+
decisionId: string;
|
|
128
|
+
constructor(decisionId: string);
|
|
129
|
+
}
|
|
130
|
+
export declare const SETTLE_OUTCOMES: readonly SettleOutcome[];
|
|
131
|
+
export declare function assertSettlable(id: string, state: ExecutionState, outcome: SettleOutcome, hasObservedEvidence?: boolean): void;
|
|
132
|
+
export interface ClaimResult {
|
|
133
|
+
/** True only when THIS attempt may proceed to invoke. */
|
|
134
|
+
ok: boolean;
|
|
135
|
+
/** The state that refused the claim, when ok is false — so a caller can tell "another worker
|
|
136
|
+
* has it" from "this already executed and must be reconciled". */
|
|
137
|
+
state?: ExecutionState;
|
|
138
|
+
reason?: string;
|
|
139
|
+
}
|
|
140
|
+
/** Agent-side durable state. The shipped stores are file- and memory-backed; swap in
|
|
141
|
+
* Redis/Postgres/your queue for production. AGENT-SIDE only — Decidio never sees this.
|
|
142
|
+
*
|
|
143
|
+
* v2 (0.2.0) makes the state machine MANDATORY. The 0.1.x contract left `claim()` optional and
|
|
144
|
+
* fell back to a process-local set, which cannot survive a restart and therefore could not
|
|
145
|
+
* carry a single-execution guarantee across processes at all. Every method below must be
|
|
146
|
+
* atomic against concurrent callers — compare-and-swap, a transaction, or an equivalent
|
|
147
|
+
* fencing token. */
|
|
32
148
|
export interface PendingStore {
|
|
33
149
|
put(p: PendingAction): Promise<void>;
|
|
34
150
|
get(decisionId: string): Promise<PendingAction | null>;
|
|
151
|
+
/** CLEANUP ONLY. Removing a row must never be what prevents a second execution. */
|
|
35
152
|
delete(decisionId: string): Promise<void>;
|
|
153
|
+
/** Parked entries only — anything claimed or beyond is excluded. */
|
|
36
154
|
list(): Promise<PendingAction[]>;
|
|
37
|
-
/** Atomically take ownership
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
155
|
+
/** Atomically take ownership for THIS attempt. Must refuse when the decision is already
|
|
156
|
+
* claimed by a live attempt, or is in any NON_REPLAYABLE state. */
|
|
157
|
+
claim(decisionId: string, attemptId: string): Promise<ClaimResult>;
|
|
158
|
+
/** The handler is about to be called. After this point the store must never report the
|
|
159
|
+
* decision as executable again, whatever else fails. */
|
|
160
|
+
markInvoking(decisionId: string, attemptId: string): Promise<void>;
|
|
161
|
+
/** The handler returned. `evidence` is the canonical reported value. */
|
|
162
|
+
recordExecuted(decisionId: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
|
|
163
|
+
/** The handler was invoked and threw: the downstream outcome is unknown. */
|
|
164
|
+
markIndeterminate(decisionId: string, attemptId: string, error: string): Promise<void>;
|
|
165
|
+
/** Decidio accepted the execution report. */
|
|
166
|
+
markReported(decisionId: string, attemptId: string, confirmation: string): Promise<void>;
|
|
167
|
+
/** The authoritative state for one decision, or null if this store has never seen it. */
|
|
168
|
+
getExecution(decisionId: string): Promise<ExecutionRecord | null>;
|
|
169
|
+
/** Every execution record the store holds — the basis for honest recovery reporting. */
|
|
170
|
+
listExecutions(): Promise<ExecutionRecord[]>;
|
|
171
|
+
/** Operator reconciliation — the only legal backwards transition, and the only exit from
|
|
172
|
+
* `indeterminate`. Never called by any automatic path.
|
|
173
|
+
*
|
|
174
|
+
* A CUSTOM STORE MUST RAISE THE TYPED ERRORS at its own fences: `DecidioStaleAttemptError` when
|
|
175
|
+
* a superseded attempt writes, and `DecidioAlreadySettledError` when a second execution report
|
|
176
|
+
* lands on a decision an operator settled as committed. The reporting stage deliberately keeps
|
|
177
|
+
* going when the ledger fails — that is what stops an evidence gap from costing an execution —
|
|
178
|
+
* so a store that raises a generic error for either case sends a duplicate authority report
|
|
179
|
+
* instead of stopping. */
|
|
180
|
+
settle(decisionId: string, outcome: SettleOutcome, opts?: {
|
|
181
|
+
evidence?: unknown;
|
|
182
|
+
reason?: string;
|
|
183
|
+
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
184
|
+
}): Promise<void>;
|
|
185
|
+
/** Release a claim taken BEFORE invocation, so a pre-invoke failure stays retryable. It is a
|
|
186
|
+
* contract error to call this once `markInvoking` has been written, and a conforming store
|
|
187
|
+
* must refuse it. */
|
|
188
|
+
releasePreinvoke(decisionId: string, attemptId: string): Promise<void>;
|
|
48
189
|
}
|
|
190
|
+
/** In-memory store. Single-process only BY CONSTRUCTION — it is the right choice for tests and
|
|
191
|
+
* for a worker that owns its queue, and the wrong one for two processes sharing work, which is
|
|
192
|
+
* exactly what `FilePendingStore` (atomic renames on one volume) or a real database is for. */
|
|
49
193
|
export declare class MemoryPendingStore implements PendingStore {
|
|
50
194
|
private m;
|
|
51
|
-
private
|
|
195
|
+
private x;
|
|
196
|
+
private rec;
|
|
197
|
+
/** Advance only. A write from a stale attempt, or one that would move the state BACKWARDS,
|
|
198
|
+
* is refused — the in-memory expression of the same rule the file store gets from
|
|
199
|
+
* strongest-wins resolution. */
|
|
200
|
+
private advance;
|
|
52
201
|
put(p: PendingAction): Promise<void>;
|
|
53
202
|
get(id: string): Promise<PendingAction | null>;
|
|
54
203
|
delete(id: string): Promise<void>;
|
|
55
204
|
list(): Promise<PendingAction[]>;
|
|
56
|
-
claim(id: string): Promise<
|
|
57
|
-
|
|
58
|
-
|
|
205
|
+
claim(id: string, attemptId: string): Promise<ClaimResult>;
|
|
206
|
+
markInvoking(id: string, attemptId: string): Promise<void>;
|
|
207
|
+
recordExecuted(id: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
|
|
208
|
+
markIndeterminate(id: string, attemptId: string, error: string): Promise<void>;
|
|
209
|
+
markReported(id: string, attemptId: string, confirmation: string): Promise<void>;
|
|
210
|
+
getExecution(id: string): Promise<ExecutionRecord | null>;
|
|
211
|
+
listExecutions(): Promise<ExecutionRecord[]>;
|
|
212
|
+
/** OPERATOR RECONCILIATION — the ONE legal backwards transition, and the only exit from
|
|
213
|
+
* `indeterminate` or a stalled claim. It is legal only because a human asserted the downstream
|
|
214
|
+
* truth this machine cannot observe, so no automatic path may call it.
|
|
215
|
+
*
|
|
216
|
+
* `committed` → `executed_unreported` carrying the operator's evidence, so the ordinary
|
|
217
|
+
* reporting path can send it. The handler is NEVER invoked.
|
|
218
|
+
* `not_committed` → back to `parked`, permitting exactly one more ordinary attempt. */
|
|
219
|
+
settle(id: string, outcome: SettleOutcome, opts?: {
|
|
220
|
+
evidence?: unknown;
|
|
221
|
+
reason?: string;
|
|
222
|
+
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
223
|
+
}): Promise<void>;
|
|
224
|
+
releasePreinvoke(id: string, attemptId: string): Promise<void>;
|
|
59
225
|
}
|
|
60
226
|
/** Restrict a directory to its owner. On Windows POSIX modes are a no-op, so strip inheritance
|
|
61
227
|
* and grant only the effective user. A FAILURE IS SAID, never swallowed: these files hold the
|
|
62
228
|
* arguments of actions awaiting approval, and "silently kept the directory's inherited
|
|
63
229
|
* permissions" is exactly the outcome an operator must not discover from an auditor. */
|
|
64
230
|
export declare function hardenDir(dir: string): void;
|
|
65
|
-
/** File-backed store so a COLD process (the original requester long gone) can still
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
231
|
+
/** File-backed store so a COLD process (the original requester long gone) can still resume — the
|
|
232
|
+
* proof that nothing is "kept alive". One JSON file per decision holds the parked payload, and
|
|
233
|
+
* one marker file per execution state holds the ledger.
|
|
234
|
+
*
|
|
235
|
+
* A claim is `link()`, not `rename()`: link fails when the target exists, so two processes racing
|
|
236
|
+
* for the same decision produce one winner and one clean refusal, while rename would silently
|
|
237
|
+
* overwrite and let both proceed. (0.1.x renamed `<id>.json` → `<id>.claimed`, and a stale
|
|
238
|
+
* `.claimed` from a crash then needed a TTL nobody had written — that gap is closed by the
|
|
239
|
+
* `claimed_stalled` state and `reconcile()`, which release it explicitly rather than on a timer.
|
|
240
|
+
* A timer would have to guess whether the handler ran.) */
|
|
70
241
|
export declare class FilePendingStore implements PendingStore {
|
|
71
242
|
private dir;
|
|
72
243
|
constructor(dir: string);
|
|
73
244
|
private key;
|
|
74
245
|
private path;
|
|
75
|
-
|
|
246
|
+
/** One marker file per state. The FILENAME carries the state so a scan never has to parse,
|
|
247
|
+
* and the content carries the record. */
|
|
248
|
+
private statePath;
|
|
249
|
+
/** Durable write: temp file in the SAME directory → fsync → atomic rename. A crash can then
|
|
250
|
+
* only leave the old complete file or the new complete file, never a truncated one. The
|
|
251
|
+
* 0.1.x journal wrote straight to its final path with O_TRUNC, so a crash mid-write
|
|
252
|
+
* destroyed the record that proved an action had already run. */
|
|
253
|
+
private writeAtomic;
|
|
254
|
+
/** The authoritative record: the STRONGEST state present on disk. A crash between "write the
|
|
255
|
+
* new marker" and "remove the old one" leaves two markers; taking the strongest means an
|
|
256
|
+
* executed action can never read back as parked, so a partial transition is safe rather than
|
|
257
|
+
* merely unlikely. Corruption is reported, never silently treated as absent. */
|
|
258
|
+
private resolve;
|
|
259
|
+
private setState;
|
|
76
260
|
put(p: PendingAction): Promise<void>;
|
|
77
261
|
get(id: string): Promise<PendingAction | null>;
|
|
262
|
+
/** CLEANUP ONLY — the payload goes, the execution record STAYS. That is the whole point of
|
|
263
|
+
* the v2 model: a failed or skipped delete can no longer make an executed action runnable. */
|
|
78
264
|
delete(id: string): Promise<void>;
|
|
79
265
|
list(): Promise<PendingAction[]>;
|
|
80
|
-
claim(id: string): Promise<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
266
|
+
claim(id: string, attemptId: string): Promise<ClaimResult>;
|
|
267
|
+
markInvoking(id: string, attemptId: string): Promise<void>;
|
|
268
|
+
recordExecuted(id: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
|
|
269
|
+
markIndeterminate(id: string, attemptId: string, error: string): Promise<void>;
|
|
270
|
+
markReported(id: string, attemptId: string, confirmation: string): Promise<void>;
|
|
271
|
+
getExecution(id: string): Promise<ExecutionRecord | null>;
|
|
272
|
+
listExecutions(): Promise<ExecutionRecord[]>;
|
|
273
|
+
/** OPERATOR RECONCILIATION — see MemoryPendingStore.settle. Writes the new marker first and
|
|
274
|
+
* then clears every other state file for this decision, including the STRONGER ones: this is
|
|
275
|
+
* the single place where a state legitimately moves backwards, so leaving a stronger marker
|
|
276
|
+
* behind would let resolve()'s strongest-wins rule undo the operator's decision. */
|
|
277
|
+
settle(id: string, outcome: SettleOutcome, opts?: {
|
|
278
|
+
evidence?: unknown;
|
|
279
|
+
reason?: string;
|
|
280
|
+
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
281
|
+
}): Promise<void>;
|
|
282
|
+
releasePreinvoke(id: string, attemptId: string): Promise<void>;
|
|
283
|
+
/** ONE-WAY MIGRATION from the 0.1.x layout, run once at construction. `<id>.json` → parked;
|
|
284
|
+
* `<id>.claimed` → INDETERMINATE, never parked: a claimed 0.1.x entry may have been mid-flight
|
|
285
|
+
* when the process died, and assuming it did not run is the assumption that duplicates a
|
|
286
|
+
* payment. Nothing ambiguous is made executable by an upgrade. */
|
|
287
|
+
migrateLegacy(): {
|
|
288
|
+
migrated: number;
|
|
289
|
+
indeterminate: number;
|
|
290
|
+
reports: number;
|
|
291
|
+
};
|
|
292
|
+
/** 0.1.x wrote execution reports to `<pendingDir>/reports/<id>.json` — evidence that an action
|
|
293
|
+
* ALREADY RAN and still owes Decidio a report. 0.2.0 keeps that in the ledger, so without this
|
|
294
|
+
* those files become invisible: `recover()` returns nothing and `reconfirm()` answers "nothing
|
|
295
|
+
* is recorded for X … or this process never executed it" about an action that demonstrably
|
|
296
|
+
* did. That is the SDK losing a durable authority fact on upgrade, so the reports are pulled
|
|
297
|
+
* into `executed_unreported` rows where reconfirm() can actually re-send them.
|
|
298
|
+
*
|
|
299
|
+
* The file is CONSUMED (renamed to `.migrated`) rather than deleted: if anything here is
|
|
300
|
+
* wrong, the original bytes are still on disk to inspect. */
|
|
301
|
+
private migrateLegacyReports;
|
|
302
|
+
}
|
|
303
|
+
/** The state on disk could not be read. Never silently treated as "nothing here": an
|
|
304
|
+
* unreadable record is the one that proves an action already ran. */
|
|
305
|
+
export declare class DecidioStateCorruptError extends Error {
|
|
306
|
+
decisionId: string;
|
|
307
|
+
details: string[];
|
|
308
|
+
constructor(decisionId: string, details: string[]);
|
|
85
309
|
}
|
|
86
310
|
/** Thrown by a `withApproval`-wrapped call when the action is routed and configured
|
|
87
311
|
* for async resume: the action is now SUSPENDED (parked in the store), not failed.
|
|
@@ -92,29 +316,21 @@ export declare class DecidioSuspendedError extends Error {
|
|
|
92
316
|
action: string;
|
|
93
317
|
constructor(decisionId: string, action: string);
|
|
94
318
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
|
|
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
|
-
}
|
|
319
|
+
/** `indeterminate` (0.2.0): the handler was INVOKED and then threw. Whether the downstream
|
|
320
|
+
* effect committed is not knowable from here, so the action is never retried automatically —
|
|
321
|
+
* it waits for reconcile(). Callers that switch exhaustively on this union must handle it. */
|
|
322
|
+
export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown" | "indeterminate";
|
|
113
323
|
/** The states a durable action can actually be in — every one of them nameable, including the
|
|
114
324
|
* one nobody likes. */
|
|
115
325
|
export type RecoveryState =
|
|
116
326
|
/** Parked, waiting on a human. Nothing has executed. */
|
|
117
327
|
"parked"
|
|
328
|
+
/** Claimed by an attempt that never reached the invocation boundary, so the handler PROVABLY
|
|
329
|
+
* did not run — but the row is not parked either: `list()` excludes it and every ordinary
|
|
330
|
+
* signal answers `unknown`, so nothing will pick it up on its own. It is stalled until an
|
|
331
|
+
* operator releases it. Reporting this as "parked" (as 0.2.0's first draft did) told a UI an
|
|
332
|
+
* approved action was still queued to resume when in fact it had been silently dropped. */
|
|
333
|
+
| "claimed_stalled"
|
|
118
334
|
/** Claimed for execution and NOT journalled: the process died somewhere between claiming and
|
|
119
335
|
* recording the result. The action MAY OR MAY NOT have run — this cannot be resolved locally
|
|
120
336
|
* and must never be retried automatically. */
|
|
@@ -128,6 +344,28 @@ export interface RecoveryItem {
|
|
|
128
344
|
/** What a human should do about it, in words. */
|
|
129
345
|
guidance: string;
|
|
130
346
|
}
|
|
347
|
+
/** What the SDK knows about THIS execution, handed to a handler that opts in with
|
|
348
|
+
* `withContext: true`.
|
|
349
|
+
*
|
|
350
|
+
* The point is `idempotencyKey`. This SDK can guarantee it will not invoke your handler twice;
|
|
351
|
+
* it cannot guarantee your downstream API only commits once, because that is the vendor's
|
|
352
|
+
* property, not ours. Passing this key to Stripe/Salesforce/your own service is what closes the
|
|
353
|
+
* remaining window — and until now the handler had no way to reach the decision id at all, so
|
|
354
|
+
* the one thing that could close it was the one thing a caller could not get.
|
|
355
|
+
*
|
|
356
|
+
* Opt-in, never inferred from arity. A signature check would have to guess whether a bound
|
|
357
|
+
* method, a *args function or a decorated tool wanted the context, and guessing wrong means
|
|
358
|
+
* passing a context object as someone's first business argument — in a library whose whole job
|
|
359
|
+
* is not doing surprising things to a payment call. */
|
|
360
|
+
export interface ExecutionContext {
|
|
361
|
+
decisionId: string;
|
|
362
|
+
/** The attempt that owns this execution. Null on the inline path, where there is no durable
|
|
363
|
+
* attempt — the value is honest about which door you came through. */
|
|
364
|
+
attemptId: string | null;
|
|
365
|
+
/** Defaults to `decisionId`. Stable across every attempt at the same decision, which is exactly
|
|
366
|
+
* what a downstream idempotency key must be. */
|
|
367
|
+
idempotencyKey: string;
|
|
368
|
+
}
|
|
131
369
|
export interface ResumeOutcome {
|
|
132
370
|
status: ResumeStatus;
|
|
133
371
|
decisionId: string;
|
|
@@ -142,8 +380,19 @@ export interface ResumeOutcome {
|
|
|
142
380
|
executionConfirmed?: boolean;
|
|
143
381
|
/** executed only: the evidence tier Decidio recorded, verbatim. */
|
|
144
382
|
evidenceTier?: EvidenceTier;
|
|
383
|
+
/** executed only, when the gate reported on the SIGNED execution report: whether the
|
|
384
|
+
* attestation proof VERIFIED (C3, 0.1.12). */
|
|
385
|
+
executionReportVerified?: boolean;
|
|
145
386
|
/** executed only: why the evidence landed where it did, when that is worth explaining. */
|
|
146
387
|
diagnostic?: string;
|
|
388
|
+
/** The four fields the INLINE door returns. The durable door computed the same outcome and kept
|
|
389
|
+
* three of them for its own prose, so the same question got a field on one path and a sentence
|
|
390
|
+
* on the other — and `reportJournalled` was worst, because the durable path is the one where
|
|
391
|
+
* `reconfirm()` genuinely CAN help and it never said so. */
|
|
392
|
+
executionReportReason?: string;
|
|
393
|
+
confirmationRetryable?: boolean;
|
|
394
|
+
reportJournalled?: boolean;
|
|
395
|
+
evidenceError?: string;
|
|
147
396
|
}
|
|
148
397
|
/** Minimal config the controller needs to confirm outcomes + poll status. Shared
|
|
149
398
|
* with GuardConfig (apiUrl/apiToken). */
|
|
@@ -162,8 +411,12 @@ export interface ResumeConfig {
|
|
|
162
411
|
/** Shared secret Decidio signs the resume webhook with (HMAC-SHA256). Defaults to
|
|
163
412
|
* the API token if unset. Verified before any re-execution. */
|
|
164
413
|
webhookSecret?: string;
|
|
165
|
-
/**
|
|
166
|
-
*
|
|
414
|
+
/** @deprecated Ignored since 0.2.0 and accepted only so existing callers keep compiling.
|
|
415
|
+
* Execution evidence now lives in the PendingStore's own execution record, beside the state
|
|
416
|
+
* that makes a decision non-replayable. 0.1.x kept it in a separate local directory, so two
|
|
417
|
+
* workers sharing one Redis/Postgres store still held two disjoint journals and neither could
|
|
418
|
+
* reconfirm the other's work — and a store-recorded execution could read back as "nothing has
|
|
419
|
+
* executed" if the local file was lost. One store, one truth. */
|
|
167
420
|
reportDir?: string;
|
|
168
421
|
/** Map the action's natural return value to the evidence Decidio records — same option, same
|
|
169
422
|
* meaning as `protect`'s, so a durable resume reports exactly what the inline path would. */
|
|
@@ -179,8 +432,11 @@ export interface ResumeConfig {
|
|
|
179
432
|
/** The callback URL Decidio should POST the verdict to (webhook transport). Sent
|
|
180
433
|
* on /agent/authorize so the decision carries it; omit when using the poll worker. */
|
|
181
434
|
resumeUrl?: string;
|
|
182
|
-
/** Fired after every terminal resolution (executed / rejected / blocked /
|
|
183
|
-
* via either transport. Use it to log, close a listener, or exit a CLI.
|
|
435
|
+
/** Fired after every terminal resolution (executed / indeterminate / rejected / blocked /
|
|
436
|
+
* no_handler), via either transport. Use it to log, close a listener, or exit a CLI.
|
|
437
|
+
* `indeterminate` is new in 0.2.0 and DOES fire here: a handler that ran and then threw is a
|
|
438
|
+
* terminal outcome for automation — nothing will retry it — so a listener that only handled
|
|
439
|
+
* the old four would silently miss the one case that needs a human. */
|
|
184
440
|
onResolved?: (o: ResumeOutcome) => void;
|
|
185
441
|
fetchImpl?: typeof fetch;
|
|
186
442
|
}
|
|
@@ -191,7 +447,7 @@ export interface ResumeController extends ResumeConfig {
|
|
|
191
447
|
/** Register a RAW fn under its action name so a (possibly cold) resume can replay
|
|
192
448
|
* it. `withApproval` auto-registers on the hot path; a cold worker/service should
|
|
193
449
|
* register at startup so it can resume actions it never originated. */
|
|
194
|
-
register(action: string, fn: (...args: any[]) => Promise<any
|
|
450
|
+
register(action: string, fn: (...args: any[]) => Promise<any>, evidence?: (result: any) => unknown, withContext?: boolean): void;
|
|
195
451
|
/** Park a routed action WITH its integrity binding (prefer this over store.put — the
|
|
196
452
|
* binding is what lets resolve() refuse a tampered payload). */
|
|
197
453
|
park(p: Omit<PendingAction, "createdAt" | "argsSig">): Promise<void>;
|
|
@@ -216,11 +472,27 @@ export interface ResumeController extends ResumeConfig {
|
|
|
216
472
|
}): {
|
|
217
473
|
stop: () => void;
|
|
218
474
|
};
|
|
219
|
-
/** Re-send a
|
|
220
|
-
* the
|
|
221
|
-
* handler binding. Returns the evidence outcome; a decision with nothing
|
|
475
|
+
/** Re-send a recorded execution report, IDEMPOTENTLY. Re-signs at send time over the value in
|
|
476
|
+
* the store's execution ledger — it can never re-run the business action, because the ledger
|
|
477
|
+
* holds no handler binding. Returns the evidence outcome; a decision with nothing recorded returns
|
|
222
478
|
* `reportRecorded: false` with guidance rather than pretending. */
|
|
223
|
-
|
|
479
|
+
/** `acceptUnbound` is an OPERATOR override for a record whose integrity binding is missing or
|
|
480
|
+
* unverifiable — the case a 0.1.x upgrade legitimately produces. It is a call-site argument on
|
|
481
|
+
* purpose: any flag stored in the record itself would be writable by whoever planted the
|
|
482
|
+
* record. */
|
|
483
|
+
reconfirm(decisionId: string, opts?: {
|
|
484
|
+
acceptUnbound?: boolean;
|
|
485
|
+
}): Promise<EvidenceOutcome>;
|
|
486
|
+
/** Settle a decision an operator has checked downstream — the ONLY exit from `indeterminate`
|
|
487
|
+
* and from a claim that stalled before invocation, and the only backwards transition in the
|
|
488
|
+
* machine. `committed` records and reports the evidence you supply WITHOUT invoking the
|
|
489
|
+
* handler; `not_committed` parks the decision so exactly one more ordinary attempt can run.
|
|
490
|
+
* Never called automatically: the whole point is that a human looked. */
|
|
491
|
+
reconcile(decisionId: string, opts: {
|
|
492
|
+
outcome: SettleOutcome;
|
|
493
|
+
evidence?: unknown;
|
|
494
|
+
reason?: string;
|
|
495
|
+
}): Promise<EvidenceOutcome>;
|
|
224
496
|
/** Every durable item that needs a human's attention, with its state named — including the one
|
|
225
497
|
* state that CANNOT be resolved automatically: `indeterminate`, where a process died between
|
|
226
498
|
* claiming an action and journalling its result, so whether the action ran is unknown locally.
|
|
@@ -231,10 +503,15 @@ declare function sign(secret: string, body: string): string;
|
|
|
231
503
|
/** Verify Decidio's `x-decidio-signature` over the raw webhook body (HMAC-SHA256).
|
|
232
504
|
* Exported so engine adapters (e.g. the Inngest bridge) reuse the exact check. */
|
|
233
505
|
export declare function verifyResumeSignature(secret: string, body: string, header: string | undefined): boolean;
|
|
234
|
-
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
506
|
+
/** Throwing wrapper over the ONE confirm-body rule, for a caller building their own confirm door.
|
|
507
|
+
* Returns the recorded tier or throws.
|
|
508
|
+
*
|
|
509
|
+
* It now DELEGATES to `readConfirmResponse` rather than re-implementing the check, because the
|
|
510
|
+
* two had drifted into opposite answers: this function accepted `confirmation: "anything"` and
|
|
511
|
+
* handed it back as a recorded tier, while the parser every real door uses treats an
|
|
512
|
+
* unrecognised tier as NOT evidence (closed enum, fail-closed — an answer we do not understand
|
|
513
|
+
* is never evidence). Its own comment claimed it was "shared by every confirm door", and no door
|
|
514
|
+
* called it: the claim was what kept anyone from noticing the disagreement. */
|
|
238
515
|
export declare function assertConfirmRecorded(body: any, decisionId: string, signed: boolean): string;
|
|
239
516
|
export declare function createDecidioResume(opts: ResumeConfig & {
|
|
240
517
|
store: PendingStore;
|