@decidio/sdk 0.2.2 → 0.3.3
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 +189 -12
- package/dist/adapters/_gate.js +1 -1
- 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 +97 -24
- package/dist/errors.js +12 -4
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.js +13 -7
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -2
- package/dist/inngest.d.ts +10 -2
- package/dist/inngest.js +2 -2
- package/dist/resume.d.ts +164 -27
- package/dist/resume.js +432 -201
- package/dist/signing.d.ts +8 -1
- package/dist/signing.js +18 -23
- package/openapi.yaml +38 -0
- package/package.json +3 -8
package/dist/evidence.d.ts
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
* we do not understand is never treated as evidence. */
|
|
4
4
|
export declare const EVIDENCE_TIERS: readonly ["pending", "execution_unconfirmed", "agent_asserted", "sensor_observed", "application_confirmed", "independently_reconciled"];
|
|
5
5
|
export type EvidenceTier = (typeof EVIDENCE_TIERS)[number];
|
|
6
|
+
/** Whether a recorded tier attests a COMMITTED execution - the same set the live reader uses, so a
|
|
7
|
+
* tombstone that kept only the tier can still answer the question honestly (retest V030-07). */
|
|
8
|
+
export declare function tierConfirms(tier: string | undefined | null): boolean;
|
|
9
|
+
/** The tier a `reported` tombstone records. The server caps a confirming tier whose proof failed to
|
|
10
|
+
* `execution_unconfirmed` (apps/api agent-confirm.ts, lint-locked CONFIRM-TRUTH), so a body that
|
|
11
|
+
* says otherwise is one the current server cannot emit; the tombstone applies the same rule, so
|
|
12
|
+
* reconfirm() - which derives from the tier - can never answer "confirmed" where resolve() answered
|
|
13
|
+
* "not confirmed" over the same record (invariant-guard, 0.3.3). Every other tier is stored verbatim. */
|
|
14
|
+
export declare function tombstoneTier(ev: EvidenceOutcome): string;
|
|
6
15
|
export interface EvidenceOutcome {
|
|
7
16
|
/** Decidio accepted the report and named the tier it recorded. */
|
|
8
17
|
reportRecorded: boolean;
|
package/dist/evidence.js
CHANGED
|
@@ -7,6 +7,10 @@ export const EVIDENCE_TIERS = [
|
|
|
7
7
|
"independently_reconciled",
|
|
8
8
|
];
|
|
9
9
|
const CONFIRMING = ["sensor_observed", "application_confirmed", "independently_reconciled"];
|
|
10
|
+
export function tierConfirms(tier) { return typeof tier === "string" && CONFIRMING.includes(tier); }
|
|
11
|
+
export function tombstoneTier(ev) {
|
|
12
|
+
return ev.executionReportVerified === false && tierConfirms(ev.evidenceTier) ? "execution_unconfirmed" : (ev.evidenceTier ?? "recorded");
|
|
13
|
+
}
|
|
10
14
|
export function describeTierGap(capturedResponse) {
|
|
11
15
|
const r = (capturedResponse ?? {});
|
|
12
16
|
const id = r.id ?? r.recordId ?? r.Id ?? null;
|
|
@@ -32,20 +36,22 @@ export function readConfirmResponse(body, opts) {
|
|
|
32
36
|
if (!EVIDENCE_TIERS.includes(tier)) {
|
|
33
37
|
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}` };
|
|
34
38
|
}
|
|
35
|
-
const
|
|
39
|
+
const rpt = body?.executionReport;
|
|
40
|
+
const hasVerdict = !!rpt && typeof rpt === "object" && typeof rpt.verified === "boolean";
|
|
41
|
+
const proofExplicitlyFailed = hasVerdict && rpt.verified === false;
|
|
42
|
+
const executionConfirmed = CONFIRMING.includes(tier) && !proofExplicitlyFailed;
|
|
36
43
|
const outcome = { reportRecorded: true, evidenceTier: tier, executionConfirmed, reportJournalled: journalled };
|
|
37
|
-
if (!executionConfirmed && opts.signed)
|
|
44
|
+
if (!executionConfirmed && !proofExplicitlyFailed && opts.signed)
|
|
38
45
|
outcome.diagnostic = describeTierGap(opts.capturedResponse);
|
|
39
|
-
else if (!executionConfirmed)
|
|
46
|
+
else if (!executionConfirmed && !proofExplicitlyFailed)
|
|
40
47
|
outcome.diagnostic = "no signing key is configured, so the gate capped this report at an unproven agent assertion. Run `npx @decidio/sdk init` to generate the agent's key and earn a confirming tier.";
|
|
41
|
-
|
|
42
|
-
if (rpt && typeof rpt === "object" && typeof rpt.verified === "boolean") {
|
|
48
|
+
if (hasVerdict) {
|
|
43
49
|
outcome.executionReportVerified = rpt.verified;
|
|
44
50
|
if (typeof rpt.reason === "string" && rpt.reason)
|
|
45
51
|
outcome.executionReportReason = rpt.reason;
|
|
46
|
-
if (
|
|
52
|
+
if (proofExplicitlyFailed) {
|
|
47
53
|
const why = typeof rpt.reason === "string" && rpt.reason ? ` (${rpt.reason})` : "";
|
|
48
|
-
outcome.diagnostic =
|
|
54
|
+
outcome.diagnostic = `the signed execution report did NOT verify${why}, so this is NOT a confirmed execution — the gate reported tier "${tier}", and this SDK refuses to read an unproven attestation as confirmation. Your action DID run and the report IS recorded at that tier - there is nothing to re-send, and re-sending would not change the verdict; the record stands as unverified. Check the agent's key/DID/registration (npx @decidio/sdk doctor) so the NEXT execution verifies.`;
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
return outcome;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResumeController } from "./resume.js";
|
|
2
2
|
import { type EvidenceOutcome } from "./evidence.js";
|
|
3
|
-
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
3
|
+
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
4
4
|
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome } from "./resume.js";
|
|
5
5
|
export { guard, Guard } from "./guard.js";
|
|
6
6
|
export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
@@ -12,7 +12,10 @@ export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS
|
|
|
12
12
|
export type { EvidenceOutcome, EvidenceTier } from "./evidence.js";
|
|
13
13
|
export type { RequesterIdentity } from "./signing.js";
|
|
14
14
|
export interface GuardConfig {
|
|
15
|
-
/** Base URL of the Decidio gate
|
|
15
|
+
/** Base URL of the Decidio gate. The hosted sandbox is "https://decidio-api.onrender.com",
|
|
16
|
+
* which is also the default when this is unset; a self-hosted deployment is your own origin.
|
|
17
|
+
* (This example named a host the product does not use — it shipped in the .d.ts, so it was the
|
|
18
|
+
* first URL a typing-aware editor suggested to anyone configuring the SDK.) */
|
|
16
19
|
apiUrl: string;
|
|
17
20
|
/** The agent identity registered with Decidio (onboarding wizard), e.g. "salesforce/SalesforceBot". */
|
|
18
21
|
agentId: string;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { DecidioSuspendedError } from "./resume.js";
|
|
|
2
2
|
import { signBoundProof, signConfirm, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
3
3
|
import { canonicalize, canonicalizeArgs } from "./canonical.js";
|
|
4
4
|
import { readConfirmResponse, buildConfirmBody } from "./evidence.js";
|
|
5
|
-
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
5
|
+
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
6
6
|
export { guard, Guard } from "./guard.js";
|
|
7
7
|
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
8
8
|
import { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
@@ -138,10 +138,10 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
138
138
|
}
|
|
139
139
|
throw e;
|
|
140
140
|
}
|
|
141
|
+
ctx = canonicalize(ctx, "describe(...)");
|
|
141
142
|
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
142
143
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
143
144
|
: undefined;
|
|
144
|
-
canonicalize(ctx, "describe(...)");
|
|
145
145
|
if (typeof ctx?.action !== "string" || ctx.action.trim().length === 0) {
|
|
146
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
147
|
}
|
package/dist/inngest.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RequestListener } from "node:http";
|
|
2
2
|
import { verifyResumeSignature } from "./resume.js";
|
|
3
|
-
import type { GuardConfig, ApprovalContext } from "./index.js";
|
|
3
|
+
import type { GuardConfig, ApprovalContext, EvidenceOutcome } from "./index.js";
|
|
4
4
|
/** The Inngest event Decidio's verdict is translated into. The waiting function
|
|
5
5
|
* correlates on `data.decisionId`. */
|
|
6
6
|
export declare const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
|
|
@@ -24,10 +24,18 @@ export interface InngestClientLike {
|
|
|
24
24
|
data: Record<string, any>;
|
|
25
25
|
}): Promise<unknown>;
|
|
26
26
|
}
|
|
27
|
+
/** ONE RESULT SHAPE ACROSS EVERY DOOR (F-008), with the one field this door genuinely needs on
|
|
28
|
+
* top of it. `status` stays because Inngest's durable path can legitimately end in `rejected` or
|
|
29
|
+
* `blocked` without ever calling your action — the other adapters signal that by throwing. What
|
|
30
|
+
* changed: the value is `value` (not `result`) so it reads the same as every other door, and
|
|
31
|
+
* `confirmation` is CARRIED rather than discarded. It used to be thrown away inside the confirm
|
|
32
|
+
* step, which made "it ran and Decidio recorded it" and "it ran and Decidio does not know"
|
|
33
|
+
* indistinguishable to the caller. */
|
|
27
34
|
export type GateStepOutcome<R> = {
|
|
28
35
|
status: "executed" | "rejected" | "blocked";
|
|
29
36
|
decisionId: string;
|
|
30
|
-
|
|
37
|
+
value?: R;
|
|
38
|
+
confirmation?: EvidenceOutcome;
|
|
31
39
|
};
|
|
32
40
|
/**
|
|
33
41
|
* Gate an action INSIDE an Inngest function, with durable suspend on routing.
|
package/dist/inngest.js
CHANGED
|
@@ -22,8 +22,8 @@ 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 () =>
|
|
26
|
-
return { status: "executed", decisionId: auth.decisionId, result };
|
|
25
|
+
const confirmation = await step.run("decidio-confirm", async () => gateConfirm(config, auth.decisionId, result, args.evidence));
|
|
26
|
+
return { status: "executed", decisionId: auth.decisionId, value: result, confirmation };
|
|
27
27
|
}
|
|
28
28
|
export function createInngestResumeBridge(opts) {
|
|
29
29
|
const secret = opts.webhookSecret ?? "";
|
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,26 @@ 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
|
+
/** Set when a DENIED decision also carries an execution record. That pair is the single most
|
|
133
|
+
* alarming state the machine can hold — a human refused it and something ran anyway — so it must
|
|
134
|
+
* never resolve to silence. The revision chain cannot produce it (no transition publishes over
|
|
135
|
+
* a denial); it is DERIVED by the legacy-marker reader, and PERSISTED only when a 0.2/0.3
|
|
136
|
+
* directory that held a denial marker beside an execution marker is migrated onto the chain —
|
|
137
|
+
* there it is the record of what that layout held. */
|
|
138
|
+
executionDespiteDenial?: {
|
|
139
|
+
state: ExecutionState;
|
|
140
|
+
at: string;
|
|
141
|
+
hasEvidence: boolean;
|
|
102
142
|
};
|
|
103
143
|
at: string;
|
|
104
144
|
}
|
|
@@ -127,8 +167,38 @@ export declare class DecidioAlreadySettledError extends Error {
|
|
|
127
167
|
decisionId: string;
|
|
128
168
|
constructor(decisionId: string);
|
|
129
169
|
}
|
|
170
|
+
/** A DENIAL IS TERMINAL, and that has to be enforced where the state is WRITTEN — not only where
|
|
171
|
+
* it is reconciled. Refusing `settle` from `discarded` closed the operator's door and left three
|
|
172
|
+
* others open, each of which walked a refused action all the way to execution:
|
|
173
|
+
* - `discarded` sits mid-rank in EXECUTION_STATES, so the advance-only rule happily accepted
|
|
174
|
+
* `discarded → invoking → executed_unreported`;
|
|
175
|
+
* - `markDiscarded` leaves the record UNOWNED (`attemptId: ""`) when nothing had claimed it, so
|
|
176
|
+
* the ownership fence — which skips when there is no owner — did not stop that walk either;
|
|
177
|
+
* - reachable through the shipped controller, not just the store API: an `approved` signal
|
|
178
|
+
* claims, a `rejected` signal writes the tombstone, and the first call then invokes anyway.
|
|
179
|
+
* Typed, for the same reason DecidioAlreadySettledError is: the reporting stage deliberately keeps
|
|
180
|
+
* going when the ledger fails, so a generic error here would read as "the disk write failed" and
|
|
181
|
+
* send an authority report for an action a human refused. */
|
|
182
|
+
/** `recover()` could not enumerate the ledger. It is a THROW, not an empty list, because those two
|
|
183
|
+
* are the same value to a caller and mean opposite things: `[]` is "you owe nothing", and this is
|
|
184
|
+
* "I cannot tell you what you owe". The old code logged to stderr and returned `[]`, so a script
|
|
185
|
+
* reading `if (!(await recover()).length) allClear()` treated an unreadable store — a permissions
|
|
186
|
+
* change, a mounted volume gone, a corrupt directory — as proof that no action was left
|
|
187
|
+
* outstanding. A crash is the correct failure here; silence is not. */
|
|
188
|
+
export declare class DecidioRecoveryUnavailableError extends Error {
|
|
189
|
+
cause: unknown;
|
|
190
|
+
constructor(cause: unknown);
|
|
191
|
+
}
|
|
192
|
+
export declare class DecidioDeniedError extends Error {
|
|
193
|
+
decisionId: string;
|
|
194
|
+
verdict: string;
|
|
195
|
+
constructor(decisionId: string, verdict: string);
|
|
196
|
+
}
|
|
130
197
|
export declare const SETTLE_OUTCOMES: readonly SettleOutcome[];
|
|
131
|
-
export declare function assertSettlable(id: string, state: ExecutionState, outcome: SettleOutcome,
|
|
198
|
+
export declare function assertSettlable(id: string, state: ExecutionState, outcome: SettleOutcome, opts?: {
|
|
199
|
+
hasObservedEvidence?: boolean;
|
|
200
|
+
workerStopped?: boolean;
|
|
201
|
+
}): void;
|
|
132
202
|
export interface ClaimResult {
|
|
133
203
|
/** True only when THIS attempt may proceed to invoke. */
|
|
134
204
|
ok: boolean;
|
|
@@ -148,8 +218,13 @@ export interface ClaimResult {
|
|
|
148
218
|
export interface PendingStore {
|
|
149
219
|
put(p: PendingAction): Promise<void>;
|
|
150
220
|
get(decisionId: string): Promise<PendingAction | null>;
|
|
151
|
-
/** CLEANUP ONLY. Removing a row must never be what prevents a second execution.
|
|
152
|
-
|
|
221
|
+
/** CLEANUP ONLY. Removing a row must never be what prevents a second execution.
|
|
222
|
+
*
|
|
223
|
+
* `attemptId` scopes the cleanup to its OWNER. A superseded attempt tidies up on its way out
|
|
224
|
+
* (see the stale-attempt branch of the reporting stage), and without this it deleted the payload
|
|
225
|
+
* of the attempt that had legitimately taken over — the live worker's own recovery data, removed
|
|
226
|
+
* by the dead one. Omit it for genuine housekeeping that is not attributable to an attempt. */
|
|
227
|
+
delete(decisionId: string, attemptId?: string): Promise<void>;
|
|
153
228
|
/** Parked entries only — anything claimed or beyond is excluded. */
|
|
154
229
|
list(): Promise<PendingAction[]>;
|
|
155
230
|
/** Atomically take ownership for THIS attempt. Must refuse when the decision is already
|
|
@@ -164,6 +239,12 @@ export interface PendingStore {
|
|
|
164
239
|
markIndeterminate(decisionId: string, attemptId: string, error: string): Promise<void>;
|
|
165
240
|
/** Decidio accepted the execution report. */
|
|
166
241
|
markReported(decisionId: string, attemptId: string, confirmation: string): Promise<void>;
|
|
242
|
+
/** A human or policy DENIED this decision. Writes the terminal tombstone that makes the decision
|
|
243
|
+
* permanently unclaimable — and it must be written BEFORE any payload cleanup, because cleanup
|
|
244
|
+
* is best-effort and a store that swallows its failure would otherwise leave the row readable as
|
|
245
|
+
* `parked`. A conforming store persists this durably; returning without persisting reintroduces
|
|
246
|
+
* F-001. Safe to call more than once (a denial is idempotent). */
|
|
247
|
+
markDiscarded(decisionId: string, verdict: "rejected" | "blocked", reason?: string): Promise<void>;
|
|
167
248
|
/** The authoritative state for one decision, or null if this store has never seen it. */
|
|
168
249
|
getExecution(decisionId: string): Promise<ExecutionRecord | null>;
|
|
169
250
|
/** Every execution record the store holds — the basis for honest recovery reporting. */
|
|
@@ -181,6 +262,7 @@ export interface PendingStore {
|
|
|
181
262
|
evidence?: unknown;
|
|
182
263
|
reason?: string;
|
|
183
264
|
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
265
|
+
workerStopped?: boolean;
|
|
184
266
|
}): Promise<void>;
|
|
185
267
|
/** Release a claim taken BEFORE invocation, so a pre-invoke failure stays retryable. It is a
|
|
186
268
|
* contract error to call this once `markInvoking` has been written, and a conforming store
|
|
@@ -200,13 +282,16 @@ export declare class MemoryPendingStore implements PendingStore {
|
|
|
200
282
|
private advance;
|
|
201
283
|
put(p: PendingAction): Promise<void>;
|
|
202
284
|
get(id: string): Promise<PendingAction | null>;
|
|
203
|
-
|
|
285
|
+
/** Cleanup only — the execution record STAYS. Scoped to the owning attempt: see PendingStore. */
|
|
286
|
+
delete(id: string, attemptId?: string): Promise<void>;
|
|
287
|
+
private ownsPayload;
|
|
204
288
|
list(): Promise<PendingAction[]>;
|
|
205
289
|
claim(id: string, attemptId: string): Promise<ClaimResult>;
|
|
206
290
|
markInvoking(id: string, attemptId: string): Promise<void>;
|
|
207
291
|
recordExecuted(id: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
|
|
208
292
|
markIndeterminate(id: string, attemptId: string, error: string): Promise<void>;
|
|
209
293
|
markReported(id: string, attemptId: string, confirmation: string): Promise<void>;
|
|
294
|
+
markDiscarded(id: string, verdict: "rejected" | "blocked", reason?: string): Promise<void>;
|
|
210
295
|
getExecution(id: string): Promise<ExecutionRecord | null>;
|
|
211
296
|
listExecutions(): Promise<ExecutionRecord[]>;
|
|
212
297
|
/** OPERATOR RECONCILIATION — the ONE legal backwards transition, and the only exit from
|
|
@@ -220,9 +305,28 @@ export declare class MemoryPendingStore implements PendingStore {
|
|
|
220
305
|
evidence?: unknown;
|
|
221
306
|
reason?: string;
|
|
222
307
|
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
308
|
+
workerStopped?: boolean;
|
|
223
309
|
}): Promise<void>;
|
|
224
310
|
releasePreinvoke(id: string, attemptId: string): Promise<void>;
|
|
225
311
|
}
|
|
312
|
+
/** The ACL principal for THIS process, resolved from the token SID rather than the login name.
|
|
313
|
+
*
|
|
314
|
+
* Shared by every hardening site (F-009): the durable pending directory AND the `.env` the CLI
|
|
315
|
+
* writes. `writeSecretFile` had its own weaker copy that granted `userInfo().username` — who
|
|
316
|
+
* logged IN, which is not always who is writing. A Windows service, a scheduled task or an
|
|
317
|
+
* impersonated thread has a different effective identity, and granting the wrong name while
|
|
318
|
+
* `/inheritance:r` strips every other ACE is how a hardening step locks the writer out of its own
|
|
319
|
+
* secrets. A SID also needs no localisation or escaping, where a name can carry spaces, a domain
|
|
320
|
+
* prefix, or be renamed out from under the ACE. */
|
|
321
|
+
export declare function windowsPrincipal(): string;
|
|
322
|
+
/** Restrict a FILE to its owner on Windows, with the same verify-then-roll-back discipline as
|
|
323
|
+
* `hardenDir`. Returns null on success, or the reason it could not be done.
|
|
324
|
+
*
|
|
325
|
+
* The verification is the point, and it is what the CLI's own version lacked: a SUCCESSFUL
|
|
326
|
+
* `icacls` that locks this process out of the file it just wrote is more dangerous than a failed
|
|
327
|
+
* one, and it printed nothing at all. Here the file is read back, and if that fails the
|
|
328
|
+
* inheritance goes back rather than leaving credentials the SDK can no longer read. */
|
|
329
|
+
export declare function hardenFile(file: string): string | null;
|
|
226
330
|
/** Restrict a directory to its owner. On Windows POSIX modes are a no-op, so strip inheritance
|
|
227
331
|
* and grant only the effective user. A FAILURE IS SAID, never swallowed: these files hold the
|
|
228
332
|
* arguments of actions awaiting approval, and "silently kept the directory's inherited
|
|
@@ -243,51 +347,78 @@ export declare class FilePendingStore implements PendingStore {
|
|
|
243
347
|
constructor(dir: string);
|
|
244
348
|
private key;
|
|
245
349
|
private path;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
private
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
350
|
+
private revPath;
|
|
351
|
+
/** Legacy (0.2/0.3) marker path, read for migration and never written. */
|
|
352
|
+
private markerPath;
|
|
353
|
+
private listRevs;
|
|
354
|
+
private legacyMarkers;
|
|
355
|
+
/** Write `text` to a fresh temp file beside `target`, fsynced. Returns the temp path. */
|
|
356
|
+
private writeTemp;
|
|
357
|
+
/** Durable write for the PAYLOAD file (not the chain): temp → fsync → atomic rename. */
|
|
253
358
|
private writeAtomic;
|
|
254
|
-
/** The
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
|
|
258
|
-
|
|
359
|
+
/** The head of the chain, or the migrated reading of a legacy layout that has not been
|
|
360
|
+
* upgraded. Corruption is REPORTED, never treated as absent: an unreadable record is the one
|
|
361
|
+
* that proves an action already ran. */
|
|
362
|
+
private readHead;
|
|
363
|
+
/** TEST SEAM, a no-op in production. Called with the operation name and the state about to be
|
|
364
|
+
* published, AFTER validation and BEFORE the publish — the exact window a concurrent writer
|
|
365
|
+
* can land in. The deterministic two-process suites subclass the store and block here so the
|
|
366
|
+
* race the retest found is reproduced on purpose, every run, rather than by luck. */
|
|
367
|
+
protected _barrier(_op: string, _id: string, _next: ExecutionState): void;
|
|
368
|
+
/** ONE TRANSITION = read head → compute successor → publish with link(). `compute` receives the
|
|
369
|
+
* current record (null if none) and returns the successor, or null for "nothing to write" (a
|
|
370
|
+
* refusal that is a return value, like a lost claim, rather than an error). Every rule runs
|
|
371
|
+
* INSIDE `compute`, against the revision that is about to be superseded; if a rival publishes
|
|
372
|
+
* first, `compute` runs again against the rival's record. */
|
|
373
|
+
private transition;
|
|
374
|
+
/** The 0.2/0.3 reading of a marker layout — strongest marker wins, a denial marker wins at any
|
|
375
|
+
* rank, and a denial beside an execution marker is reported as both. Used ONLY to migrate an
|
|
376
|
+
* older directory and to report one that could not be migrated; nothing writes markers. */
|
|
377
|
+
private resolveLegacyMarkers;
|
|
378
|
+
/** The generic ADVANCE, with every fence evaluated against the head the publish will supersede.
|
|
379
|
+
* `pre` is a per-method check (markInvoking's claim requirement) that must ALSO run inside the
|
|
380
|
+
* transition — a check that runs before it is a check against a snapshot. */
|
|
259
381
|
private setState;
|
|
260
382
|
put(p: PendingAction): Promise<void>;
|
|
261
383
|
get(id: string): Promise<PendingAction | null>;
|
|
262
384
|
/** CLEANUP ONLY — the payload goes, the execution record STAYS. That is the whole point of
|
|
263
385
|
* the v2 model: a failed or skipped delete can no longer make an executed action runnable. */
|
|
264
|
-
delete(id: string): Promise<void>;
|
|
386
|
+
delete(id: string, attemptId?: string): Promise<void>;
|
|
265
387
|
list(): Promise<PendingAction[]>;
|
|
266
388
|
claim(id: string, attemptId: string): Promise<ClaimResult>;
|
|
267
389
|
markInvoking(id: string, attemptId: string): Promise<void>;
|
|
268
390
|
recordExecuted(id: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
|
|
269
391
|
markIndeterminate(id: string, attemptId: string, error: string): Promise<void>;
|
|
270
392
|
markReported(id: string, attemptId: string, confirmation: string): Promise<void>;
|
|
393
|
+
markDiscarded(id: string, verdict: "rejected" | "blocked", reason?: string): Promise<void>;
|
|
271
394
|
getExecution(id: string): Promise<ExecutionRecord | null>;
|
|
272
395
|
listExecutions(): Promise<ExecutionRecord[]>;
|
|
273
|
-
/** OPERATOR RECONCILIATION — see MemoryPendingStore.settle.
|
|
274
|
-
*
|
|
275
|
-
* the
|
|
276
|
-
*
|
|
396
|
+
/** OPERATOR RECONCILIATION — see MemoryPendingStore.settle. The one legal backwards move, and
|
|
397
|
+
* now a transition like any other: two conflicting settlements both validate, both try to
|
|
398
|
+
* publish the next revision, exactly one wins, and the loser re-validates against the winner's
|
|
399
|
+
* record — where `assertSettlable` refuses it (captured evidence protects a `committed`; a
|
|
400
|
+
* `parked` record refuses `committed`). Before this, both won and the second erased the first. */
|
|
277
401
|
settle(id: string, outcome: SettleOutcome, opts?: {
|
|
278
402
|
evidence?: unknown;
|
|
279
403
|
reason?: string;
|
|
280
404
|
evidenceSig?: ExecutionRecord["evidenceSig"];
|
|
405
|
+
workerStopped?: boolean;
|
|
281
406
|
}): Promise<void>;
|
|
282
407
|
releasePreinvoke(id: string, attemptId: string): Promise<void>;
|
|
283
|
-
/** ONE-WAY MIGRATION
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
408
|
+
/** ONE-WAY MIGRATION of older layouts, run at construction.
|
|
409
|
+
* 0.2/0.3 one-marker-per-state → the head of a fresh revision chain (markers removed)
|
|
410
|
+
* 0.1.x `<id>.json` with no state → parked
|
|
411
|
+
* 0.1.x `<id>.claimed` → INDETERMINATE, never parked: a claimed 0.1.x entry may have
|
|
412
|
+
* been mid-flight when the process died, and assuming it did
|
|
413
|
+
* not run is the assumption that duplicates a payment
|
|
414
|
+
* 0.1.x `reports/<id>.json` → executed_unreported (the report can be re-sent)
|
|
415
|
+
* Nothing ambiguous is made executable by an upgrade, and nothing unreadable is silently
|
|
416
|
+
* dropped: a key whose markers cannot be parsed is left exactly as found and reads as corrupt. */
|
|
287
417
|
migrateLegacy(): {
|
|
288
418
|
migrated: number;
|
|
289
419
|
indeterminate: number;
|
|
290
420
|
reports: number;
|
|
421
|
+
chained: number;
|
|
291
422
|
};
|
|
292
423
|
/** 0.1.x wrote execution reports to `<pendingDir>/reports/<id>.json` — evidence that an action
|
|
293
424
|
* ALREADY RAN and still owes Decidio a report. 0.2.0 keeps that in the ledger, so without this
|
|
@@ -331,6 +462,11 @@ export type RecoveryState =
|
|
|
331
462
|
* operator releases it. Reporting this as "parked" (as 0.2.0's first draft did) told a UI an
|
|
332
463
|
* approved action was still queued to resume when in fact it had been silently dropped. */
|
|
333
464
|
| "claimed_stalled"
|
|
465
|
+
/** DENIED, and an execution marker exists anyway. Nothing here will run it and nothing will
|
|
466
|
+
* report it — the decision is closed — but an effect may exist downstream, so this is the one
|
|
467
|
+
* recovery row that asks a human to go and look. It cannot be reconciled or retried; a denial is
|
|
468
|
+
* terminal. Callers switching exhaustively on this union must handle it. */
|
|
469
|
+
| "denied_with_evidence"
|
|
334
470
|
/** Claimed for execution and NOT journalled: the process died somewhere between claiming and
|
|
335
471
|
* recording the result. The action MAY OR MAY NOT have run — this cannot be resolved locally
|
|
336
472
|
* and must never be retried automatically. */
|
|
@@ -492,6 +628,7 @@ export interface ResumeController extends ResumeConfig {
|
|
|
492
628
|
outcome: SettleOutcome;
|
|
493
629
|
evidence?: unknown;
|
|
494
630
|
reason?: string;
|
|
631
|
+
workerStopped?: boolean;
|
|
495
632
|
}): Promise<EvidenceOutcome>;
|
|
496
633
|
/** Every durable item that needs a human's attention, with its state named — including the one
|
|
497
634
|
* state that CANNOT be resolved automatically: `indeterminate`, where a process died between
|