@decidio/sdk 0.4.0 → 0.4.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/CHANGELOG.md +208 -0
- package/README.md +21 -6
- package/dist/adapters/_gate.d.ts +6 -6
- package/dist/adapters/_gate.js +1 -1
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +791 -171
- package/dist/inngest.d.ts +14 -6
- package/dist/resume.d.ts +7 -1
- package/dist/resume.js +61 -11
- package/package.json +3 -2
package/dist/inngest.d.ts
CHANGED
|
@@ -31,13 +31,21 @@ export interface InngestClientLike {
|
|
|
31
31
|
* `confirmation` is CARRIED rather than discarded. It used to be thrown away inside the confirm
|
|
32
32
|
* step, which made "it ran and Decidio recorded it" and "it ran and Decidio does not know"
|
|
33
33
|
* indistinguishable to the caller. */
|
|
34
|
-
export type GateStepOutcome<R> =
|
|
35
|
-
|
|
34
|
+
export type GateStepOutcome<R> =
|
|
35
|
+
/** The action ran: the value AND what Decidio recorded (the step confirms before it returns). Encoded in the
|
|
36
|
+
* type, not only in prose (prodsec V040-02): narrow on `status` and `confirmation` is there. */
|
|
37
|
+
{
|
|
38
|
+
status: "executed";
|
|
36
39
|
decisionId: string;
|
|
37
|
-
value
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
value: R;
|
|
41
|
+
confirmation: EvidenceOutcome;
|
|
42
|
+
}
|
|
43
|
+
/** The action never ran — a human said no, or a rule blocked it — so there is nothing to confirm. */
|
|
44
|
+
| {
|
|
45
|
+
status: "rejected" | "blocked";
|
|
46
|
+
decisionId: string;
|
|
47
|
+
value?: undefined;
|
|
48
|
+
confirmation?: undefined;
|
|
41
49
|
};
|
|
42
50
|
/**
|
|
43
51
|
* Gate an action INSIDE an Inngest function, with durable suspend on routing.
|
package/dist/resume.d.ts
CHANGED
|
@@ -455,7 +455,13 @@ export declare class DecidioSuspendedError extends Error {
|
|
|
455
455
|
* to read `unknown`, the word for "not ours" (prodsec V033-06). An `invoking` row is NOT final and answers
|
|
456
456
|
* `indeterminate` (prodsec V035-02). Both members are additive: an exhaustive switch over this union
|
|
457
457
|
* gains a case per release, which the changelog announces. */
|
|
458
|
-
|
|
458
|
+
/** `lost_race` (0.4.1): this signal lost the pre-invocation claim to ANOTHER ATTEMPT that is still in flight, or
|
|
459
|
+
* the invocation boundary could not be recorded at all (a store fault) - and it did NOTHING. (A boundary lost to
|
|
460
|
+
* an attempt already in flight reads `indeterminate`: that attempt may be running.) Nothing ran on this signal; the winner reports, or the entry stays
|
|
461
|
+
* parked for a later signal. It used to read `unknown` (the word for "not ours") for the claim loser and
|
|
462
|
+
* `no_handler` for the boundary loser although a handler was registered (prodsec V040-01). A loss to a FINAL
|
|
463
|
+
* answer (a denial, an execution) stays `already_resolved`. Additive, like the two above. */
|
|
464
|
+
export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown" | "indeterminate" | "already_resolved" | "lost_race";
|
|
459
465
|
/** The states a durable action can actually be in — every one of them nameable, including the
|
|
460
466
|
* one nobody likes. */
|
|
461
467
|
export type RecoveryState =
|
package/dist/resume.js
CHANGED
|
@@ -335,8 +335,10 @@ export class FilePendingStore {
|
|
|
335
335
|
try {
|
|
336
336
|
names = readdirSync(this.dir);
|
|
337
337
|
}
|
|
338
|
-
catch {
|
|
339
|
-
|
|
338
|
+
catch (e) {
|
|
339
|
+
if (e?.code === "ENOENT")
|
|
340
|
+
return out;
|
|
341
|
+
throw new DecidioStateCorruptError(key, [`${this.dir}: ${e?.message ?? e}`]);
|
|
340
342
|
}
|
|
341
343
|
}
|
|
342
344
|
for (const name of names) {
|
|
@@ -753,6 +755,20 @@ export class DecidioSuspendedError extends Error {
|
|
|
753
755
|
this.name = "DecidioSuspendedError";
|
|
754
756
|
}
|
|
755
757
|
}
|
|
758
|
+
function outcomeFromHead(head, decisionId, action) {
|
|
759
|
+
if (!head)
|
|
760
|
+
return undefined;
|
|
761
|
+
const base = action ? { decisionId, action } : { decisionId };
|
|
762
|
+
switch (head.state) {
|
|
763
|
+
case "discarded": return { status: "already_resolved", ...base, reason: `this decision was DENIED (${head.verdict ?? "rejected"}); that answer is final and this signal did nothing` };
|
|
764
|
+
case "executed_unreported":
|
|
765
|
+
case "reported": return { status: "already_resolved", ...base, reason: `this decision already ran (durable state ${head.state}); this signal arrived after it and did nothing` };
|
|
766
|
+
case "invoking":
|
|
767
|
+
case "indeterminate": return { status: "indeterminate", ...base, reason: `this decision is in durable state ${head.state}: its action may still be running or may have finished without reporting - it may or may not have run. Nothing will retry it; check the worker and the downstream system, then settle it with reconcile("${decisionId}", …).` };
|
|
768
|
+
case "claimed_preinvoke": return { status: "lost_race", ...base, reason: `another attempt holds the pre-invocation claim on this decision; this signal did nothing. If that attempt is alive it will report; if it died, recover() lists the decision and reconcile() can settle it` };
|
|
769
|
+
default: return undefined;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
756
772
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
757
773
|
function sign(secret, body) {
|
|
758
774
|
return "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
|
|
@@ -951,7 +967,7 @@ export function createDecidioResume(opts) {
|
|
|
951
967
|
if (refusalWarned.has(decisionId))
|
|
952
968
|
return;
|
|
953
969
|
refusalWarned.add(decisionId);
|
|
954
|
-
console.error(msg + (tail ?? " (This prints once — the entry stays parked until you act.)"));
|
|
970
|
+
console.error(msg + (tail ?? " (This prints once per worker process — the entry stays parked until you act.)"));
|
|
955
971
|
};
|
|
956
972
|
const ctrl = {
|
|
957
973
|
...opts,
|
|
@@ -991,8 +1007,12 @@ export function createDecidioResume(opts) {
|
|
|
991
1007
|
if (idProblem)
|
|
992
1008
|
return { status: "unknown", decisionId: String(decisionId ?? ""), reason: `not a decision id this SDK will act on (${idProblem})` };
|
|
993
1009
|
const pending = await ctrl.store.get(decisionId);
|
|
994
|
-
if (!pending)
|
|
1010
|
+
if (!pending) {
|
|
1011
|
+
const mapped = outcomeFromHead(await ctrl.store.getExecution(decisionId), decisionId);
|
|
1012
|
+
if (mapped)
|
|
1013
|
+
return finish(mapped);
|
|
995
1014
|
return { status: "unknown", decisionId };
|
|
1015
|
+
}
|
|
996
1016
|
if (verdict === "rejected" || verdict === "blocked") {
|
|
997
1017
|
await ctrl.store.markDiscarded(decisionId, verdict, reason);
|
|
998
1018
|
await ctrl.store.delete(decisionId);
|
|
@@ -1036,7 +1056,7 @@ export function createDecidioResume(opts) {
|
|
|
1036
1056
|
if (claim.state && NON_REPLAYABLE.includes(claim.state)) {
|
|
1037
1057
|
refuseOnce(decisionId, claim.state === "discarded"
|
|
1038
1058
|
? `[decidio] REFUSING to execute ${decisionId}: it was DENIED (${(await ctrl.store.getExecution(decisionId))?.verdict ?? "rejected"}) and that answer is final. Nothing ran. A denial cannot be reconciled or retried — if this action should now proceed, it needs a NEW request and a new approval.`
|
|
1039
|
-
: `[decidio] REFUSING to execute ${decisionId}: its durable state is ${claim.state} — invocation already began, so re-running it could duplicate a committed effect. Inspect it with guard.resume.recover(); settle it with guard.resume.reconcile("${decisionId}", …).`, claim.state === "discarded" ? " (This prints once. The decision is closed — no action is needed.)" : undefined);
|
|
1059
|
+
: `[decidio] REFUSING to execute ${decisionId}: its durable state is ${claim.state} — invocation already began, so re-running it could duplicate a committed effect. Inspect it with guard.resume.recover(); settle it with guard.resume.reconcile("${decisionId}", …).`, claim.state === "discarded" ? " (This prints once per worker process. The decision is closed — no action is needed.)" : undefined);
|
|
1040
1060
|
if (claim.state === "invoking" || claim.state === "indeterminate") {
|
|
1041
1061
|
return finish({ status: "indeterminate", decisionId, action: pending.action, reason: claim.state === "invoking"
|
|
1042
1062
|
? `this decision is in durable state invoking: its action may still be running, may have crashed before any effect, or may have finished without reporting - it may or may not have run. Nothing will retry it; check the worker and the downstream system, then settle it with reconcile("${decisionId}", …).`
|
|
@@ -1044,7 +1064,10 @@ export function createDecidioResume(opts) {
|
|
|
1044
1064
|
}
|
|
1045
1065
|
return finish({ status: "already_resolved", decisionId, action: pending.action, reason: claim.state === "discarded" ? `this decision was already DENIED (${(await ctrl.store.getExecution(decisionId))?.verdict ?? "rejected"}); that answer is final and this signal lost the race to it` : `this decision already ran (durable state ${claim.state}); this signal lost the race to it` });
|
|
1046
1066
|
}
|
|
1047
|
-
|
|
1067
|
+
const mapped = outcomeFromHead(await ctrl.store.getExecution(decisionId), decisionId, pending.action);
|
|
1068
|
+
if (mapped)
|
|
1069
|
+
return finish(mapped);
|
|
1070
|
+
return finish({ status: "lost_race", decisionId, action: pending.action, reason: `another attempt holds the pre-invocation claim on this decision${claim.state ? ` (durable state ${claim.state})` : ""}; this signal lost the race and did nothing. If that attempt is alive it will report; if it died, recover() lists the decision and reconcile() can settle it` });
|
|
1048
1071
|
}
|
|
1049
1072
|
try {
|
|
1050
1073
|
await ctrl.store.markInvoking(decisionId, attemptId);
|
|
@@ -1052,7 +1075,10 @@ export function createDecidioResume(opts) {
|
|
|
1052
1075
|
catch (e) {
|
|
1053
1076
|
await ctrl.store.releasePreinvoke(decisionId, attemptId).catch(() => { });
|
|
1054
1077
|
console.error(`[decidio] ${decisionId}: could not record the invocation boundary (${e?.message ?? e}) — refusing to run the action rather than run it unrecorded.`);
|
|
1055
|
-
|
|
1078
|
+
const mapped = outcomeFromHead(await ctrl.store.getExecution(decisionId), decisionId, pending.action);
|
|
1079
|
+
if (mapped)
|
|
1080
|
+
return finish(mapped);
|
|
1081
|
+
return finish({ status: "lost_race", decisionId, action: pending.action, reason: `could not durably record the invocation boundary (${e?.message ?? e}); nothing ran, the entry stays parked, and a later signal may retry` });
|
|
1056
1082
|
}
|
|
1057
1083
|
let result;
|
|
1058
1084
|
try {
|
|
@@ -1098,7 +1124,7 @@ export function createDecidioResume(opts) {
|
|
|
1098
1124
|
}
|
|
1099
1125
|
catch (e) {
|
|
1100
1126
|
res.statusCode = 500;
|
|
1101
|
-
res.end(JSON.stringify({ error: e
|
|
1127
|
+
res.end(JSON.stringify({ error: e instanceof DecidioStateCorruptError ? "DecidioStateCorruptError" : "resume failed", message: e?.message ?? String(e) }));
|
|
1102
1128
|
}
|
|
1103
1129
|
});
|
|
1104
1130
|
};
|
|
@@ -1121,7 +1147,7 @@ export function createDecidioResume(opts) {
|
|
|
1121
1147
|
return new Response(JSON.stringify(webhookBody(outcome)), { status: 200, headers: { "content-type": "application/json" } });
|
|
1122
1148
|
}
|
|
1123
1149
|
catch (e) {
|
|
1124
|
-
return new Response(JSON.stringify({ error: e
|
|
1150
|
+
return new Response(JSON.stringify({ error: e instanceof DecidioStateCorruptError ? "DecidioStateCorruptError" : "resume failed", message: e?.message ?? String(e) }), { status: 500 });
|
|
1125
1151
|
}
|
|
1126
1152
|
};
|
|
1127
1153
|
},
|
|
@@ -1294,12 +1320,23 @@ export function createDecidioResume(opts) {
|
|
|
1294
1320
|
return items;
|
|
1295
1321
|
},
|
|
1296
1322
|
worker(workerOpts) {
|
|
1323
|
+
const corruptWarned = new Set();
|
|
1324
|
+
let listWarned = false;
|
|
1297
1325
|
const interval = workerOpts?.intervalMs ?? 2_500;
|
|
1298
1326
|
const deadline = workerOpts?.timeoutMs ? Date.now() + workerOpts.timeoutMs : Infinity;
|
|
1299
1327
|
let stopped = false;
|
|
1300
1328
|
(async () => {
|
|
1301
1329
|
while (!stopped) {
|
|
1302
|
-
|
|
1330
|
+
let pendings = [];
|
|
1331
|
+
try {
|
|
1332
|
+
pendings = await ctrl.store.list();
|
|
1333
|
+
}
|
|
1334
|
+
catch (e) {
|
|
1335
|
+
if (!listWarned) {
|
|
1336
|
+
listWarned = true;
|
|
1337
|
+
console.error(`[decidio] the durable store could not be listed (${e?.message ?? e}) — this worker resumes nothing until it can be read again.`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1303
1340
|
for (const p of pendings) {
|
|
1304
1341
|
let st;
|
|
1305
1342
|
try {
|
|
@@ -1314,7 +1351,20 @@ export function createDecidioResume(opts) {
|
|
|
1314
1351
|
if (!verdict)
|
|
1315
1352
|
continue;
|
|
1316
1353
|
try {
|
|
1317
|
-
|
|
1354
|
+
let outcome;
|
|
1355
|
+
try {
|
|
1356
|
+
outcome = await ctrl.resolve({ decisionId: p.decisionId, verdict, reason: st.reason });
|
|
1357
|
+
}
|
|
1358
|
+
catch (e) {
|
|
1359
|
+
if (e instanceof DecidioStateCorruptError) {
|
|
1360
|
+
if (!corruptWarned.has(p.decisionId)) {
|
|
1361
|
+
corruptWarned.add(p.decisionId);
|
|
1362
|
+
console.error(`[decidio] LEDGER CORRUPT for ${p.decisionId}: ${e.message} — this worker will not touch it until the ledger is repaired; the other parked actions still resume.`);
|
|
1363
|
+
}
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
throw e;
|
|
1367
|
+
}
|
|
1318
1368
|
if (outcome.status === "executed" && outcome.reportRecorded)
|
|
1319
1369
|
console.error(`[decidio] resumed ${p.decisionId}: approved → "${p.action}" executed; evidence recorded as ${outcome.evidenceTier}${outcome.executionConfirmed ? "" : " (execution NOT confirmed — see the warning above)"}.`);
|
|
1320
1370
|
else if (outcome.status === "executed")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decidio/sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "One-line approval gate for AI-agent actions. Wrap a risky tool call with guard.protect(); Decidio's policy engine decides proceed | route | block, a human approves routed actions, and every outcome is sealed as a verifiable Authority Receipt.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"@langchain/langgraph": "^1.4.13",
|
|
76
76
|
"@temporalio/workflow": "^1.23.0",
|
|
77
77
|
"@types/node": "^22.20.1",
|
|
78
|
+
"tsx": "^4.23.13",
|
|
78
79
|
"typescript": "^5.9.3"
|
|
79
80
|
},
|
|
80
81
|
"scripts": {
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
"prepublishOnly": "npm run typecheck && npm run build && node ../publish-boundary/check.mjs --only @decidio/sdk"
|
|
84
85
|
},
|
|
85
86
|
"dependencies": {
|
|
86
|
-
"@decidio/verify": "0.4.
|
|
87
|
+
"@decidio/verify": "0.4.3"
|
|
87
88
|
},
|
|
88
89
|
"author": "OmniTwin Technologies Inc.",
|
|
89
90
|
"bugs": {
|