@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.js
CHANGED
|
@@ -78,9 +78,9 @@ export function verifyEvidence(rec, agentKey, webhookSecret) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
export const EXECUTION_STATES = [
|
|
81
|
-
"parked", "claimed_preinvoke", "invoking", "executed_unreported", "indeterminate", "reported",
|
|
81
|
+
"parked", "claimed_preinvoke", "discarded", "invoking", "executed_unreported", "indeterminate", "reported",
|
|
82
82
|
];
|
|
83
|
-
export const NON_REPLAYABLE = ["invoking", "executed_unreported", "indeterminate", "reported"];
|
|
83
|
+
export const NON_REPLAYABLE = ["discarded", "invoking", "executed_unreported", "indeterminate", "reported"];
|
|
84
84
|
export class DecidioStaleAttemptError extends Error {
|
|
85
85
|
decisionId;
|
|
86
86
|
owner;
|
|
@@ -101,16 +101,47 @@ export class DecidioAlreadySettledError extends Error {
|
|
|
101
101
|
this.name = "DecidioAlreadySettledError";
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
export class DecidioRecoveryUnavailableError extends Error {
|
|
105
|
+
cause;
|
|
106
|
+
constructor(cause) {
|
|
107
|
+
super(`[decidio] recover(): the execution ledger could not be read (${cause?.message ?? cause}). Refusing to return an empty recovery list, which would read as "nothing is outstanding" — inspect the store; some actions may be awaiting a report.`);
|
|
108
|
+
this.cause = cause;
|
|
109
|
+
this.name = "DecidioRecoveryUnavailableError";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export class DecidioDeniedError extends Error {
|
|
113
|
+
decisionId;
|
|
114
|
+
verdict;
|
|
115
|
+
constructor(decisionId, verdict) {
|
|
116
|
+
super(`[decidio] ${decisionId}: this decision was DENIED (${verdict}) and a denial is terminal — refusing a state write that would move it back toward execution. Nothing may advance out of a denial; if this action should now proceed it needs a NEW request and a new approval.`);
|
|
117
|
+
this.decisionId = decisionId;
|
|
118
|
+
this.verdict = verdict;
|
|
119
|
+
this.name = "DecidioDeniedError";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function assertNotDenied(id, cur, next) {
|
|
123
|
+
if (cur.state === "discarded" && next !== "discarded")
|
|
124
|
+
throw new DecidioDeniedError(id, cur.verdict ?? "rejected");
|
|
125
|
+
}
|
|
126
|
+
function assertClaimedBy(id, cur, attemptId) {
|
|
127
|
+
if (cur.state !== "claimed_preinvoke" || cur.attemptId !== attemptId) {
|
|
128
|
+
throw new Error(`[decidio] ${id}: refusing to cross the invocation boundary — attempt ${attemptId} does not hold a claim on this decision (state ${cur.state}, held by ${cur.attemptId || "nobody"}). A claim is the only way to acquire ownership; call claim() and act on its result.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
104
131
|
function assertNotSettled(id, cur, next) {
|
|
105
132
|
if (cur.reconciled?.outcome === "committed" && (next === "executed_unreported" || next === "indeterminate")) {
|
|
106
133
|
throw new DecidioAlreadySettledError(id);
|
|
107
134
|
}
|
|
108
135
|
}
|
|
109
136
|
export const SETTLE_OUTCOMES = ["committed", "not_committed"];
|
|
110
|
-
export function assertSettlable(id, state, outcome,
|
|
137
|
+
export function assertSettlable(id, state, outcome, opts = {}) {
|
|
138
|
+
const hasObservedEvidence = opts.hasObservedEvidence ?? true;
|
|
111
139
|
if (!SETTLE_OUTCOMES.includes(outcome)) {
|
|
112
140
|
throw new Error(`[decidio] ${id}: outcome must be "committed" or "not_committed", not ${JSON.stringify(outcome)}.`);
|
|
113
141
|
}
|
|
142
|
+
if (state === "discarded") {
|
|
143
|
+
throw new Error(`[decidio] ${id}: this decision was DENIED by a human or a policy, and a denial is final — it cannot be reconciled, released or retried. Nothing ran, so there is no outcome to record. If this action should now proceed it needs a NEW request and a new approval.`);
|
|
144
|
+
}
|
|
114
145
|
if (outcome === "committed") {
|
|
115
146
|
if (state === "reported")
|
|
116
147
|
throw new Error(`[decidio] ${id}: already reported — there is nothing left to reconcile.`);
|
|
@@ -126,6 +157,9 @@ export function assertSettlable(id, state, outcome, hasObservedEvidence = true)
|
|
|
126
157
|
throw new Error(`[decidio] ${id}: this decision holds CAPTURED EVIDENCE of an execution — releasing it would re-run a proven write. Re-send the evidence with reconfirm("${id}") instead.`);
|
|
127
158
|
if (state === "parked")
|
|
128
159
|
throw new Error(`[decidio] ${id}: already parked — nothing to release.`);
|
|
160
|
+
if (state === "invoking" && opts.workerStopped !== true) {
|
|
161
|
+
throw new Error(`[decidio] ${id}: this decision is still INVOKING — the worker that claimed it may be running the action right now, and releasing it would let a second worker run the same approved action. Stop or confirm the death of that worker first, then say so explicitly: reconcile("${id}", { outcome: "not_committed", reason: …, workerStopped: true }). If you cannot confirm it stopped, treat the decision as possibly executed and reconcile it as "committed" once you have checked the downstream system.`);
|
|
162
|
+
}
|
|
129
163
|
}
|
|
130
164
|
}
|
|
131
165
|
const rank = (s) => EXECUTION_STATES.indexOf(s);
|
|
@@ -135,11 +169,12 @@ export class MemoryPendingStore {
|
|
|
135
169
|
rec(id) {
|
|
136
170
|
return this.x.get(id) ?? { decisionId: id, state: "parked", attemptId: null, at: new Date().toISOString() };
|
|
137
171
|
}
|
|
138
|
-
advance(id, attemptId, state, patch = {}) {
|
|
172
|
+
advance(id, attemptId, state, patch = {}, takeover = false) {
|
|
139
173
|
const cur = this.rec(id);
|
|
140
|
-
if (cur.attemptId && cur.attemptId !== attemptId
|
|
174
|
+
if (!takeover && cur.attemptId && cur.attemptId !== attemptId) {
|
|
141
175
|
throw new DecidioStaleAttemptError(id, cur.attemptId, cur.state);
|
|
142
176
|
}
|
|
177
|
+
assertNotDenied(id, cur, state);
|
|
143
178
|
assertNotSettled(id, cur, state);
|
|
144
179
|
if (rank(state) < rank(cur.state)) {
|
|
145
180
|
throw new Error(`[decidio] ${id}: refusing to move ${cur.state} → ${state}; execution state only ever advances.`);
|
|
@@ -152,7 +187,15 @@ export class MemoryPendingStore {
|
|
|
152
187
|
this.x.set(p.decisionId, { decisionId: p.decisionId, state: "parked", attemptId: null, action: p.action, at: new Date().toISOString() });
|
|
153
188
|
}
|
|
154
189
|
async get(id) { return this.m.get(id) ?? null; }
|
|
155
|
-
async delete(id) {
|
|
190
|
+
async delete(id, attemptId) {
|
|
191
|
+
if (attemptId && !this.ownsPayload(id, attemptId))
|
|
192
|
+
return;
|
|
193
|
+
this.m.delete(id);
|
|
194
|
+
}
|
|
195
|
+
ownsPayload(id, attemptId) {
|
|
196
|
+
const owner = this.x.get(id)?.attemptId;
|
|
197
|
+
return !owner || owner === attemptId;
|
|
198
|
+
}
|
|
156
199
|
async list() { return [...this.m.values()].filter((p) => this.rec(p.decisionId).state === "parked"); }
|
|
157
200
|
async claim(id, attemptId) {
|
|
158
201
|
if (!this.m.has(id))
|
|
@@ -162,59 +205,87 @@ export class MemoryPendingStore {
|
|
|
162
205
|
return { ok: false, state: cur.state, reason: `already ${cur.state}` };
|
|
163
206
|
if (cur.state === "claimed_preinvoke")
|
|
164
207
|
return { ok: false, state: cur.state, reason: "claimed by another attempt" };
|
|
165
|
-
this.advance(id, attemptId, "claimed_preinvoke", { action: this.m.get(id)?.action });
|
|
208
|
+
this.advance(id, attemptId, "claimed_preinvoke", { action: this.m.get(id)?.action }, true);
|
|
166
209
|
return { ok: true };
|
|
167
210
|
}
|
|
168
|
-
async markInvoking(id, attemptId) {
|
|
211
|
+
async markInvoking(id, attemptId) {
|
|
212
|
+
const cur = this.rec(id);
|
|
213
|
+
assertNotDenied(id, cur, "invoking");
|
|
214
|
+
assertClaimedBy(id, cur, attemptId);
|
|
215
|
+
this.advance(id, attemptId, "invoking");
|
|
216
|
+
}
|
|
169
217
|
async recordExecuted(id, attemptId, evidence, evidenceSig, evidenceError) { this.advance(id, attemptId, "executed_unreported", { evidence, hasEvidence: true, evidenceSig, evidenceError }); }
|
|
170
218
|
async markIndeterminate(id, attemptId, error) { this.advance(id, attemptId, "indeterminate", { error }); }
|
|
171
219
|
async markReported(id, attemptId, confirmation) { this.advance(id, attemptId, "reported", { confirmation }); }
|
|
220
|
+
async markDiscarded(id, verdict, reason) { this.advance(id, this.x.get(id)?.attemptId ?? "", "discarded", { verdict, ...(reason ? { discardReason: reason } : {}) }); }
|
|
172
221
|
async getExecution(id) { return this.x.get(id) ?? null; }
|
|
173
222
|
async listExecutions() { return [...this.x.values()]; }
|
|
174
223
|
async settle(id, outcome, opts = {}) {
|
|
175
224
|
const cur = this.rec(id);
|
|
176
|
-
assertSettlable(id, cur.state, outcome, !cur.evidenceError);
|
|
225
|
+
assertSettlable(id, cur.state, outcome, { hasObservedEvidence: !cur.evidenceError, workerStopped: opts.workerStopped });
|
|
177
226
|
const at = new Date().toISOString();
|
|
178
|
-
const reconciled = { outcome, reason: opts.reason, at };
|
|
227
|
+
const reconciled = { outcome, reason: opts.reason, at, ...(opts.workerStopped ? { workerStopped: true } : {}) };
|
|
179
228
|
this.x.set(id, outcome === "committed"
|
|
180
229
|
? { ...cur, state: "executed_unreported", evidence: opts.evidence ?? null, hasEvidence: true, evidenceSig: opts.evidenceSig, evidenceError: undefined, reconciled, at }
|
|
181
230
|
: { ...cur, state: "parked", attemptId: null, error: undefined, reconciled, at });
|
|
182
231
|
}
|
|
183
232
|
async releasePreinvoke(id, attemptId) {
|
|
184
233
|
const cur = this.rec(id);
|
|
185
|
-
if (
|
|
186
|
-
throw new Error(`[decidio] ${id}: refusing to release
|
|
234
|
+
if (NON_REPLAYABLE.includes(cur.state))
|
|
235
|
+
throw new Error(`[decidio] ${id}: refusing to release this decision — its durable state is ${cur.state}, which is terminal or past the invocation boundary. releasePreinvoke only ever releases a claim nothing has acted on.`);
|
|
187
236
|
if (cur.attemptId && cur.attemptId !== attemptId)
|
|
188
237
|
return;
|
|
189
238
|
this.x.set(id, { ...cur, state: "parked", attemptId: null, at: new Date().toISOString() });
|
|
190
239
|
}
|
|
191
240
|
}
|
|
241
|
+
export function windowsPrincipal() {
|
|
242
|
+
const sysRoot = process.env.SystemRoot ?? process.env.windir ?? "C:\\Windows";
|
|
243
|
+
for (const exe of [join(sysRoot, "System32", "whoami.exe"), "whoami"]) {
|
|
244
|
+
try {
|
|
245
|
+
const out = execFileSync(exe, ["/user", "/fo", "csv", "/nh"], { encoding: "utf8" });
|
|
246
|
+
const m = out.match(/S-1-[0-9-]+/);
|
|
247
|
+
if (m)
|
|
248
|
+
return `*${m[0]}`;
|
|
249
|
+
}
|
|
250
|
+
catch { }
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
return userInfo().username;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return process.env.USERNAME ?? process.env.USER ?? "";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
export function hardenFile(file) {
|
|
260
|
+
if (process.platform !== "win32")
|
|
261
|
+
return null;
|
|
262
|
+
try {
|
|
263
|
+
const principal = windowsPrincipal();
|
|
264
|
+
if (!principal)
|
|
265
|
+
return "could not determine the current user";
|
|
266
|
+
execFileSync("icacls", [file, "/inheritance:r", "/grant:r", `${principal}:F`, "/grant:r", "*S-1-5-18:F"], { stdio: "ignore" });
|
|
267
|
+
try {
|
|
268
|
+
readFileSync(file, "utf8");
|
|
269
|
+
}
|
|
270
|
+
catch (verifyError) {
|
|
271
|
+
try {
|
|
272
|
+
execFileSync("icacls", [file, "/inheritance:e"], { stdio: "ignore" });
|
|
273
|
+
}
|
|
274
|
+
catch { }
|
|
275
|
+
return `the restriction applied but this process could no longer read the file (${verifyError?.message ?? verifyError}), so it was rolled back`;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
return String(e?.message ?? e);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
192
283
|
export function hardenDir(dir) {
|
|
193
284
|
if (process.platform !== "win32")
|
|
194
285
|
return;
|
|
195
286
|
const warn = (why) => console.warn(`[decidio] WARNING: could not restrict ${dir} to owner-only (${why}). It keeps the directory's inherited permissions, so other accounts on this machine may be able to read parked action arguments. Move the pending store to a protected location, or set DECIDIO_PENDING_DIR to one.`);
|
|
196
287
|
try {
|
|
197
|
-
const
|
|
198
|
-
const sysRoot = process.env.SystemRoot ?? process.env.windir ?? "C:\\Windows";
|
|
199
|
-
for (const exe of [join(sysRoot, "System32", "whoami.exe"), "whoami"]) {
|
|
200
|
-
try {
|
|
201
|
-
const out = execFileSync(exe, ["/user", "/fo", "csv", "/nh"], { encoding: "utf8" });
|
|
202
|
-
const m = out.match(/S-1-[0-9-]+/);
|
|
203
|
-
if (m)
|
|
204
|
-
return m[0];
|
|
205
|
-
}
|
|
206
|
-
catch { }
|
|
207
|
-
}
|
|
208
|
-
return null;
|
|
209
|
-
})();
|
|
210
|
-
const principal = sid ? `*${sid}` : (() => {
|
|
211
|
-
try {
|
|
212
|
-
return userInfo().username;
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
return process.env.USERNAME ?? process.env.USER ?? "";
|
|
216
|
-
}
|
|
217
|
-
})();
|
|
288
|
+
const principal = windowsPrincipal();
|
|
218
289
|
if (!principal)
|
|
219
290
|
throw new Error("could not determine the current user");
|
|
220
291
|
execFileSync("icacls", [dir, "/inheritance:r", "/grant:r", `${principal}:(OI)(CI)F`, "/grant:r", "*S-1-5-18:(OI)(CI)F"], { stdio: "ignore" });
|
|
@@ -271,6 +342,7 @@ export class FilePendingStore {
|
|
|
271
342
|
resolve(id) {
|
|
272
343
|
const corrupt = [];
|
|
273
344
|
let best = null;
|
|
345
|
+
let denial = null;
|
|
274
346
|
for (const s of EXECUTION_STATES) {
|
|
275
347
|
const f = this.statePath(id, s);
|
|
276
348
|
if (!existsSync(f))
|
|
@@ -279,21 +351,27 @@ export class FilePendingStore {
|
|
|
279
351
|
const r = JSON.parse(readFileSync(f, "utf8"));
|
|
280
352
|
if (!best || rank(s) >= rank(best.state))
|
|
281
353
|
best = { ...r, state: s };
|
|
354
|
+
if (s === "discarded")
|
|
355
|
+
denial = { ...r, state: "discarded" };
|
|
282
356
|
}
|
|
283
357
|
catch (e) {
|
|
284
358
|
corrupt.push(`${f}: ${e.message}`);
|
|
285
359
|
}
|
|
286
360
|
}
|
|
287
|
-
|
|
361
|
+
if (!denial)
|
|
362
|
+
return { rec: best, corrupt };
|
|
363
|
+
const ran = best && rank(best.state) >= rank("invoking") ? best : null;
|
|
364
|
+
return { rec: ran ? { ...denial, executionDespiteDenial: { state: ran.state, at: ran.at, hasEvidence: !!ran.hasEvidence } } : denial, corrupt };
|
|
288
365
|
}
|
|
289
366
|
setState(id, attemptId, state, patch = {}) {
|
|
290
367
|
const { rec, corrupt } = this.resolve(id);
|
|
291
368
|
if (corrupt.length)
|
|
292
369
|
throw new DecidioStateCorruptError(id, corrupt);
|
|
293
370
|
const cur = rec ?? { decisionId: id, state: "parked", attemptId: null, at: new Date().toISOString() };
|
|
294
|
-
if (cur.attemptId && cur.attemptId !== attemptId
|
|
371
|
+
if (cur.attemptId && cur.attemptId !== attemptId) {
|
|
295
372
|
throw new DecidioStaleAttemptError(id, cur.attemptId, cur.state);
|
|
296
373
|
}
|
|
374
|
+
assertNotDenied(id, cur, state);
|
|
297
375
|
assertNotSettled(id, cur, state);
|
|
298
376
|
if (rank(state) < rank(cur.state)) {
|
|
299
377
|
throw new Error(`[decidio] ${id}: refusing to move ${cur.state} → ${state}; execution state only ever advances.`);
|
|
@@ -323,11 +401,18 @@ export class FilePendingStore {
|
|
|
323
401
|
const f = this.path(id);
|
|
324
402
|
return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : null;
|
|
325
403
|
}
|
|
326
|
-
async delete(id) {
|
|
327
|
-
if (
|
|
328
|
-
|
|
404
|
+
async delete(id, attemptId) {
|
|
405
|
+
if (attemptId) {
|
|
406
|
+
const owner = this.resolve(id).rec?.attemptId;
|
|
407
|
+
if (owner && owner !== attemptId)
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
if (existsSync(this.path(id)))
|
|
412
|
+
rmSync(this.path(id));
|
|
413
|
+
}
|
|
414
|
+
catch { }
|
|
329
415
|
}
|
|
330
|
-
catch { } }
|
|
331
416
|
async list() {
|
|
332
417
|
if (!existsSync(this.dir))
|
|
333
418
|
return [];
|
|
@@ -360,7 +445,7 @@ export class FilePendingStore {
|
|
|
360
445
|
return { ok: false, state, reason: "claimed by another attempt" };
|
|
361
446
|
const target = this.statePath(id, "claimed_preinvoke");
|
|
362
447
|
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
363
|
-
const payload = JSON.stringify({ decisionId: id, state: "claimed_preinvoke", attemptId, action: rec?.action, at: new Date().toISOString() }, null, 2);
|
|
448
|
+
const payload = JSON.stringify({ ...(rec ?? {}), decisionId: id, state: "claimed_preinvoke", attemptId, action: rec?.action, at: new Date().toISOString() }, null, 2);
|
|
364
449
|
const fd = openSync(tmp, "w", 0o600);
|
|
365
450
|
try {
|
|
366
451
|
writeFileSync(fd, payload);
|
|
@@ -391,10 +476,19 @@ export class FilePendingStore {
|
|
|
391
476
|
catch { }
|
|
392
477
|
return { ok: true };
|
|
393
478
|
}
|
|
394
|
-
async markInvoking(id, attemptId) {
|
|
479
|
+
async markInvoking(id, attemptId) {
|
|
480
|
+
const { rec, corrupt } = this.resolve(id);
|
|
481
|
+
if (corrupt.length)
|
|
482
|
+
throw new DecidioStateCorruptError(id, corrupt);
|
|
483
|
+
const cur = rec ?? { decisionId: id, state: "parked", attemptId: null, at: new Date().toISOString() };
|
|
484
|
+
assertNotDenied(id, cur, "invoking");
|
|
485
|
+
assertClaimedBy(id, cur, attemptId);
|
|
486
|
+
this.setState(id, attemptId, "invoking");
|
|
487
|
+
}
|
|
395
488
|
async recordExecuted(id, attemptId, evidence, evidenceSig, evidenceError) { this.setState(id, attemptId, "executed_unreported", { evidence, hasEvidence: true, evidenceSig, evidenceError }); }
|
|
396
489
|
async markIndeterminate(id, attemptId, error) { this.setState(id, attemptId, "indeterminate", { error }); }
|
|
397
490
|
async markReported(id, attemptId, confirmation) { this.setState(id, attemptId, "reported", { confirmation }); }
|
|
491
|
+
async markDiscarded(id, verdict, reason) { this.setState(id, this.resolve(id).rec?.attemptId ?? "", "discarded", { verdict, ...(reason ? { discardReason: reason } : {}) }); }
|
|
398
492
|
async getExecution(id) {
|
|
399
493
|
const { rec, corrupt } = this.resolve(id);
|
|
400
494
|
if (corrupt.length)
|
|
@@ -431,11 +525,12 @@ export class FilePendingStore {
|
|
|
431
525
|
if (corrupt.length)
|
|
432
526
|
throw new DecidioStateCorruptError(id, corrupt);
|
|
433
527
|
const cur = rec ?? { decisionId: id, state: "parked", attemptId: null, at: new Date().toISOString() };
|
|
434
|
-
assertSettlable(id, cur.state, outcome, !cur.evidenceError);
|
|
528
|
+
assertSettlable(id, cur.state, outcome, { hasObservedEvidence: !cur.evidenceError, workerStopped: opts.workerStopped });
|
|
435
529
|
const at = new Date().toISOString();
|
|
530
|
+
const reconciled = { outcome, reason: opts.reason, at, ...(opts.workerStopped ? { workerStopped: true } : {}) };
|
|
436
531
|
const next = outcome === "committed"
|
|
437
|
-
? { ...cur, state: "executed_unreported", evidence: opts.evidence ?? null, hasEvidence: true, evidenceSig: opts.evidenceSig, evidenceError: undefined, reconciled
|
|
438
|
-
: { ...cur, state: "parked", attemptId: null, error: undefined, reconciled
|
|
532
|
+
? { ...cur, state: "executed_unreported", evidence: opts.evidence ?? null, hasEvidence: true, evidenceSig: opts.evidenceSig, evidenceError: undefined, reconciled, at }
|
|
533
|
+
: { ...cur, state: "parked", attemptId: null, error: undefined, reconciled, at };
|
|
439
534
|
this.writeAtomic(this.statePath(id, next.state), JSON.stringify(next, null, 2));
|
|
440
535
|
for (const st of EXECUTION_STATES) {
|
|
441
536
|
if (st === next.state)
|
|
@@ -451,11 +546,11 @@ export class FilePendingStore {
|
|
|
451
546
|
async releasePreinvoke(id, attemptId) {
|
|
452
547
|
const { rec } = this.resolve(id);
|
|
453
548
|
const state = rec?.state ?? "parked";
|
|
454
|
-
if (
|
|
455
|
-
throw new Error(`[decidio] ${id}: refusing to release
|
|
549
|
+
if (NON_REPLAYABLE.includes(state))
|
|
550
|
+
throw new Error(`[decidio] ${id}: refusing to release this decision — its durable state is ${state}, which is terminal or past the invocation boundary. releasePreinvoke only ever releases a claim nothing has acted on.`);
|
|
456
551
|
if (rec?.attemptId && rec.attemptId !== attemptId)
|
|
457
552
|
return;
|
|
458
|
-
this.writeAtomic(this.statePath(id, "parked"), JSON.stringify({ decisionId: id, state: "parked", attemptId: null, action: rec?.action, at: new Date().toISOString() }, null, 2));
|
|
553
|
+
this.writeAtomic(this.statePath(id, "parked"), JSON.stringify({ ...(rec ?? {}), decisionId: id, state: "parked", attemptId: null, action: rec?.action, at: new Date().toISOString() }, null, 2));
|
|
459
554
|
try {
|
|
460
555
|
const c = this.statePath(id, "claimed_preinvoke");
|
|
461
556
|
if (existsSync(c))
|
|
@@ -472,6 +567,11 @@ export class FilePendingStore {
|
|
|
472
567
|
const key = name.slice(0, -".claimed".length);
|
|
473
568
|
try {
|
|
474
569
|
const p = JSON.parse(readFileSync(join(this.dir, name), "utf8"));
|
|
570
|
+
const existing = this.resolve(p.decisionId).rec;
|
|
571
|
+
if (existing && NON_REPLAYABLE.includes(existing.state)) {
|
|
572
|
+
renameSync(join(this.dir, name), join(this.dir, `${name}.migrated`));
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
475
575
|
this.writeAtomic(this.statePath(p.decisionId, "indeterminate"), JSON.stringify({ decisionId: p.decisionId, state: "indeterminate", attemptId: null, action: p.action, error: "upgraded from 0.1.x while claimed — whether it ran is not knowable; reconcile it", at: new Date().toISOString() }, null, 2));
|
|
476
576
|
this.writeAtomic(this.path(p.decisionId), JSON.stringify(p, null, 2));
|
|
477
577
|
rmSync(join(this.dir, name));
|
|
@@ -518,6 +618,11 @@ export class FilePendingStore {
|
|
|
518
618
|
renameSync(file, `${file}.migrated`);
|
|
519
619
|
continue;
|
|
520
620
|
}
|
|
621
|
+
if (existing?.state === "discarded") {
|
|
622
|
+
renameSync(file, `${file}.migrated`);
|
|
623
|
+
console.error(`[decidio] ${e.decisionId}: a 0.1.x execution report was found for a decision that was DENIED (${existing.verdict ?? "rejected"}). It has NOT been added to the ledger — a denial is terminal and this report will not be sent. The file is kept as ${file}.migrated: if that action really ran, check the downstream system and correct it there.`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
521
626
|
this.writeAtomic(this.statePath(e.decisionId, "executed_unreported"), JSON.stringify({
|
|
522
627
|
decisionId: e.decisionId, state: "executed_unreported", attemptId: null, action: e.action,
|
|
523
628
|
evidence: e.reported, hasEvidence: true, migratedFrom: "0.1.x report journal",
|
|
@@ -615,6 +720,7 @@ async function statusOf(cfg, decisionId) {
|
|
|
615
720
|
const V2_STORE_METHODS = [
|
|
616
721
|
"put", "get", "delete", "list",
|
|
617
722
|
"claim", "markInvoking", "recordExecuted", "markIndeterminate", "markReported",
|
|
723
|
+
"markDiscarded",
|
|
618
724
|
"getExecution", "listExecutions", "releasePreinvoke", "settle",
|
|
619
725
|
];
|
|
620
726
|
function assertStoreV2(store) {
|
|
@@ -622,7 +728,7 @@ function assertStoreV2(store) {
|
|
|
622
728
|
if (!missing.length)
|
|
623
729
|
return;
|
|
624
730
|
throw new Error(`[decidio] this PendingStore is missing ${missing.length} required method${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}.\n` +
|
|
625
|
-
`
|
|
731
|
+
` The durable execution state machine is MANDATORY (parked → claimed_preinvoke → invoking → executed_unreported | indeterminate → reported, plus the terminal "discarded" that a denial writes).\n` +
|
|
626
732
|
` The 0.1.x contract left claim() optional and fell back to a process-local guard, which cannot survive a restart — so it never carried the single-execution guarantee it appeared to. A store that cannot record that invocation BEGAN cannot prevent a committed action from running twice.\n` +
|
|
627
733
|
` Fix: implement the missing methods (see the PendingStore docs), or use the shipped FilePendingStore / MemoryPendingStore. Nothing was started.`);
|
|
628
734
|
}
|
|
@@ -679,7 +785,7 @@ export function createDecidioResume(opts) {
|
|
|
679
785
|
if (e instanceof DecidioStaleAttemptError) {
|
|
680
786
|
console.warn(`[decidio] ${decisionId}: this action finished, but the decision is now owned by a newer attempt — it was released for retry while this one was still running. This attempt's report is NOT being sent; the owning attempt reports. Your action DID run: if that means it ran twice, the release was premature.`);
|
|
681
787
|
try {
|
|
682
|
-
await ctrl.store.delete(decisionId);
|
|
788
|
+
await ctrl.store.delete(decisionId, attemptId);
|
|
683
789
|
}
|
|
684
790
|
catch { }
|
|
685
791
|
return { ...base, reportRecorded: false, executionConfirmed: false, confirmationRetryable: false, reportJournalled: false, diagnostic: `${decisionId} was released to a newer attempt while this one was running; this attempt's report was not sent. If your action is not idempotent, note that it has now run more than once.` };
|
|
@@ -687,11 +793,15 @@ export function createDecidioResume(opts) {
|
|
|
687
793
|
if (e instanceof DecidioAlreadySettledError) {
|
|
688
794
|
console.warn(`[decidio] ${decisionId}: this action finished, but an operator had already reconciled the decision as committed and Decidio has that report. This attempt's own report is NOT being sent — one decision, one report. If your result differs from what was reconciled, correct it in Decidio; do NOT re-run the action.`);
|
|
689
795
|
try {
|
|
690
|
-
await ctrl.store.delete(decisionId);
|
|
796
|
+
await ctrl.store.delete(decisionId, attemptId);
|
|
691
797
|
}
|
|
692
798
|
catch { }
|
|
693
799
|
return { ...base, reportRecorded: true, executionConfirmed: false, reportJournalled: true, diagnostic: `an operator settled ${decisionId} as committed while this attempt was running; their report stands and this one was not sent.` };
|
|
694
800
|
}
|
|
801
|
+
if (e instanceof DecidioDeniedError) {
|
|
802
|
+
console.error(`[decidio] ${decisionId}: this decision is DENIED, so no execution report will be sent. If your action nevertheless ran, your store let a refused decision reach the handler — treat that as a defect in the store, and correct the downstream effect by hand.`);
|
|
803
|
+
return { ...base, reportRecorded: false, executionConfirmed: false, confirmationRetryable: false, reportJournalled: false, diagnostic: `${decisionId} is denied; no report was sent.` };
|
|
804
|
+
}
|
|
695
805
|
console.error(`[decidio] ${decisionId}: the action RAN but its execution report could NOT be recorded (${e?.message ?? e}). If the report also fails to send, this execution is NOT locally recoverable — record the outcome from your own system.`);
|
|
696
806
|
}
|
|
697
807
|
}
|
|
@@ -704,7 +814,7 @@ export function createDecidioResume(opts) {
|
|
|
704
814
|
}
|
|
705
815
|
const deletePending = async () => {
|
|
706
816
|
try {
|
|
707
|
-
await ctrl.store.delete(decisionId);
|
|
817
|
+
await ctrl.store.delete(decisionId, attemptId);
|
|
708
818
|
}
|
|
709
819
|
catch (e) {
|
|
710
820
|
console.warn(`[decidio] ${decisionId}: the parked payload could not be removed (${e?.message ?? e}). This is cleanup only — the execution ledger already records this decision as executed, so no process can run it again; remove the row at your convenience.`);
|
|
@@ -743,11 +853,11 @@ export function createDecidioResume(opts) {
|
|
|
743
853
|
return { ...base, reportRecorded: ev.reportRecorded, executionConfirmed: ev.executionConfirmed, ...(ev.evidenceTier ? { evidenceTier: ev.evidenceTier } : {}), ...(ev.executionReportVerified !== undefined ? { executionReportVerified: ev.executionReportVerified } : {}), ...(ev.executionReportReason ? { executionReportReason: ev.executionReportReason } : {}), ...(ev.confirmationRetryable !== undefined ? { confirmationRetryable: ev.confirmationRetryable } : {}), reportJournalled: journalled, ...(buildProblem ? { evidenceError: buildProblem } : {}), ...(diagnostic ? { diagnostic } : {}) };
|
|
744
854
|
}
|
|
745
855
|
const refusalWarned = new Set();
|
|
746
|
-
const refuseOnce = (decisionId, msg) => {
|
|
856
|
+
const refuseOnce = (decisionId, msg, tail) => {
|
|
747
857
|
if (refusalWarned.has(decisionId))
|
|
748
858
|
return;
|
|
749
859
|
refusalWarned.add(decisionId);
|
|
750
|
-
console.error(msg + " (This prints once — the entry stays parked until you act.)");
|
|
860
|
+
console.error(msg + (tail ?? " (This prints once — the entry stays parked until you act.)"));
|
|
751
861
|
};
|
|
752
862
|
const ctrl = {
|
|
753
863
|
...opts,
|
|
@@ -783,13 +893,10 @@ export function createDecidioResume(opts) {
|
|
|
783
893
|
const pending = await ctrl.store.get(decisionId);
|
|
784
894
|
if (!pending)
|
|
785
895
|
return { status: "unknown", decisionId };
|
|
786
|
-
if (verdict === "rejected") {
|
|
896
|
+
if (verdict === "rejected" || verdict === "blocked") {
|
|
897
|
+
await ctrl.store.markDiscarded(decisionId, verdict, reason);
|
|
787
898
|
await ctrl.store.delete(decisionId);
|
|
788
|
-
return finish({ status:
|
|
789
|
-
}
|
|
790
|
-
if (verdict === "blocked") {
|
|
791
|
-
await ctrl.store.delete(decisionId);
|
|
792
|
-
return finish({ status: "blocked", decisionId, action: pending.action, reason });
|
|
899
|
+
return finish({ status: verdict, decisionId, action: pending.action, reason });
|
|
793
900
|
}
|
|
794
901
|
if (verdict !== "approved" && verdict !== "auto_approved")
|
|
795
902
|
return { status: "unknown", decisionId };
|
|
@@ -827,7 +934,9 @@ export function createDecidioResume(opts) {
|
|
|
827
934
|
const claim = await ctrl.store.claim(decisionId, attemptId);
|
|
828
935
|
if (!claim.ok) {
|
|
829
936
|
if (claim.state && NON_REPLAYABLE.includes(claim.state)) {
|
|
830
|
-
refuseOnce(decisionId,
|
|
937
|
+
refuseOnce(decisionId, claim.state === "discarded"
|
|
938
|
+
? `[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.`
|
|
939
|
+
: `[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);
|
|
831
940
|
return finish({ status: claim.state === "indeterminate" ? "indeterminate" : "unknown", decisionId, action: pending.action, reason: claim.reason });
|
|
832
941
|
}
|
|
833
942
|
return { status: "unknown", decisionId };
|
|
@@ -933,7 +1042,7 @@ export function createDecidioResume(opts) {
|
|
|
933
1042
|
? signEvidence(decisionId, entry.attemptId ?? null, settleEvidence, ctrl.agentKey, ctrl.webhookSecret)
|
|
934
1043
|
: undefined;
|
|
935
1044
|
try {
|
|
936
|
-
await ctrl.store.settle(decisionId, opts.outcome, { evidence: settleEvidence, reason: opts.reason, evidenceSig: settleSig });
|
|
1045
|
+
await ctrl.store.settle(decisionId, opts.outcome, { evidence: settleEvidence, reason: opts.reason, evidenceSig: settleSig, workerStopped: opts.workerStopped });
|
|
937
1046
|
}
|
|
938
1047
|
catch (e) {
|
|
939
1048
|
return { reportRecorded: false, executionConfirmed: false, diagnostic: `${e?.message ?? e}` };
|
|
@@ -977,6 +1086,14 @@ export function createDecidioResume(opts) {
|
|
|
977
1086
|
if (entry?.state === "indeterminate") {
|
|
978
1087
|
return { reportRecorded: false, executionConfirmed: false, diagnostic: `${decisionId} is INDETERMINATE: it was invoked and its outcome is unknown, so there is no captured result to re-send. Settle it with reconcile("${decisionId}", …) once you have checked the downstream system.` };
|
|
979
1088
|
}
|
|
1089
|
+
if (entry?.state === "discarded") {
|
|
1090
|
+
return {
|
|
1091
|
+
reportRecorded: false, executionConfirmed: false,
|
|
1092
|
+
diagnostic: entry.executionDespiteDenial
|
|
1093
|
+
? `${decisionId} was DENIED (${entry.verdict ?? "rejected"}), and this store ALSO holds an execution marker for it (${entry.executionDespiteDenial.state} at ${entry.executionDespiteDenial.at}). A denial is terminal, so no report will be sent for it — but something may have run. Check the downstream system and correct it there; do NOT re-run the action.`
|
|
1094
|
+
: `${decisionId} was DENIED (${entry.verdict ?? "rejected"}) and nothing ran, so there is no execution report to send. A denial cannot be reconciled or retried.`,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
980
1097
|
if (!entry || !entry.hasEvidence) {
|
|
981
1098
|
return {
|
|
982
1099
|
reportRecorded: false, executionConfirmed: false,
|
|
@@ -1018,12 +1135,13 @@ export function createDecidioResume(opts) {
|
|
|
1018
1135
|
},
|
|
1019
1136
|
async recover() {
|
|
1020
1137
|
const items = [];
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1138
|
+
let execs;
|
|
1139
|
+
try {
|
|
1140
|
+
execs = await ctrl.store.listExecutions();
|
|
1141
|
+
}
|
|
1142
|
+
catch (e) {
|
|
1143
|
+
throw new DecidioRecoveryUnavailableError(e);
|
|
1144
|
+
}
|
|
1027
1145
|
for (const r of execs) {
|
|
1028
1146
|
const a = r.action ?? "(unknown action)";
|
|
1029
1147
|
switch (r.state) {
|
|
@@ -1037,7 +1155,7 @@ export function createDecidioResume(opts) {
|
|
|
1037
1155
|
break;
|
|
1038
1156
|
case "invoking":
|
|
1039
1157
|
items.push({ decisionId: r.decisionId, action: r.action, state: "indeterminate",
|
|
1040
|
-
guidance: `"${a}" was invoked at ${r.at} and
|
|
1158
|
+
guidance: `"${a}" was invoked at ${r.at} by attempt ${r.attemptId ?? "(unknown)"} and no outcome has been recorded since. That does NOT mean the worker died — it may still be running this action. It will not be retried automatically. Check that worker first: if it is gone, settle it with { outcome: "committed", evidence: … } once you have checked the downstream system, or release it with { outcome: "not_committed", reason: …, workerStopped: true }.` });
|
|
1041
1159
|
break;
|
|
1042
1160
|
case "claimed_preinvoke":
|
|
1043
1161
|
items.push({ decisionId: r.decisionId, action: r.action, state: "claimed_stalled",
|
|
@@ -1046,6 +1164,12 @@ export function createDecidioResume(opts) {
|
|
|
1046
1164
|
case "parked":
|
|
1047
1165
|
items.push({ decisionId: r.decisionId, action: r.action, state: "parked", guidance: `"${a}" is parked awaiting a human decision. Nothing has executed.` });
|
|
1048
1166
|
break;
|
|
1167
|
+
case "discarded":
|
|
1168
|
+
if (r.executionDespiteDenial) {
|
|
1169
|
+
items.push({ decisionId: r.decisionId, action: r.action, state: "denied_with_evidence",
|
|
1170
|
+
guidance: `"${a}" was DENIED (${r.verdict ?? "rejected"})${r.discardReason ? ` — ${r.discardReason}` : ""}, but this store also holds an execution marker for it (${r.executionDespiteDenial.state} at ${r.executionDespiteDenial.at}${r.executionDespiteDenial.hasEvidence ? ", with a captured result" : ""}). Nothing will run it and nothing will report it — a denial is terminal and cannot be reconciled. CHECK THE DOWNSTREAM SYSTEM: if the action really ran, correct it there.` });
|
|
1171
|
+
}
|
|
1172
|
+
break;
|
|
1049
1173
|
case "reported":
|
|
1050
1174
|
break;
|
|
1051
1175
|
}
|
package/dist/signing.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
/** Sorted-key JCS — must match apps/api/src/canonical.ts byte-for-byte.
|
|
1
|
+
/** Sorted-key JCS — must match apps/api/src/canonical.ts byte-for-byte.
|
|
2
|
+
*
|
|
3
|
+
* ONE IMPLEMENTATION (F-006). This was a module-local copy that reached `JSON.stringify` for every
|
|
4
|
+
* primitive, so it applied NONE of the strict rules the exported canonicalizer enforces — most
|
|
5
|
+
* sharply, it emitted an escaped lone surrogate instead of refusing it. The bytes a bound proof
|
|
6
|
+
* SIGNS and the bytes the digest BINDS are the two places that must never disagree with the value
|
|
7
|
+
* model, and they were the two places that did. Python already delegated (`signing.canonicalize` →
|
|
8
|
+
* `canonical_json`); this is the twin-parity repair on the TypeScript side. */
|
|
2
9
|
export declare function canonicalize(v: unknown): string;
|
|
3
10
|
/** Generate a fresh Ed25519 keypair LOCALLY. Returns the public `did:key` (register this)
|
|
4
11
|
* and the PKCS8 private key base64 (keep this in the agent host's env as DECIDIO_AGENT_KEY —
|
package/dist/signing.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateKeyPairSync, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { canonicalJson } from "./canonical.js";
|
|
2
3
|
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
3
4
|
function b58encode(buf) {
|
|
4
5
|
const digits = [0];
|
|
@@ -26,12 +27,7 @@ function b58encode(buf) {
|
|
|
26
27
|
return str;
|
|
27
28
|
}
|
|
28
29
|
export function canonicalize(v) {
|
|
29
|
-
|
|
30
|
-
return JSON.stringify(v);
|
|
31
|
-
if (Array.isArray(v))
|
|
32
|
-
return `[${v.map(canonicalize).join(",")}]`;
|
|
33
|
-
const o = v;
|
|
34
|
-
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
|
|
30
|
+
return canonicalJson(v, "canonicalize(...)");
|
|
35
31
|
}
|
|
36
32
|
export function didFromPrivateKey(privateKeyBase64) {
|
|
37
33
|
const priv = createPrivateKey({ key: Buffer.from(privateKeyBase64, "base64"), format: "der", type: "pkcs8" });
|
|
@@ -111,20 +107,6 @@ export function assertJsonSafe(value, path = "args") {
|
|
|
111
107
|
walk(value, path);
|
|
112
108
|
}
|
|
113
109
|
export function assertSafeTransport(url, hasCredential) {
|
|
114
|
-
if (!hasCredential) {
|
|
115
|
-
let probe = null;
|
|
116
|
-
try {
|
|
117
|
-
probe = new URL(url);
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
probe = null;
|
|
121
|
-
}
|
|
122
|
-
if (probe && (probe.username || probe.password)) {
|
|
123
|
-
throw new Error(`[decidio] the gate URL carries credentials in the URL itself (${probe.protocol}//user:***@${probe.host}). Decidio authenticates with a bearer token, so this is either a mistake or a secret you did not mean to put on the wire — and it is sent on every request regardless of whether an api token is configured. Remove the user:password from the URL. Nothing was sent.`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (!hasCredential)
|
|
127
|
-
return;
|
|
128
110
|
let u;
|
|
129
111
|
try {
|
|
130
112
|
u = new URL(url);
|
|
@@ -132,9 +114,9 @@ export function assertSafeTransport(url, hasCredential) {
|
|
|
132
114
|
catch {
|
|
133
115
|
const spelled = url.replace(/[\t\r\n]/g, "").replace(/^[\s\u0000-\u001f\u0085\ufeff]+/, "");
|
|
134
116
|
if (/^http:/i.test(spelled)) {
|
|
135
|
-
throw new Error(`[decidio] refusing to send credentials over plaintext http to ${JSON.stringify(url)} — that is not a complete absolute URL, so where the request would actually connect is undefined (the path is appended to it, which can turn it into a remote host). Set a full https URL, or http://localhost for local dev. Nothing was sent.`);
|
|
117
|
+
throw new Error(`[decidio] refusing to ${hasCredential ? "send credentials" : "send a request"} over plaintext http to ${JSON.stringify(url)} — that is not a complete absolute URL, so where the request would actually connect is undefined (the path is appended to it, which can turn it into a remote host). Set a full https URL, or http://localhost for local dev. Nothing was sent.`);
|
|
136
118
|
}
|
|
137
|
-
|
|
119
|
+
throw new Error(`[decidio] the gate URL ${JSON.stringify(url)} is not an absolute URL, so there is no host to send to — where a request would actually connect depends entirely on whichever transport resolves it. Decidio needs a full https URL (or http://localhost for local dev). Nothing was sent.`);
|
|
138
120
|
}
|
|
139
121
|
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
140
122
|
throw new Error(`[decidio] the gate URL uses the ${u.protocol.replace(":", "")} scheme, which is not a Decidio endpoint — use https (or http://localhost for local dev). Nothing was sent.`);
|
|
@@ -148,7 +130,9 @@ export function assertSafeTransport(url, hasCredential) {
|
|
|
148
130
|
const local = host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
149
131
|
if (local)
|
|
150
132
|
return;
|
|
151
|
-
throw new Error(
|
|
133
|
+
throw new Error(hasCredential
|
|
134
|
+
? `[decidio] refusing to send credentials over plaintext http to ${u.origin} — use https, or localhost for local dev. Nothing was sent.`
|
|
135
|
+
: `[decidio] refusing to talk to ${u.origin} over plaintext http — Decidio is https, and an unauthenticated plaintext reply cannot be trusted to carry an authority decision. Use https, or localhost for local dev. Nothing was sent.`);
|
|
152
136
|
}
|
|
153
137
|
export const carriesCredential = (c) => !!c.apiToken || !!(c.agentKey && c.agentDid);
|
|
154
138
|
export function signExecutionReport(privateKeyBase64, fields) {
|