@decidio/sdk 0.1.12 → 0.2.1
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 +52 -15
- package/dist/adapters/_gate.d.ts +8 -2
- package/dist/adapters/_gate.js +16 -7
- package/dist/adapters/langgraph.d.ts +3 -1
- package/dist/adapters/langgraph.js +2 -2
- package/dist/adapters/openai.d.ts +7 -2
- package/dist/adapters/openai.js +11 -3
- package/dist/adapters/temporal.d.ts +6 -2
- package/dist/adapters/temporal.js +3 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/canonical.js +8 -4
- package/dist/cli.js +68 -35
- package/dist/errors.d.ts +12 -5
- package/dist/errors.js +37 -5
- package/dist/evidence.d.ts +19 -0
- package/dist/evidence.js +7 -4
- package/dist/guard.d.ts +19 -2
- package/dist/guard.js +7 -5
- package/dist/index.d.ts +27 -2
- package/dist/index.js +32 -12
- package/dist/inngest.d.ts +4 -0
- package/dist/inngest.js +1 -1
- package/dist/resume.d.ts +331 -57
- package/dist/resume.js +678 -139
- package/dist/signing.js +19 -1
- package/openapi.yaml +89 -6
- package/package.json +15 -9
package/README.md
CHANGED
|
@@ -90,8 +90,9 @@ const payInvoice = guard.protect(
|
|
|
90
90
|
{ mode: "blocking" }, // blocking = watch the whole loop live in one terminal sitting.
|
|
91
91
|
); // Production agents use durable mode instead — see below.
|
|
92
92
|
|
|
93
|
-
const
|
|
94
|
-
console.log("executed after approval:",
|
|
93
|
+
const { value, confirmation } = await payInvoice({ id: "INV-2026-001", amount: 86_000 });
|
|
94
|
+
console.log("executed after approval:", value);
|
|
95
|
+
console.log("Decidio recorded it as:", confirmation.evidenceTier); // e.g. application_confirmed
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
**4. Trigger it:**
|
|
@@ -200,7 +201,7 @@ That's the whole contract: gate → human decision → your execution → owned,
|
|
|
200
201
|
- **proceed** — your function runs immediately (auto-approved under a named, versioned rule).
|
|
201
202
|
- **route** — the action suspends (durable) or waits (blocking) for a human decision in Decidio's queue, then runs your function (or throws `DecidioRejectedError`).
|
|
202
203
|
- **block** — the wrapper throws `DecidioBlockedError`; your function never runs.
|
|
203
|
-
3. After your function runs, the wrapper reports the **real response it captured** back to Decidio
|
|
204
|
+
3. After your function runs, the wrapper reports the **real response it captured** back to Decidio (which minimizes + tokenizes before storing — the immutable record never stores raw payloads). The tier that report earns depends on what it can prove: `application_confirmed` when the agent has key material and signs the execution report, `agent_asserted` when it does not, and `execution_unconfirmed` when the reported value carries no usable evidence. The SDK tells you which it got; it never claims the top tier by default.
|
|
204
205
|
|
|
205
206
|
Your agent executes its own action. **Decidio holds no write credentials for your system** — it authorizes the decision, records it, and (optionally) independently verifies it.
|
|
206
207
|
|
|
@@ -313,13 +314,42 @@ canonicalization preserves **value** semantics, not JavaScript object identity.
|
|
|
313
314
|
pointing at the same object arrive as two equal objects, and mutating an argument inside your
|
|
314
315
|
function does not affect the caller's copy.
|
|
315
316
|
|
|
316
|
-
**What the SDK can and cannot promise about duplicates.**
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
317
|
+
**What the SDK can and cannot promise about duplicates.** The guarantee, stated exactly:
|
|
318
|
+
|
|
319
|
+
> Once invocation may have begun, the SDK never automatically invokes it again unless the
|
|
320
|
+
> downstream system provides an idempotency guarantee or an operator explicitly reconciles it.
|
|
321
|
+
|
|
322
|
+
It holds across processes and restarts, not just within one worker. Before your function is
|
|
323
|
+
called the SDK writes a durable `invoking` marker to the pending store; from that moment no
|
|
324
|
+
poll, no duplicate webhook, no fresh process and no worker cycle will call it again. Deletion of
|
|
325
|
+
the parked row is cleanup, not the guarantee — a store whose `delete()` silently fails cannot
|
|
326
|
+
cause a second execution.
|
|
327
|
+
|
|
328
|
+
**Closing the last window yourself.** Pass `withContext: true` and your action receives an
|
|
329
|
+
`ExecutionContext` as its first argument:
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
const payInvoice = guard.protect(
|
|
333
|
+
async (ctx, invoice) => stripe.paymentIntents.create(
|
|
334
|
+
{ amount: invoice.amount, currency: "usd" },
|
|
335
|
+
{ idempotencyKey: ctx.idempotencyKey }, // ← stable across every attempt at this decision
|
|
336
|
+
),
|
|
337
|
+
(invoice) => ({ action: "payInvoice", amount: invoice.amount }),
|
|
338
|
+
{ withContext: true },
|
|
339
|
+
);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`ctx` is `{ decisionId, attemptId, idempotencyKey }`. It is opt-in rather than inferred from your
|
|
343
|
+
function's arity, because guessing wrong would hand a payment call a context object where it
|
|
344
|
+
expected an invoice. `describe` still receives your arguments only — it describes the *request*,
|
|
345
|
+
while the context describes the *execution*.
|
|
346
|
+
|
|
347
|
+
It still cannot make an external side effect exactly-once — no client can. If your action
|
|
348
|
+
succeeds and the process dies before the result is recorded, whether the downstream write
|
|
349
|
+
committed is not knowable from this machine. The difference is what the SDK does about it: that
|
|
350
|
+
decision becomes **`indeterminate`** and stops, rather than being retried into a duplicate
|
|
351
|
+
payment. You settle it with `reconcile()` once you have looked downstream. Pass the decision id
|
|
352
|
+
as your vendor's idempotency key and that window closes on their side too.
|
|
323
353
|
|
|
324
354
|
Recovery is explicit rather than magical:
|
|
325
355
|
|
|
@@ -332,9 +362,12 @@ for (const item of await guard.resume.recover()) {
|
|
|
332
362
|
// "parked" → still waiting on a human; nothing has executed
|
|
333
363
|
```
|
|
334
364
|
|
|
335
|
-
The report a worker sends is
|
|
336
|
-
recoverable; and because
|
|
337
|
-
evidence — never re-run your function.
|
|
365
|
+
The report a worker sends is written to the pending store's execution ledger *before* the first
|
|
366
|
+
send attempt, so a lost confirmation is recoverable; and because that record holds no handler
|
|
367
|
+
binding, replaying it can only re-send evidence — never re-run your function. The ledger lives in
|
|
368
|
+
the store you configured, so two workers sharing one Redis or Postgres store can reconfirm each
|
|
369
|
+
other's work. (Through 0.1.x this was a separate local-filesystem journal, which meant two
|
|
370
|
+
workers kept two disjoint journals and neither could. `reportDir` is accepted and ignored now.)
|
|
338
371
|
|
|
339
372
|
**Parked arguments must be JSON-serializable.** A suspended action is signed and then written to
|
|
340
373
|
disk as JSON, so values that don't survive `JSON.stringify` round-tripping — `Date`, `Map`,
|
|
@@ -380,6 +413,10 @@ for engines you don't. Each adapter's shipped `.d.ts` header documents its wirin
|
|
|
380
413
|
`npx @decidio/sdk <cmd>`:
|
|
381
414
|
|
|
382
415
|
- `quickstart` — the guided two-minute tour (gate → approve → execute → proof)
|
|
416
|
+
- `--auto-approve-demo` approves the tour's own synthetic decision so it can run headless; it
|
|
417
|
+
then CLEANS UP the tour agent and token automatically. Pass `--keep-agent` to keep them.
|
|
418
|
+
- An interactive run keeps the agent by default and offers cleanup at the end;
|
|
419
|
+
`quickstart cleanup` works later too.
|
|
383
420
|
- `init [agentId] [--unique] [--force] [--expires-days N]` — sign in, register, mint the agent token, write `.env` (+ `.gitignore` entries). Refuses to overwrite a *different* agent's key in this directory; `--force` backs the old `.env` up first.
|
|
384
421
|
- `doctor` — config, connectivity, and what your current token can do
|
|
385
422
|
- `approvals [--mine] [--json]` / `approvals approve|reject <id> [reason]` — the human side, from your terminal (the list is workspace-wide; `--mine` narrows it to this directory's agent)
|
|
@@ -392,7 +429,7 @@ for engines you don't. Each adapter's shipped `.d.ts` header documents its wirin
|
|
|
392
429
|
The CLI's stream contract: **stdout carries results only** (safe to pipe — `--json` output is
|
|
393
430
|
pure JSON); errors, usage shown on *error* paths, and the per-run target-host announcement go
|
|
394
431
|
to **stderr**. Asked-for help (an explicit `--help`, or invoking the bare CLI) is itself a
|
|
395
|
-
result and prints on stdout. Exit codes: 0 success · 1 failure or honest no-op · 2 usage error.
|
|
432
|
+
result and prints on stdout. Exit codes: 0 success · 1 failure or honest no-op · 2 usage error · 3 `quickstart` stopped at the human moment (headless: the decision routed, but nothing executed and no receipt was written, so the run is deliberately not reported as a completed tour).
|
|
396
433
|
|
|
397
434
|
Commands that administer your workspace (`init`, `approvals`, `receipt`) use a **session**
|
|
398
435
|
sign-in held in memory for that run. What lands on disk in `.env`: the agent's floor-limited
|
|
@@ -431,6 +468,6 @@ your queue.
|
|
|
431
468
|
## Notes
|
|
432
469
|
|
|
433
470
|
- **Framework-agnostic:** it wraps at the tool-call boundary, so it works with any agent runtime (LangGraph, OpenAI Agents SDK, CrewAI, a raw tool loop) and any action (Salesforce, M365, SAP, a DB write).
|
|
434
|
-
- **Confirmation integrity:** the wrapper runs in your process, so it captures the genuine API response — a materially stronger signal than an agent self-report, but it trusts the host. For high-assurance, Decidio can *independently* read the source back (`
|
|
471
|
+
- **Confirmation integrity:** the wrapper runs in your process, so it captures the genuine API response — a materially stronger signal than an agent self-report, but it trusts the host. For high-assurance, Decidio can *independently* read the source back (`independently_reconciled`) where it holds a read connection.
|
|
435
472
|
- **Zero-code alternative:** an agent that speaks MCP (e.g. Claude on the web) can reach the same gate via Decidio's `request_approval` MCP tool with no code at all.
|
|
436
473
|
- **API surface:** the agent-gate REST contract this SDK speaks is described in [`openapi.yaml`](./openapi.yaml), shipped in the package.
|
package/dist/adapters/_gate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
1
|
+
import { type GuardConfig, type ApprovalContext, type EvidenceOutcome } from "../index.js";
|
|
2
2
|
/** The ONLY authorization verdicts this SDK understands. A gate answer outside this set means
|
|
3
3
|
* the SDK does not understand the decision — and an answer you do not understand means NO. */
|
|
4
4
|
export declare const AUTHORIZE_VERDICTS: readonly ["proceed", "route", "block"];
|
|
@@ -16,7 +16,13 @@ export interface GateAuth {
|
|
|
16
16
|
* fourth verdict) is the realistic trigger, and it must never mean "run it". */
|
|
17
17
|
export declare function assertAuthorizeVerdict(auth: unknown, fallbackDecisionId?: string): GateAuth;
|
|
18
18
|
export declare function gateAuthorize(config: GuardConfig, ctx: ApprovalContext): Promise<GateAuth>;
|
|
19
|
-
|
|
19
|
+
/** Report an adapter-path execution and RETURN what Decidio recorded.
|
|
20
|
+
*
|
|
21
|
+
* Two 0.2.0 changes, both closing the same gap. It applies the caller's `evidence` mapper — the
|
|
22
|
+
* warning below tells you to use that option, and on every engine adapter it did nothing, because
|
|
23
|
+
* the mapper never reached this function. And it RETURNS the outcome instead of swallowing it, so
|
|
24
|
+
* an adapter can hand its caller the same confirmation the core path now does. */
|
|
25
|
+
export declare function gateConfirm(config: GuardConfig, decisionId: string, result: unknown, evidence?: (r: any) => unknown): Promise<EvidenceOutcome>;
|
|
20
26
|
/** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
|
|
21
27
|
export declare function verdictFrom(payload: any): string;
|
|
22
28
|
/** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
|
package/dist/adapters/_gate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { DecidioBlockedError, DecidioRejectedError, DecidioHttpError, signBoundProof, signConfirm, assertSafeTransport, carriesCredential, canonicalize, readConfirmResponse, buildConfirmBody } from "../index.js";
|
|
2
|
+
import { DecidioBlockedError, DecidioRejectedError, DecidioHttpError, signBoundProof, signConfirm, assertSafeTransport, carriesCredential, canonicalize, readConfirmResponse, buildConfirmBody, decisionIdProblem } from "../index.js";
|
|
3
3
|
export const AUTHORIZE_VERDICTS = ["proceed", "route", "block"];
|
|
4
4
|
export function assertAuthorizeVerdict(auth, fallbackDecisionId = "unknown") {
|
|
5
5
|
const a = auth;
|
|
@@ -7,8 +7,9 @@ export function assertAuthorizeVerdict(auth, fallbackDecisionId = "unknown") {
|
|
|
7
7
|
if (!a || !AUTHORIZE_VERDICTS.includes(a.decision)) {
|
|
8
8
|
throw new DecidioBlockedError(`unrecognised authorization verdict ${JSON.stringify(a?.decision ?? null)} — refusing to execute. The gate answers ${AUTHORIZE_VERDICTS.join(" | ")}; anything else means this SDK version does not understand the decision (upgrade @decidio/sdk), and an answer you do not understand is never permission.`, decisionId);
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const idProblem = decisionIdProblem(a.decisionId);
|
|
11
|
+
if (idProblem) {
|
|
12
|
+
throw new DecidioBlockedError(`the gate answered "${a.decision}" with no usable decisionId (${JSON.stringify(a.decisionId ?? null)} — ${idProblem}) — refusing to execute. Every authorized action must be addressable: without an id it cannot be polled, resumed, confirmed or audited.`, decisionId);
|
|
12
13
|
}
|
|
13
14
|
return a;
|
|
14
15
|
}
|
|
@@ -40,19 +41,23 @@ export async function gateAuthorize(config, ctx) {
|
|
|
40
41
|
}
|
|
41
42
|
return assertAuthorizeVerdict(await res.json(), "unknown");
|
|
42
43
|
}
|
|
43
|
-
export async function gateConfirm(config, decisionId, result) {
|
|
44
|
+
export async function gateConfirm(config, decisionId, result, evidence) {
|
|
44
45
|
const f = config.fetchImpl ?? fetch;
|
|
46
|
+
const failed = (why, retryable = true) => ({
|
|
47
|
+
reportRecorded: false, executionConfirmed: false, confirmationRetryable: retryable, reportJournalled: false,
|
|
48
|
+
diagnostic: `your action RAN and Decidio does NOT have its execution report (${why}). Record the outcome from your own system; do NOT re-run the action.`,
|
|
49
|
+
});
|
|
45
50
|
try {
|
|
46
51
|
assertSafeTransport(config.apiUrl, carriesCredential(config));
|
|
47
52
|
}
|
|
48
53
|
catch (e) {
|
|
49
54
|
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
50
|
-
return;
|
|
55
|
+
return failed(e.message, false);
|
|
51
56
|
}
|
|
52
57
|
let reported = null;
|
|
53
58
|
let evidenceProblem;
|
|
54
59
|
try {
|
|
55
|
-
reported = canonicalize(result ?? null, "result");
|
|
60
|
+
reported = canonicalize((evidence ? evidence(result) : result) ?? null, evidence ? "evidence(result)" : "result");
|
|
56
61
|
}
|
|
57
62
|
catch (e) {
|
|
58
63
|
evidenceProblem = String(e?.message ?? e);
|
|
@@ -69,7 +74,7 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
69
74
|
});
|
|
70
75
|
if (!res.ok) {
|
|
71
76
|
console.warn(`[decidio] confirm returned ${res.status} (action succeeded)`);
|
|
72
|
-
return;
|
|
77
|
+
return { ...failed(`HTTP ${res.status}`, res.status === 408 || res.status === 429 || res.status >= 500), evidenceError: evidenceProblem };
|
|
73
78
|
}
|
|
74
79
|
const ev = readConfirmResponse(await res.json().catch(() => null), { signed: !!requesterIdentity, capturedResponse: reported });
|
|
75
80
|
if (!ev.reportRecorded)
|
|
@@ -78,9 +83,13 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
78
83
|
console.warn(`[decidio] execution evidence for ${decisionId} recorded as ${ev.evidenceTier} — ${ev.diagnostic}`);
|
|
79
84
|
if (evidenceProblem)
|
|
80
85
|
console.warn(`[decidio] ${decisionId}: the execution IS recorded, but with an empty captured response — see the evidence note above.`);
|
|
86
|
+
if (evidenceProblem)
|
|
87
|
+
ev.evidenceError = evidenceProblem;
|
|
88
|
+
return ev;
|
|
81
89
|
}
|
|
82
90
|
catch (e) {
|
|
83
91
|
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
92
|
+
return { ...failed(e.message), evidenceError: evidenceProblem };
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
export function verdictFrom(payload) {
|
|
@@ -11,7 +11,9 @@ import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
|
11
11
|
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
12
12
|
*/
|
|
13
13
|
/** Gate `run()` behind Decidio inside a LangGraph node. */
|
|
14
|
-
export declare function decidioGate<R>(config: GuardConfig, ctx: ApprovalContext, run: () => Promise<R
|
|
14
|
+
export declare function decidioGate<R>(config: GuardConfig, ctx: ApprovalContext, run: () => Promise<R>, opts?: {
|
|
15
|
+
evidence?: (r: R) => unknown;
|
|
16
|
+
}): Promise<R>;
|
|
15
17
|
/** Translate Decidio's signed resume webhook into the LangGraph resume Command. The host
|
|
16
18
|
* maps decisionId → thread_id and passes this to graph.invoke/stream. */
|
|
17
19
|
export declare function decidioResumeCommand(body: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { interrupt, Command } from "@langchain/langgraph";
|
|
2
2
|
import { DecidioBlockedError } from "../index.js";
|
|
3
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict, checkEngineVersion } from "./_gate.js";
|
|
4
|
-
export async function decidioGate(config, ctx, run) {
|
|
4
|
+
export async function decidioGate(config, ctx, run, opts) {
|
|
5
5
|
const auth = await gateAuthorize(config, ctx);
|
|
6
6
|
if (auth.decision === "block")
|
|
7
7
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
@@ -10,7 +10,7 @@ export async function decidioGate(config, ctx, run) {
|
|
|
10
10
|
assertVerdict(verdictFrom(resumed), auth.decisionId, resumed?.reason);
|
|
11
11
|
}
|
|
12
12
|
const result = await run();
|
|
13
|
-
await gateConfirm(config, auth.decisionId, result);
|
|
13
|
+
await gateConfirm(config, auth.decisionId, result, opts?.evidence);
|
|
14
14
|
return result;
|
|
15
15
|
}
|
|
16
16
|
export function decidioResumeCommand(body) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
2
2
|
type ToolApprovalItem = any;
|
|
3
3
|
interface RunStateLike {
|
|
4
4
|
getInterruptions(): ToolApprovalItem[];
|
|
@@ -41,6 +41,11 @@ export declare function gateInterruptions(config: GuardConfig, state: RunStateLi
|
|
|
41
41
|
resolved: Resolved[];
|
|
42
42
|
pending: Pending[];
|
|
43
43
|
}>;
|
|
44
|
-
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString).
|
|
44
|
+
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString).
|
|
45
|
+
*
|
|
46
|
+
* VALIDATES BEFORE MUTATING. `else state.reject(item)` treated every unrecognised string as a
|
|
47
|
+
* rejection, so a garbled payload, a version-skew verdict or an empty string became a rejection
|
|
48
|
+
* that a caller could not tell apart from a human deciding no — and the RunState was already
|
|
49
|
+
* mutated by the time anyone could look. The recognised set is exactly the one `resume` uses. */
|
|
45
50
|
export declare function applyResume(state: RunStateLike, item: ToolApprovalItem, verdict: string): void;
|
|
46
51
|
export {};
|
package/dist/adapters/openai.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { gateAuthorize, checkEngineVersion } from "./_gate.js";
|
|
2
|
+
import { DecidioBlockedError } from "../index.js";
|
|
2
3
|
export async function gateInterruptions(config, state, describe) {
|
|
3
4
|
const resolved = [];
|
|
4
5
|
const pending = [];
|
|
@@ -12,15 +13,22 @@ export async function gateInterruptions(config, state, describe) {
|
|
|
12
13
|
state.reject(item);
|
|
13
14
|
resolved.push({ item, verdict: "blocked", decisionId: auth.decisionId });
|
|
14
15
|
}
|
|
15
|
-
else
|
|
16
|
+
else if (auth.decision === "route")
|
|
16
17
|
pending.push({ item, decisionId: auth.decisionId });
|
|
18
|
+
else
|
|
19
|
+
throw new DecidioBlockedError(`unhandled authorization verdict ${JSON.stringify(auth.decision)}`, auth.decisionId);
|
|
17
20
|
}
|
|
18
21
|
return { resolved, pending };
|
|
19
22
|
}
|
|
20
23
|
export function applyResume(state, item, verdict) {
|
|
21
|
-
if (verdict === "approved" || verdict === "auto_approved")
|
|
24
|
+
if (verdict === "approved" || verdict === "auto_approved") {
|
|
22
25
|
state.approve(item);
|
|
23
|
-
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (verdict === "rejected" || verdict === "blocked") {
|
|
24
29
|
state.reject(item);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
throw new DecidioBlockedError(`unrecognised resume verdict ${JSON.stringify(verdict)} — refusing to apply it. This SDK understands approved / auto_approved / rejected / blocked; anything else is a version skew, and guessing would turn "I do not understand" into a decision. The RunState was NOT modified.`, "");
|
|
25
33
|
}
|
|
26
34
|
checkEngineVersion("openai");
|
|
@@ -21,11 +21,15 @@ export declare function installDecidioSignal(): Map<string, {
|
|
|
21
21
|
verdict: string;
|
|
22
22
|
reason?: string;
|
|
23
23
|
}>;
|
|
24
|
-
export interface DecidioGateOpts {
|
|
24
|
+
export interface DecidioGateOpts<R = unknown> {
|
|
25
25
|
authorize?: () => Promise<GateAuth>;
|
|
26
26
|
confirm?: (decisionId: string, result: unknown) => Promise<void>;
|
|
27
|
+
/** Map your action's return value to the evidence Decidio records — the same option the core
|
|
28
|
+
* path takes. Every adapter's stderr note named it and no adapter accepted one, so on this
|
|
29
|
+
* path the advice could not be followed. */
|
|
30
|
+
evidence?: (result: R) => unknown;
|
|
27
31
|
}
|
|
28
32
|
export declare function decidioGate<R>(config: GuardConfig, ctx: ApprovalContext, run: () => Promise<R>, verdicts: Map<string, {
|
|
29
33
|
verdict: string;
|
|
30
34
|
reason?: string;
|
|
31
|
-
}>, opts?: DecidioGateOpts): Promise<R>;
|
|
35
|
+
}>, opts?: DecidioGateOpts<R>): Promise<R>;
|
|
@@ -22,6 +22,8 @@ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
|
22
22
|
}
|
|
23
23
|
const result = await run();
|
|
24
24
|
if (opts.confirm) {
|
|
25
|
+
if (opts.evidence)
|
|
26
|
+
console.warn("[decidio] temporal: `evidence` is ignored when you supply `confirm` — your callback owns the report, so map the result inside it. Remove one of the two to make the intent clear.");
|
|
25
27
|
await opts.confirm(auth.decisionId, result);
|
|
26
28
|
if (!confirmWarned) {
|
|
27
29
|
confirmWarned = true;
|
|
@@ -29,7 +31,7 @@ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
else {
|
|
32
|
-
await gateConfirm(config, auth.decisionId, result);
|
|
34
|
+
await gateConfirm(config, auth.decisionId, result, opts?.evidence);
|
|
33
35
|
}
|
|
34
36
|
return result;
|
|
35
37
|
}
|
package/dist/canonical.d.ts
CHANGED
|
@@ -15,4 +15,4 @@ export declare function canonicalize<T = JsonValue>(value: unknown, path?: strin
|
|
|
15
15
|
export declare function canonicalizeArgs(args: readonly unknown[]): JsonValue[];
|
|
16
16
|
/** RFC 8785-shaped canonical JSON of an already-canonical value — the exact bytes hashed for an
|
|
17
17
|
* execution report, so both languages produce the same digest. */
|
|
18
|
-
export declare function canonicalJson(value: JsonValue): string;
|
|
18
|
+
export declare function canonicalJson(value: JsonValue, path?: string): string;
|
package/dist/canonical.js
CHANGED
|
@@ -114,15 +114,19 @@ function deepFreeze(v) {
|
|
|
114
114
|
export function canonicalizeArgs(args) {
|
|
115
115
|
return args.map((a, i) => deepFreeze(walk(a, `args[${i}]`, new Set())));
|
|
116
116
|
}
|
|
117
|
-
export function canonicalJson(value) {
|
|
117
|
+
export function canonicalJson(value, path = "canonicalJson(...)") {
|
|
118
118
|
if (value === null)
|
|
119
119
|
return "null";
|
|
120
120
|
if (typeof value === "boolean" || typeof value === "number")
|
|
121
121
|
return JSON.stringify(value);
|
|
122
|
-
if (typeof value === "string")
|
|
122
|
+
if (typeof value === "string") {
|
|
123
|
+
assertNoLoneSurrogate(value, path);
|
|
123
124
|
return JSON.stringify(value);
|
|
125
|
+
}
|
|
124
126
|
if (Array.isArray(value))
|
|
125
|
-
return `[${value.map(canonicalJson).join(",")}]`;
|
|
127
|
+
return `[${value.map((v, i) => canonicalJson(v, `${path}[${i}]`)).join(",")}]`;
|
|
126
128
|
const keys = Object.keys(value).sort();
|
|
127
|
-
|
|
129
|
+
for (const k of keys)
|
|
130
|
+
assertNoLoneSurrogate(k, `${path} key`);
|
|
131
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k], `${path}.${k}`)}`).join(",")}}`;
|
|
128
132
|
}
|
package/dist/cli.js
CHANGED
|
@@ -258,6 +258,18 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
258
258
|
}
|
|
259
259
|
const { did: localDid, privateKeyBase64: localKey } = generateKeypair();
|
|
260
260
|
const registerBody = JSON.stringify({ agentId, did: localDid, ...(sourceSystem ? { sourceSystem } : {}) });
|
|
261
|
+
let envPrepared = false;
|
|
262
|
+
const prepareEnvForWrite = () => {
|
|
263
|
+
if (envPrepared)
|
|
264
|
+
return;
|
|
265
|
+
envPrepared = true;
|
|
266
|
+
ensureGitignore();
|
|
267
|
+
if (opts.force && existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId) {
|
|
268
|
+
const backup = `.env.${existingEnv.DECIDIO_AGENT_ID.replace(/[^\w.-]/g, "_")}.bak`;
|
|
269
|
+
writeSecretFile(backup, readFileSync(ENV_PATH, "utf8"));
|
|
270
|
+
log(`✔ backed the previous agent's .env up to ${backup} (its key would otherwise be unrecoverable)`);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
261
273
|
let session = null;
|
|
262
274
|
let r = await api("/api/agents/register", { method: "POST", body: registerBody });
|
|
263
275
|
if (r.status === 401 || r.status === 403) {
|
|
@@ -288,6 +300,8 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
288
300
|
did = r.body?.did;
|
|
289
301
|
privateKey = localKey;
|
|
290
302
|
log(`✔ registered (did ${did}) — key generated locally; only the public half went to Decidio`);
|
|
303
|
+
prepareEnvForWrite();
|
|
304
|
+
upsertEnv({ DECIDIO_AGENT_ID: agentId, DECIDIO_AGENT_DID: did ?? localDid, DECIDIO_AGENT_KEY: localKey });
|
|
291
305
|
}
|
|
292
306
|
else {
|
|
293
307
|
err(`✘ registration failed (${r.status}): ${JSON.stringify(r.body)}`);
|
|
@@ -310,7 +324,7 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
310
324
|
}
|
|
311
325
|
else if (session) {
|
|
312
326
|
err(`✘ could not mint the agent's API token (${mint.status}): ${JSON.stringify(mint.body)}`);
|
|
313
|
-
err(` Without it the SDK has no runtime credential.
|
|
327
|
+
err(` Without it the SDK has no runtime credential. The agent IS registered and its key is saved in ${ENV_PATH} — fix the cause above and re-run init; it will reuse that key and mint again.`);
|
|
314
328
|
return 1;
|
|
315
329
|
}
|
|
316
330
|
else {
|
|
@@ -331,12 +345,7 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
331
345
|
envOut.DECIDIO_WORKSPACE_ID = workspaceId;
|
|
332
346
|
if (tokenJti)
|
|
333
347
|
envOut.DECIDIO_TOKEN_JTI = tokenJti;
|
|
334
|
-
|
|
335
|
-
if (opts.force && existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId) {
|
|
336
|
-
const backup = `.env.${existingEnv.DECIDIO_AGENT_ID.replace(/[^\w.-]/g, "_")}.bak`;
|
|
337
|
-
writeSecretFile(backup, readFileSync(ENV_PATH, "utf8"));
|
|
338
|
-
log(`✔ backed the previous agent's .env up to ${backup} (its key would otherwise be unrecoverable)`);
|
|
339
|
-
}
|
|
348
|
+
prepareEnvForWrite();
|
|
340
349
|
upsertEnv(envOut);
|
|
341
350
|
for (const [k, v] of Object.entries(envOut))
|
|
342
351
|
process.env[k] = v;
|
|
@@ -358,8 +367,9 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
358
367
|
log(` { mode: "blocking" }, // blocking = watch it happen live; durable is the production mode`);
|
|
359
368
|
log(` );`);
|
|
360
369
|
log("");
|
|
361
|
-
log(` const
|
|
362
|
-
log(` console.log("executed after approval:",
|
|
370
|
+
log(` const { value, confirmation } = await payInvoice({ id: "INV-2026-001", amount: 86_000 });`);
|
|
371
|
+
log(` console.log("executed after approval:", value);`);
|
|
372
|
+
log(` console.log("Decidio recorded it as:", confirmation.evidenceTier);`);
|
|
363
373
|
log("");
|
|
364
374
|
log(" 2. Run it, approve it, watch it complete:");
|
|
365
375
|
log("");
|
|
@@ -411,7 +421,7 @@ async function cmdDoctor() {
|
|
|
411
421
|
if (typeof payload?.exp === "number") {
|
|
412
422
|
const msLeft = payload.exp * 1000 - Date.now();
|
|
413
423
|
const days = Math.floor((msLeft + 60_000) / 86_400_000);
|
|
414
|
-
const left = msLeft < 86_400_000 ? `${Math.max(1, Math.floor(msLeft / 3_600_000))} hour(s)` : `${
|
|
424
|
+
const left = msLeft < 86_400_000 ? `${Math.max(1, Math.floor(msLeft / 3_600_000))} hour(s)` : `${Math.round(msLeft / 86_400_000)} day(s)`;
|
|
415
425
|
const daysRounded = Math.round(msLeft / 86_400_000);
|
|
416
426
|
if (msLeft < 0)
|
|
417
427
|
line(false, `agent token: EXPIRED ${-days} day(s) ago — re-run \`npx @decidio/sdk init ${env.DECIDIO_AGENT_ID ?? "<agent>"}\` to mint a fresh one.`);
|
|
@@ -577,12 +587,18 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
577
587
|
const detail = await api(`/api/records/${encodeURIComponent(id)}`, {}, bearer);
|
|
578
588
|
const conf = detail.body?.record?.confirmation;
|
|
579
589
|
const issuer = r.body?.issuer;
|
|
590
|
+
const resolved = r.body?.resolved;
|
|
580
591
|
if (jsonMode) {
|
|
581
|
-
log(JSON.stringify({ file: name, decisionId: id, evidenceTier: conf?.status ?? null, confirmed: !!conf?.confirmed, issuer: issuer ?? null }, null, 2));
|
|
592
|
+
log(JSON.stringify({ file: name, decisionId: id, evidenceTier: conf?.status ?? null, confirmed: !!conf?.confirmed, issuer: issuer ?? null, ...(resolved ? { resolved } : {}) }, null, 2));
|
|
582
593
|
return 0;
|
|
583
594
|
}
|
|
584
595
|
if (conf?.status)
|
|
585
596
|
log(` Sealed credential: the authority decision (its confirmation field reads "pending" forever — immutable). Current record evidence tier: ${conf.status}${conf.confirmed ? " (confirmed)" : ""}.`);
|
|
597
|
+
if (resolved && Object.keys(resolved).length) {
|
|
598
|
+
log(` Resolved for you (${Object.keys(resolved).length} field${Object.keys(resolved).length === 1 ? "" : "s"} your access covers — the FILE keeps the tokens, so it stays verifiable and safe to hand on):`);
|
|
599
|
+
for (const [tok, val] of Object.entries(resolved))
|
|
600
|
+
log(` ${tok} → ${val}`);
|
|
601
|
+
}
|
|
586
602
|
log(` Verify it offline — no Decidio account or network needed (the verifier is built in):`);
|
|
587
603
|
if (issuer) {
|
|
588
604
|
log(` npx @decidio/sdk verify --issuer ${issuer} ${name}`);
|
|
@@ -656,6 +672,10 @@ async function cmdQuickstart(opts = {}) {
|
|
|
656
672
|
const session = await acquireSession("The quickstart");
|
|
657
673
|
if (!session)
|
|
658
674
|
return 1;
|
|
675
|
+
const envFile = readEnvFile().DECIDIO_AGENT_KEY ? ".env.quickstart" : ENV_PATH;
|
|
676
|
+
if (!assertNotTracked(envFile))
|
|
677
|
+
return 1;
|
|
678
|
+
ensureGitignore();
|
|
659
679
|
let agentId = `quickstart-${randomBytes(5).toString("hex")}`;
|
|
660
680
|
log(`① Registering agent "${agentId}"…`);
|
|
661
681
|
const { did, privateKeyBase64 } = generateKeypair();
|
|
@@ -690,30 +710,36 @@ async function cmdQuickstart(opts = {}) {
|
|
|
690
710
|
DECIDIO_WORKSPACE_ID: mint.body.workspaceId ?? "",
|
|
691
711
|
...(mint.body.jti ? { DECIDIO_TOKEN_JTI: mint.body.jti } : {}),
|
|
692
712
|
};
|
|
693
|
-
|
|
694
|
-
if (readEnvFile().DECIDIO_AGENT_KEY) {
|
|
695
|
-
envFile = ".env.quickstart";
|
|
696
|
-
if (!assertNotTracked(envFile))
|
|
697
|
-
return 1;
|
|
698
|
-
ensureGitignore();
|
|
713
|
+
if (envFile !== ENV_PATH) {
|
|
699
714
|
if (existsSync(envFile)) {
|
|
700
715
|
const prev = readFileSync(envFile, "utf8");
|
|
701
716
|
const prevId = prev.match(/^DECIDIO_AGENT_ID=(.+)$/m)?.[1];
|
|
717
|
+
const prevJti = prev.match(/^DECIDIO_TOKEN_JTI=(.+)$/m)?.[1];
|
|
702
718
|
if (prevId && prevId !== agentId) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
719
|
+
if (prevJti) {
|
|
720
|
+
const rv = await api(`/api/workspace/agent-token/${encodeURIComponent(prevJti)}`, { method: "DELETE" }, session);
|
|
721
|
+
if (rv.status >= 200 && rv.status < 300)
|
|
722
|
+
log(` ✔ the previous tour agent (${prevId}) had its token REVOKED before this tour replaced it — no live credential is left behind`);
|
|
723
|
+
else
|
|
724
|
+
log(` ⚠ could not revoke the previous tour's token (jti ${prevJti}, ${rv.status}) — revoke it by hand: npx @decidio/sdk tokens revoke ${prevJti}`);
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
log(` ⚠ the previous tour file carried no revocation id, so its token (if any) must be revoked from your Decidio app.`);
|
|
728
|
+
}
|
|
706
729
|
}
|
|
707
730
|
}
|
|
708
731
|
writeSecretFile(envFile, Object.entries(envOut).map(([k, v]) => `${k}=${v}`).join("\n") + "\n");
|
|
709
732
|
log(` ✔ this directory already has an agent in .env — tour credentials went to ${envFile} (your agent is untouched)`);
|
|
710
733
|
}
|
|
711
734
|
else {
|
|
712
|
-
if (!assertNotTracked(ENV_PATH))
|
|
713
|
-
return 1;
|
|
714
|
-
ensureGitignore();
|
|
715
735
|
upsertEnv(envOut);
|
|
716
736
|
}
|
|
737
|
+
const failTour = async (code) => {
|
|
738
|
+
log("");
|
|
739
|
+
log(" The tour did not complete — cleaning up the credentials it created:");
|
|
740
|
+
await quickstartCleanup(session, envFile).catch(() => { });
|
|
741
|
+
return code;
|
|
742
|
+
};
|
|
717
743
|
log(` ✔ key generated locally (public half registered) · 7-day floor-limited token minted${mint.body.expiresAt ? ` (expires ${mint.body.expiresAt})` : ""} · ${envFile} written`);
|
|
718
744
|
log("");
|
|
719
745
|
log(`② Gating a demo action — the tour runs this (blocking mode: watch it live):`);
|
|
@@ -740,15 +766,15 @@ async function cmdQuickstart(opts = {}) {
|
|
|
740
766
|
err(`✘ this workspace's governed-action allowance is used up${earlyError.limit != null ? ` (${earlyError.limit} actions/hour on lab workspaces)` : ""}.`);
|
|
741
767
|
err(` Nothing was routed or executed — the cap is the sandbox protecting itself, not a fault${earlyError.resetAt ? `. It resets ${earlyError.resetAt}` : ""}.`);
|
|
742
768
|
err(` Re-run the tour after the reset (or talk to us about a production workspace).`);
|
|
743
|
-
return 1;
|
|
769
|
+
return failTour(1);
|
|
744
770
|
}
|
|
745
771
|
if (!decisionId && earlyError) {
|
|
746
772
|
err(`✘ the gate refused before routing: ${earlyError?.message ?? earlyError}`);
|
|
747
|
-
return 1;
|
|
773
|
+
return failTour(1);
|
|
748
774
|
}
|
|
749
775
|
if (!decisionId) {
|
|
750
776
|
err("✘ the gate did not route within 30s — check connectivity (npx @decidio/sdk doctor) and re-run.");
|
|
751
|
-
return 1;
|
|
777
|
+
return failTour(1);
|
|
752
778
|
}
|
|
753
779
|
log(` ✔ $86,000 payInvoice ROUTED to a human (decision ${decisionId}) — this agent has no auto-approve rule,`);
|
|
754
780
|
log(` so deny-by-default sends everything to a person. Nothing executes until someone decides.`);
|
|
@@ -776,29 +802,35 @@ async function cmdQuickstart(opts = {}) {
|
|
|
776
802
|
log("");
|
|
777
803
|
log(" (exit 3 = the tour stopped at the human moment. Nothing executed and no receipt was");
|
|
778
804
|
log(" written, so this run is deliberately not reported as a completed tour.)");
|
|
805
|
+
log("");
|
|
806
|
+
log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's`);
|
|
807
|
+
log(` key), because the decision above is still waiting for you. Once you have approved it —`);
|
|
808
|
+
log(` or if you abandon the tour — remove them: npx @decidio/sdk quickstart cleanup`);
|
|
779
809
|
process.exit(3);
|
|
780
810
|
}
|
|
781
811
|
const ap = await api(`/api/decisions/${encodeURIComponent(decisionId)}/approve`, { method: "POST", body: JSON.stringify({ reason: opts.autoApproveDemo && !interactive ? "quickstart --auto-approve-demo (automated)" : "quickstart" }) }, session);
|
|
782
812
|
if (!(ap.status >= 200 && ap.status < 300)) {
|
|
783
813
|
err(`✘ approve failed (${ap.status}): ${JSON.stringify(ap.body)}`);
|
|
784
|
-
return 1;
|
|
814
|
+
return failTour(1);
|
|
785
815
|
}
|
|
786
816
|
log(` ✔ approved${ap.body?.alreadyResolved ? " (it was already approved from your app — even better)" : ""}`);
|
|
787
|
-
let
|
|
817
|
+
let done;
|
|
788
818
|
try {
|
|
789
|
-
|
|
819
|
+
done = await running;
|
|
790
820
|
}
|
|
791
821
|
catch (e) {
|
|
792
822
|
err(`✘ the gated call did not complete: ${e?.message ?? e}`);
|
|
793
|
-
return 1;
|
|
823
|
+
return failTour(1);
|
|
794
824
|
}
|
|
795
|
-
log(` ✔ your function executed AFTER approval: ${JSON.stringify(
|
|
825
|
+
log(` ✔ your function executed AFTER approval: ${JSON.stringify(done.value)}`);
|
|
826
|
+
if (done.confirmation?.evidenceTier)
|
|
827
|
+
log(` …and the call itself told you what was recorded: confirmation.evidenceTier = ${done.confirmation.evidenceTier}`);
|
|
796
828
|
log("");
|
|
797
829
|
log("④ Downloading the sealed Authority Receipt and verifying it OFFLINE…");
|
|
798
830
|
const rec = await api(`/api/records/${encodeURIComponent(decisionId)}/verify`, {}, session);
|
|
799
831
|
if (!(rec.status >= 200 && rec.status < 300) || !rec.body?.credential) {
|
|
800
832
|
err(`✘ receipt fetch failed (${rec.status})`);
|
|
801
|
-
return 1;
|
|
833
|
+
return failTour(1);
|
|
802
834
|
}
|
|
803
835
|
const fname = `decidio-receipt-${decisionId.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
|
|
804
836
|
writeFileSync(fname, JSON.stringify(rec.body.credential, null, 2) + "\n");
|
|
@@ -838,9 +870,8 @@ async function cmdQuickstart(opts = {}) {
|
|
|
838
870
|
doCleanup = true;
|
|
839
871
|
log(`\nAutomated run: cleaning the tour up (pass --keep-agent to keep the agent + token).`);
|
|
840
872
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
return v.valid ? 0 : 1;
|
|
873
|
+
const cleanupCode = doCleanup ? await quickstartCleanup(session, envFile) : 0;
|
|
874
|
+
return v.valid ? (cleanupCode || 0) : 1;
|
|
844
875
|
}
|
|
845
876
|
async function quickstartCleanup(session, envFile) {
|
|
846
877
|
const env = (() => {
|
|
@@ -1009,6 +1040,8 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
1009
1040
|
at the routed decision; --auto-approve-demo for CI, labeled
|
|
1010
1041
|
automated; ends with a keep-or-cleanup choice)
|
|
1011
1042
|
npx @decidio/sdk quickstart cleanup revoke the tour's token + delete its local credentials
|
|
1043
|
+
npx @decidio/sdk quickstart --auto-approve-demo headless tour; cleans up after itself
|
|
1044
|
+
(add --keep-agent to KEEP the tour agent + token)
|
|
1012
1045
|
npx @decidio/sdk tokens list [--mine] agent tokens with status (active/expired/REVOKED) by
|
|
1013
1046
|
jti — token values are never re-shown; the list is
|
|
1014
1047
|
workspace-wide, --mine narrows to this directory's agent
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/** A gate response this SDK could not accept, carrying what a caller needs to BRANCH on rather
|
|
2
|
-
* than a stringified body they would have to parse. `status` is the HTTP status; `retryable`
|
|
3
|
-
* distinguishes a terminal refusal (401/403 — a credential problem no retry fixes) from a
|
|
4
|
-
* transient one (5xx, 429). Every long-running agent hits the terminal case eventually: an
|
|
5
|
-
* agent token expires, or `init` is re-run and revokes it. */
|
|
6
1
|
export declare class DecidioHttpError extends Error {
|
|
7
2
|
status: number;
|
|
8
3
|
endpoint: string;
|
|
@@ -10,3 +5,15 @@ export declare class DecidioHttpError extends Error {
|
|
|
10
5
|
readonly retryable: boolean;
|
|
11
6
|
constructor(status: number, endpoint: string, body?: unknown | undefined);
|
|
12
7
|
}
|
|
8
|
+
/** ONE rule for what counts as a usable decision id, returning a REASON rather than throwing, so
|
|
9
|
+
* every door can apply the same check and raise its own error type. (`resume.ts` cannot import
|
|
10
|
+
* `index.ts`, which is why the check was copy-pasted into four places and drifted.)
|
|
11
|
+
*
|
|
12
|
+
* All four copies tested truthiness only, so `" "` passed every one of them: the blocking path
|
|
13
|
+
* would poll `/agent/status/%20%20%20`, the durable path would park under a whitespace key, and
|
|
14
|
+
* the confirmation would attest a decision nobody can look up. Control characters are refused for
|
|
15
|
+
* the same reason a reason-string is sanitised — an id goes into a URL path, a filename and an
|
|
16
|
+
* audit line. Nothing is silently trimmed: an id that needs trimming is not the id the server
|
|
17
|
+
* sent, and quietly rewriting an identifier is how two systems end up disagreeing about which
|
|
18
|
+
* decision they mean. */
|
|
19
|
+
export declare function decisionIdProblem(raw: unknown): string | null;
|