@decidio/sdk 0.2.1 → 0.3.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 +128 -13
- package/dist/adapters/langgraph.d.ts +2 -2
- package/dist/adapters/langgraph.js +2 -2
- package/dist/adapters/openai.d.ts +14 -4
- package/dist/adapters/openai.js +3 -2
- package/dist/adapters/temporal.d.ts +2 -2
- package/dist/adapters/temporal.js +7 -2
- package/dist/cli.js +36 -19
- package/dist/errors.js +12 -4
- package/dist/evidence.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/inngest.d.ts +10 -2
- package/dist/inngest.js +2 -2
- package/dist/resume.d.ts +118 -7
- package/dist/resume.js +192 -68
- package/dist/signing.d.ts +8 -1
- package/dist/signing.js +7 -23
- package/openapi.yaml +38 -0
- package/package.json +2 -7
package/dist/resume.d.ts
CHANGED
|
@@ -46,13 +46,32 @@ export interface PendingAction {
|
|
|
46
46
|
* `indeterminate` is the honest answer to the case an SDK cannot decide: the handler was
|
|
47
47
|
* invoked and then threw. Whether the downstream write committed is not knowable from here, so
|
|
48
48
|
* the action is never automatically retried — it waits for `reconcile()`. */
|
|
49
|
-
export type ExecutionState = "parked" | "claimed_preinvoke"
|
|
49
|
+
export type ExecutionState = "parked" | "claimed_preinvoke"
|
|
50
|
+
/** A human or policy said NO. Terminal: the handler must never run for this decision. */
|
|
51
|
+
| "discarded" | "invoking" | "executed_unreported" | "indeterminate" | "reported";
|
|
50
52
|
/** Ordered weakest→strongest. Resolution ALWAYS takes the strongest state present for a
|
|
51
53
|
* decision, which is what makes a partially-failed transition safe: if a crash leaves both the
|
|
52
54
|
* old and new markers on disk, the stronger one wins and an executed action can never read
|
|
53
55
|
* back as parked. */
|
|
56
|
+
/** `discarded` sits ABOVE the pre-invocation states and BELOW the execution ones, and the position
|
|
57
|
+
* is load-bearing in both directions:
|
|
58
|
+
*
|
|
59
|
+
* - above `parked`/`claimed_preinvoke`, so a denial beats a payload whose deletion FAILED. That is
|
|
60
|
+
* the whole F-001 fix: the old code deleted the payload and wrote no state, the shipped file
|
|
61
|
+
* store swallows unlink errors, and the ledger therefore still read `parked` — so a later,
|
|
62
|
+
* out-of-order `approved` signal executed an action a human had explicitly refused.
|
|
63
|
+
* - below `invoking`…`reported` — but rank is NOT how the two are reconciled, and relying on it
|
|
64
|
+
* was wrong in both directions. A stronger marker written beside a tombstone outranked the
|
|
65
|
+
* denial AND was then deleted by setState's weaker-marker sweep, so a refused decision read back
|
|
66
|
+
* as runnable. `resolve()` therefore prefers the DENIAL whenever its marker is present, at any
|
|
67
|
+
* rank. That decides authority, not truth: if an execution marker is there too, something DID
|
|
68
|
+
* run, and swallowing that is the opposite failure — so resolve() flags the conflict on the
|
|
69
|
+
* record (`executionDespiteDenial`), recover() reports it, and reconfirm() says so instead of
|
|
70
|
+
* "this process never executed it". A denial governs the decision; it never edits history. */
|
|
54
71
|
export declare const EXECUTION_STATES: readonly ExecutionState[];
|
|
55
|
-
/** States from which the handler must NEVER be invoked again automatically.
|
|
72
|
+
/** States from which the handler must NEVER be invoked again automatically. `discarded` is here
|
|
73
|
+
* FOREVER — `claim()` gates on this list, so a denied decision can never be claimed again, by this
|
|
74
|
+
* process or any future one. */
|
|
56
75
|
export declare const NON_REPLAYABLE: readonly ExecutionState[];
|
|
57
76
|
export interface ExecutionRecord {
|
|
58
77
|
decisionId: string;
|
|
@@ -84,6 +103,11 @@ export interface ExecutionRecord {
|
|
|
84
103
|
kind: "ed25519" | "hmac";
|
|
85
104
|
sig: string;
|
|
86
105
|
};
|
|
106
|
+
/** WHY this decision is `discarded` — the verdict a human or policy returned, and any reason
|
|
107
|
+
* text. Recorded on the tombstone so recovery can say "a human rejected this" rather than
|
|
108
|
+
* reporting a bare terminal state an operator would have to go and look up. */
|
|
109
|
+
verdict?: "rejected" | "blocked";
|
|
110
|
+
discardReason?: string;
|
|
87
111
|
/** Set when this row was reconstructed by an upgrade rather than written by a live execution —
|
|
88
112
|
* currently only `"0.1.x report journal"`. Such a row predates evidence binding and can never
|
|
89
113
|
* carry a valid signature, so the strict check exempts it EXPLICITLY. The exemption must be a
|
|
@@ -95,10 +119,25 @@ export interface ExecutionRecord {
|
|
|
95
119
|
confirmation?: string;
|
|
96
120
|
/** Set when an OPERATOR settled this decision rather than the SDK observing it. Kept so an
|
|
97
121
|
* audit of the agent-side ledger can tell an observed execution from an asserted one. */
|
|
122
|
+
/** `workerStopped` records that a HUMAN asserted the claiming worker was stopped before this
|
|
123
|
+
* decision was released — the assertion that made the release legal. It is stamped here because
|
|
124
|
+
* the whole point of requiring it is to put a name against that judgement: without it the ledger
|
|
125
|
+
* showed a decision released from `invoking` with no trace that anyone had claimed to check. */
|
|
98
126
|
reconciled?: {
|
|
99
127
|
outcome: SettleOutcome;
|
|
100
128
|
reason?: string;
|
|
101
129
|
at: string;
|
|
130
|
+
workerStopped?: boolean;
|
|
131
|
+
};
|
|
132
|
+
/** DERIVED AT READ TIME, never persisted: set when a DENIED decision also carries an execution
|
|
133
|
+
* marker. That pair is the single most alarming state the machine can hold — a human refused it
|
|
134
|
+
* and something ran anyway — so it must never resolve to silence. Only the file store can hold
|
|
135
|
+
* it (the memory store keeps one record per decision); a crash between writing a marker and
|
|
136
|
+
* sweeping the weaker ones, or a 0.1.x report migrated beside a tombstone, both produce it. */
|
|
137
|
+
executionDespiteDenial?: {
|
|
138
|
+
state: ExecutionState;
|
|
139
|
+
at: string;
|
|
140
|
+
hasEvidence: boolean;
|
|
102
141
|
};
|
|
103
142
|
at: string;
|
|
104
143
|
}
|
|
@@ -127,8 +166,38 @@ export declare class DecidioAlreadySettledError extends Error {
|
|
|
127
166
|
decisionId: string;
|
|
128
167
|
constructor(decisionId: string);
|
|
129
168
|
}
|
|
169
|
+
/** A DENIAL IS TERMINAL, and that has to be enforced where the state is WRITTEN — not only where
|
|
170
|
+
* it is reconciled. Refusing `settle` from `discarded` closed the operator's door and left three
|
|
171
|
+
* others open, each of which walked a refused action all the way to execution:
|
|
172
|
+
* - `discarded` sits mid-rank in EXECUTION_STATES, so the advance-only rule happily accepted
|
|
173
|
+
* `discarded → invoking → executed_unreported`;
|
|
174
|
+
* - `markDiscarded` leaves the record UNOWNED (`attemptId: ""`) when nothing had claimed it, so
|
|
175
|
+
* the ownership fence — which skips when there is no owner — did not stop that walk either;
|
|
176
|
+
* - reachable through the shipped controller, not just the store API: an `approved` signal
|
|
177
|
+
* claims, a `rejected` signal writes the tombstone, and the first call then invokes anyway.
|
|
178
|
+
* Typed, for the same reason DecidioAlreadySettledError is: the reporting stage deliberately keeps
|
|
179
|
+
* going when the ledger fails, so a generic error here would read as "the disk write failed" and
|
|
180
|
+
* send an authority report for an action a human refused. */
|
|
181
|
+
/** `recover()` could not enumerate the ledger. It is a THROW, not an empty list, because those two
|
|
182
|
+
* are the same value to a caller and mean opposite things: `[]` is "you owe nothing", and this is
|
|
183
|
+
* "I cannot tell you what you owe". The old code logged to stderr and returned `[]`, so a script
|
|
184
|
+
* reading `if (!(await recover()).length) allClear()` treated an unreadable store — a permissions
|
|
185
|
+
* change, a mounted volume gone, a corrupt directory — as proof that no action was left
|
|
186
|
+
* outstanding. A crash is the correct failure here; silence is not. */
|
|
187
|
+
export declare class DecidioRecoveryUnavailableError extends Error {
|
|
188
|
+
cause: unknown;
|
|
189
|
+
constructor(cause: unknown);
|
|
190
|
+
}
|
|
191
|
+
export declare class DecidioDeniedError extends Error {
|
|
192
|
+
decisionId: string;
|
|
193
|
+
verdict: string;
|
|
194
|
+
constructor(decisionId: string, verdict: string);
|
|
195
|
+
}
|
|
130
196
|
export declare const SETTLE_OUTCOMES: readonly SettleOutcome[];
|
|
131
|
-
export declare function assertSettlable(id: string, state: ExecutionState, outcome: SettleOutcome,
|
|
197
|
+
export declare function assertSettlable(id: string, state: ExecutionState, outcome: SettleOutcome, opts?: {
|
|
198
|
+
hasObservedEvidence?: boolean;
|
|
199
|
+
workerStopped?: boolean;
|
|
200
|
+
}): void;
|
|
132
201
|
export interface ClaimResult {
|
|
133
202
|
/** True only when THIS attempt may proceed to invoke. */
|
|
134
203
|
ok: boolean;
|
|
@@ -148,8 +217,13 @@ export interface ClaimResult {
|
|
|
148
217
|
export interface PendingStore {
|
|
149
218
|
put(p: PendingAction): Promise<void>;
|
|
150
219
|
get(decisionId: string): Promise<PendingAction | null>;
|
|
151
|
-
/** CLEANUP ONLY. Removing a row must never be what prevents a second execution.
|
|
152
|
-
|
|
220
|
+
/** CLEANUP ONLY. Removing a row must never be what prevents a second execution.
|
|
221
|
+
*
|
|
222
|
+
* `attemptId` scopes the cleanup to its OWNER. A superseded attempt tidies up on its way out
|
|
223
|
+
* (see the stale-attempt branch of the reporting stage), and without this it deleted the payload
|
|
224
|
+
* of the attempt that had legitimately taken over — the live worker's own recovery data, removed
|
|
225
|
+
* by the dead one. Omit it for genuine housekeeping that is not attributable to an attempt. */
|
|
226
|
+
delete(decisionId: string, attemptId?: string): Promise<void>;
|
|
153
227
|
/** Parked entries only — anything claimed or beyond is excluded. */
|
|
154
228
|
list(): Promise<PendingAction[]>;
|
|
155
229
|
/** Atomically take ownership for THIS attempt. Must refuse when the decision is already
|
|
@@ -164,6 +238,12 @@ export interface PendingStore {
|
|
|
164
238
|
markIndeterminate(decisionId: string, attemptId: string, error: string): Promise<void>;
|
|
165
239
|
/** Decidio accepted the execution report. */
|
|
166
240
|
markReported(decisionId: string, attemptId: string, confirmation: string): Promise<void>;
|
|
241
|
+
/** A human or policy DENIED this decision. Writes the terminal tombstone that makes the decision
|
|
242
|
+
* permanently unclaimable — and it must be written BEFORE any payload cleanup, because cleanup
|
|
243
|
+
* is best-effort and a store that swallows its failure would otherwise leave the row readable as
|
|
244
|
+
* `parked`. A conforming store persists this durably; returning without persisting reintroduces
|
|
245
|
+
* F-001. Safe to call more than once (a denial is idempotent). */
|
|
246
|
+
markDiscarded(decisionId: string, verdict: "rejected" | "blocked", reason?: string): Promise<void>;
|
|
167
247
|
/** The authoritative state for one decision, or null if this store has never seen it. */
|
|
168
248
|
getExecution(decisionId: string): Promise<ExecutionRecord | null>;
|
|
169
249
|
/** Every execution record the store holds — the basis for honest recovery reporting. */
|
|
@@ -181,6 +261,7 @@ export interface PendingStore {
|
|
|
181
261
|
evidence?: unknown;
|
|
182
262
|
reason?: string;
|
|
183
263
|
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
264
|
+
workerStopped?: boolean;
|
|
184
265
|
}): Promise<void>;
|
|
185
266
|
/** Release a claim taken BEFORE invocation, so a pre-invoke failure stays retryable. It is a
|
|
186
267
|
* contract error to call this once `markInvoking` has been written, and a conforming store
|
|
@@ -200,13 +281,16 @@ export declare class MemoryPendingStore implements PendingStore {
|
|
|
200
281
|
private advance;
|
|
201
282
|
put(p: PendingAction): Promise<void>;
|
|
202
283
|
get(id: string): Promise<PendingAction | null>;
|
|
203
|
-
|
|
284
|
+
/** Cleanup only — the execution record STAYS. Scoped to the owning attempt: see PendingStore. */
|
|
285
|
+
delete(id: string, attemptId?: string): Promise<void>;
|
|
286
|
+
private ownsPayload;
|
|
204
287
|
list(): Promise<PendingAction[]>;
|
|
205
288
|
claim(id: string, attemptId: string): Promise<ClaimResult>;
|
|
206
289
|
markInvoking(id: string, attemptId: string): Promise<void>;
|
|
207
290
|
recordExecuted(id: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
|
|
208
291
|
markIndeterminate(id: string, attemptId: string, error: string): Promise<void>;
|
|
209
292
|
markReported(id: string, attemptId: string, confirmation: string): Promise<void>;
|
|
293
|
+
markDiscarded(id: string, verdict: "rejected" | "blocked", reason?: string): Promise<void>;
|
|
210
294
|
getExecution(id: string): Promise<ExecutionRecord | null>;
|
|
211
295
|
listExecutions(): Promise<ExecutionRecord[]>;
|
|
212
296
|
/** OPERATOR RECONCILIATION — the ONE legal backwards transition, and the only exit from
|
|
@@ -220,9 +304,28 @@ export declare class MemoryPendingStore implements PendingStore {
|
|
|
220
304
|
evidence?: unknown;
|
|
221
305
|
reason?: string;
|
|
222
306
|
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
307
|
+
workerStopped?: boolean;
|
|
223
308
|
}): Promise<void>;
|
|
224
309
|
releasePreinvoke(id: string, attemptId: string): Promise<void>;
|
|
225
310
|
}
|
|
311
|
+
/** The ACL principal for THIS process, resolved from the token SID rather than the login name.
|
|
312
|
+
*
|
|
313
|
+
* Shared by every hardening site (F-009): the durable pending directory AND the `.env` the CLI
|
|
314
|
+
* writes. `writeSecretFile` had its own weaker copy that granted `userInfo().username` — who
|
|
315
|
+
* logged IN, which is not always who is writing. A Windows service, a scheduled task or an
|
|
316
|
+
* impersonated thread has a different effective identity, and granting the wrong name while
|
|
317
|
+
* `/inheritance:r` strips every other ACE is how a hardening step locks the writer out of its own
|
|
318
|
+
* secrets. A SID also needs no localisation or escaping, where a name can carry spaces, a domain
|
|
319
|
+
* prefix, or be renamed out from under the ACE. */
|
|
320
|
+
export declare function windowsPrincipal(): string;
|
|
321
|
+
/** Restrict a FILE to its owner on Windows, with the same verify-then-roll-back discipline as
|
|
322
|
+
* `hardenDir`. Returns null on success, or the reason it could not be done.
|
|
323
|
+
*
|
|
324
|
+
* The verification is the point, and it is what the CLI's own version lacked: a SUCCESSFUL
|
|
325
|
+
* `icacls` that locks this process out of the file it just wrote is more dangerous than a failed
|
|
326
|
+
* one, and it printed nothing at all. Here the file is read back, and if that fails the
|
|
327
|
+
* inheritance goes back rather than leaving credentials the SDK can no longer read. */
|
|
328
|
+
export declare function hardenFile(file: string): string | null;
|
|
226
329
|
/** Restrict a directory to its owner. On Windows POSIX modes are a no-op, so strip inheritance
|
|
227
330
|
* and grant only the effective user. A FAILURE IS SAID, never swallowed: these files hold the
|
|
228
331
|
* arguments of actions awaiting approval, and "silently kept the directory's inherited
|
|
@@ -261,13 +364,14 @@ export declare class FilePendingStore implements PendingStore {
|
|
|
261
364
|
get(id: string): Promise<PendingAction | null>;
|
|
262
365
|
/** CLEANUP ONLY — the payload goes, the execution record STAYS. That is the whole point of
|
|
263
366
|
* the v2 model: a failed or skipped delete can no longer make an executed action runnable. */
|
|
264
|
-
delete(id: string): Promise<void>;
|
|
367
|
+
delete(id: string, attemptId?: string): Promise<void>;
|
|
265
368
|
list(): Promise<PendingAction[]>;
|
|
266
369
|
claim(id: string, attemptId: string): Promise<ClaimResult>;
|
|
267
370
|
markInvoking(id: string, attemptId: string): Promise<void>;
|
|
268
371
|
recordExecuted(id: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
|
|
269
372
|
markIndeterminate(id: string, attemptId: string, error: string): Promise<void>;
|
|
270
373
|
markReported(id: string, attemptId: string, confirmation: string): Promise<void>;
|
|
374
|
+
markDiscarded(id: string, verdict: "rejected" | "blocked", reason?: string): Promise<void>;
|
|
271
375
|
getExecution(id: string): Promise<ExecutionRecord | null>;
|
|
272
376
|
listExecutions(): Promise<ExecutionRecord[]>;
|
|
273
377
|
/** OPERATOR RECONCILIATION — see MemoryPendingStore.settle. Writes the new marker first and
|
|
@@ -278,6 +382,7 @@ export declare class FilePendingStore implements PendingStore {
|
|
|
278
382
|
evidence?: unknown;
|
|
279
383
|
reason?: string;
|
|
280
384
|
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
385
|
+
workerStopped?: boolean;
|
|
281
386
|
}): Promise<void>;
|
|
282
387
|
releasePreinvoke(id: string, attemptId: string): Promise<void>;
|
|
283
388
|
/** ONE-WAY MIGRATION from the 0.1.x layout, run once at construction. `<id>.json` → parked;
|
|
@@ -331,6 +436,11 @@ export type RecoveryState =
|
|
|
331
436
|
* operator releases it. Reporting this as "parked" (as 0.2.0's first draft did) told a UI an
|
|
332
437
|
* approved action was still queued to resume when in fact it had been silently dropped. */
|
|
333
438
|
| "claimed_stalled"
|
|
439
|
+
/** DENIED, and an execution marker exists anyway. Nothing here will run it and nothing will
|
|
440
|
+
* report it — the decision is closed — but an effect may exist downstream, so this is the one
|
|
441
|
+
* recovery row that asks a human to go and look. It cannot be reconciled or retried; a denial is
|
|
442
|
+
* terminal. Callers switching exhaustively on this union must handle it. */
|
|
443
|
+
| "denied_with_evidence"
|
|
334
444
|
/** Claimed for execution and NOT journalled: the process died somewhere between claiming and
|
|
335
445
|
* recording the result. The action MAY OR MAY NOT have run — this cannot be resolved locally
|
|
336
446
|
* and must never be retried automatically. */
|
|
@@ -492,6 +602,7 @@ export interface ResumeController extends ResumeConfig {
|
|
|
492
602
|
outcome: SettleOutcome;
|
|
493
603
|
evidence?: unknown;
|
|
494
604
|
reason?: string;
|
|
605
|
+
workerStopped?: boolean;
|
|
495
606
|
}): Promise<EvidenceOutcome>;
|
|
496
607
|
/** Every durable item that needs a human's attention, with its state named — including the one
|
|
497
608
|
* state that CANNOT be resolved automatically: `indeterminate`, where a process died between
|