@decidio/sdk 0.1.11 → 0.2.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 +53 -15
- package/dist/adapters/_gate.d.ts +8 -2
- package/dist/adapters/_gate.js +30 -8
- 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 +34 -6
- package/dist/cli.js +141 -52
- package/dist/errors.d.ts +12 -5
- package/dist/errors.js +37 -5
- package/dist/evidence.d.ts +23 -0
- package/dist/evidence.js +15 -4
- package/dist/guard.d.ts +20 -3
- package/dist/guard.js +8 -6
- package/dist/index.d.ts +28 -3
- package/dist/index.js +49 -17
- package/dist/inngest.d.ts +4 -0
- package/dist/inngest.js +1 -1
- package/dist/resume.d.ts +334 -57
- package/dist/resume.js +705 -145
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +36 -2
- package/openapi.yaml +89 -6
- package/package.json +13 -8
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
|
|
@@ -413,6 +450,7 @@ manager for production.
|
|
|
413
450
|
the fix. Anything else is transient, and retrying is safe because a retry creates a *new* request
|
|
414
451
|
and can never double-execute.
|
|
415
452
|
- `DecidioNetworkError` — the gate was unreachable. Nothing executed locally.
|
|
453
|
+
- `DecidioUnsafeArgumentsError` — your call arguments cannot survive JSON with their meaning intact, so nothing was routed and nothing ran. The message names the field and the fix. Most often: `NaN`/`Infinity`, `-0`, a `Date`, `Map` or class instance, a `BigInt`, or a sparse array — anything whose serialized form would differ from the value the approval and signature cover. Pass the already-serialized form.
|
|
416
454
|
|
|
417
455
|
```js
|
|
418
456
|
try { await payInvoice(inv); }
|
|
@@ -430,6 +468,6 @@ your queue.
|
|
|
430
468
|
## Notes
|
|
431
469
|
|
|
432
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).
|
|
433
|
-
- **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.
|
|
434
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.
|
|
435
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, 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,11 +7,15 @@ 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
|
+
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);
|
|
13
|
+
}
|
|
10
14
|
return a;
|
|
11
15
|
}
|
|
12
16
|
export async function gateAuthorize(config, ctx) {
|
|
13
17
|
canonicalize(ctx, "describe(...)");
|
|
14
|
-
assertSafeTransport(config.apiUrl,
|
|
18
|
+
assertSafeTransport(config.apiUrl, carriesCredential(config));
|
|
15
19
|
const f = config.fetchImpl ?? fetch;
|
|
16
20
|
const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
|
|
17
21
|
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
@@ -37,20 +41,32 @@ export async function gateAuthorize(config, ctx) {
|
|
|
37
41
|
}
|
|
38
42
|
return assertAuthorizeVerdict(await res.json(), "unknown");
|
|
39
43
|
}
|
|
40
|
-
export async function gateConfirm(config, decisionId, result) {
|
|
44
|
+
export async function gateConfirm(config, decisionId, result, evidence) {
|
|
41
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
|
+
});
|
|
42
50
|
try {
|
|
43
|
-
assertSafeTransport(config.apiUrl,
|
|
51
|
+
assertSafeTransport(config.apiUrl, carriesCredential(config));
|
|
44
52
|
}
|
|
45
53
|
catch (e) {
|
|
46
54
|
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
47
|
-
return;
|
|
55
|
+
return failed(e.message, false);
|
|
56
|
+
}
|
|
57
|
+
let reported = null;
|
|
58
|
+
let evidenceProblem;
|
|
59
|
+
try {
|
|
60
|
+
reported = canonicalize((evidence ? evidence(result) : result) ?? null, evidence ? "evidence(result)" : "result");
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
evidenceProblem = String(e?.message ?? e);
|
|
64
|
+
console.warn(`[decidio] ${decisionId}: the action SUCCEEDED but its result could not be reported as evidence (${evidenceProblem}). Reporting an empty captured response so the execution is still recorded — map the return value with the \`evidence\` option to earn a confirming tier. The action was NOT re-run.`);
|
|
48
65
|
}
|
|
49
66
|
try {
|
|
50
67
|
const requesterIdentity = signConfirm(decisionId, config);
|
|
51
68
|
if (!requesterIdentity)
|
|
52
69
|
console.warn(`[decidio] execution evidence for ${decisionId} recorded as agent_asserted — no signing key configured, so the report can't be proven to come from this agent. Set DECIDIO_AGENT_KEY/DID to earn application_confirmed.`);
|
|
53
|
-
const reported = canonicalize(result ?? null, "result");
|
|
54
70
|
const res = await f(config.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
|
|
55
71
|
method: "POST",
|
|
56
72
|
headers: { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) },
|
|
@@ -58,16 +74,22 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
58
74
|
});
|
|
59
75
|
if (!res.ok) {
|
|
60
76
|
console.warn(`[decidio] confirm returned ${res.status} (action succeeded)`);
|
|
61
|
-
return;
|
|
77
|
+
return { ...failed(`HTTP ${res.status}`, res.status === 408 || res.status === 429 || res.status >= 500), evidenceError: evidenceProblem };
|
|
62
78
|
}
|
|
63
79
|
const ev = readConfirmResponse(await res.json().catch(() => null), { signed: !!requesterIdentity, capturedResponse: reported });
|
|
64
80
|
if (!ev.reportRecorded)
|
|
65
81
|
console.warn(`[decidio] ${decisionId}: ${ev.diagnostic}`);
|
|
66
|
-
else if (!ev.executionConfirmed)
|
|
82
|
+
else if (!ev.executionConfirmed || ev.executionReportVerified === false)
|
|
67
83
|
console.warn(`[decidio] execution evidence for ${decisionId} recorded as ${ev.evidenceTier} — ${ev.diagnostic}`);
|
|
84
|
+
if (evidenceProblem)
|
|
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;
|
|
68
89
|
}
|
|
69
90
|
catch (e) {
|
|
70
91
|
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
92
|
+
return { ...failed(e.message), evidenceError: evidenceProblem };
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
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
|
@@ -10,6 +10,22 @@ const NOT_ROUTED = "The decision was NOT routed; nothing reached Decidio.";
|
|
|
10
10
|
function refuse(path, what, fix) {
|
|
11
11
|
throw new DecidioUnsafeArgumentsError(`[decidio] ${path} ${what} — the approval payload must be plain JSON so that what a human approves is exactly what executes; ${fix} ${NOT_ROUTED}`);
|
|
12
12
|
}
|
|
13
|
+
function assertNoLoneSurrogate(s, p) {
|
|
14
|
+
for (let i = 0; i < s.length; i++) {
|
|
15
|
+
const c = s.charCodeAt(i);
|
|
16
|
+
if (c >= 0xd800 && c <= 0xdbff) {
|
|
17
|
+
const n = s.charCodeAt(i + 1);
|
|
18
|
+
if (n >= 0xdc00 && n <= 0xdfff) {
|
|
19
|
+
i++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
refuse(p, `contains an unpaired surrogate (U+${c.toString(16).toUpperCase()})`, "it cannot be encoded as UTF-8, so no wire or seal can carry it — repair the string (it is usually a split emoji or truncated UTF-16).");
|
|
23
|
+
}
|
|
24
|
+
else if (c >= 0xdc00 && c <= 0xdfff) {
|
|
25
|
+
refuse(p, `contains an unpaired surrogate (U+${c.toString(16).toUpperCase()})`, "it cannot be encoded as UTF-8, so no wire or seal can carry it — repair the string (it is usually a split emoji or truncated UTF-16).");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
13
29
|
export function canonicalize(value, path = "args") {
|
|
14
30
|
return walk(value, path, new Set());
|
|
15
31
|
}
|
|
@@ -17,8 +33,12 @@ function walk(v, p, seen) {
|
|
|
17
33
|
if (v === null)
|
|
18
34
|
return null;
|
|
19
35
|
const t = typeof v;
|
|
20
|
-
if (t === "boolean"
|
|
36
|
+
if (t === "boolean")
|
|
37
|
+
return v;
|
|
38
|
+
if (t === "string") {
|
|
39
|
+
assertNoLoneSurrogate(v, p);
|
|
21
40
|
return v;
|
|
41
|
+
}
|
|
22
42
|
if (t === "number") {
|
|
23
43
|
const n = v;
|
|
24
44
|
if (!Number.isFinite(n))
|
|
@@ -72,7 +92,11 @@ function walk(v, p, seen) {
|
|
|
72
92
|
refuse(`${p}.${key}`, "is a getter/setter", "an accessor is evaluated at serialization time, so the approved value could differ from the executed one — pass a plain data property.");
|
|
73
93
|
if (!d.enumerable)
|
|
74
94
|
refuse(`${p}.${key}`, "is a non-enumerable property", "JSON drops it, so the approver would never see it — make it enumerable or remove it.");
|
|
75
|
-
|
|
95
|
+
assertNoLoneSurrogate(key, `${p}.${key}`);
|
|
96
|
+
Object.defineProperty(out, key, {
|
|
97
|
+
value: walk(d.value, `${p}.${key}`, seen),
|
|
98
|
+
writable: true, enumerable: true, configurable: true,
|
|
99
|
+
});
|
|
76
100
|
}
|
|
77
101
|
return out;
|
|
78
102
|
}
|
|
@@ -90,15 +114,19 @@ function deepFreeze(v) {
|
|
|
90
114
|
export function canonicalizeArgs(args) {
|
|
91
115
|
return args.map((a, i) => deepFreeze(walk(a, `args[${i}]`, new Set())));
|
|
92
116
|
}
|
|
93
|
-
export function canonicalJson(value) {
|
|
117
|
+
export function canonicalJson(value, path = "canonicalJson(...)") {
|
|
94
118
|
if (value === null)
|
|
95
119
|
return "null";
|
|
96
120
|
if (typeof value === "boolean" || typeof value === "number")
|
|
97
121
|
return JSON.stringify(value);
|
|
98
|
-
if (typeof value === "string")
|
|
122
|
+
if (typeof value === "string") {
|
|
123
|
+
assertNoLoneSurrogate(value, path);
|
|
99
124
|
return JSON.stringify(value);
|
|
125
|
+
}
|
|
100
126
|
if (Array.isArray(value))
|
|
101
|
-
return `[${value.map(canonicalJson).join(",")}]`;
|
|
127
|
+
return `[${value.map((v, i) => canonicalJson(v, `${path}[${i}]`)).join(",")}]`;
|
|
102
128
|
const keys = Object.keys(value).sort();
|
|
103
|
-
|
|
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(",")}}`;
|
|
104
132
|
}
|