@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/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ResumeController } from "./resume.js";
|
|
2
|
+
import { type EvidenceOutcome } from "./evidence.js";
|
|
2
3
|
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
3
4
|
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome } from "./resume.js";
|
|
4
5
|
export { guard, Guard } from "./guard.js";
|
|
5
6
|
export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
6
|
-
export { DecidioHttpError } from "./errors.js";
|
|
7
|
-
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport } from "./signing.js";
|
|
7
|
+
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
8
|
+
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
8
9
|
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
9
10
|
export type { JsonValue, JsonPrimitive } from "./canonical.js";
|
|
10
11
|
export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
|
|
@@ -107,10 +108,34 @@ export declare class DecidioNetworkError extends Error {
|
|
|
107
108
|
/** `<appUrl>/d/<decisionId>` — the deep link that opens this decision directly, on the web app or
|
|
108
109
|
* the phone build. Null when DECIDIO_APP_URL is not configured, never a half-built URL. */
|
|
109
110
|
export declare function decisionUrl(appUrl: string | undefined, decisionId: string): string | null;
|
|
111
|
+
/** What a protected call returns in 0.2.0: your function's value, the decision it was authorized
|
|
112
|
+
* under, and what Decidio actually recorded about the execution.
|
|
113
|
+
*
|
|
114
|
+
* Through 0.1.x this was just `R`. The SDK computed the full confirmation outcome — tier, whether
|
|
115
|
+
* the report was recorded, whether the signed execution report verified, whether the evidence
|
|
116
|
+
* mapper failed — and then dropped it on the floor, surfacing it only as a console warning. For a
|
|
117
|
+
* product whose value IS the record, the one thing a caller could not do was find out what got
|
|
118
|
+
* recorded. Returning it makes "my action ran but was logged as an unproven assertion" something
|
|
119
|
+
* code can branch on instead of something a human might notice in a log.
|
|
120
|
+
*
|
|
121
|
+
* `protectBestEffort` keeps the old value-only shape for callers that genuinely do not care. */
|
|
122
|
+
export interface ProtectedExecution<R> {
|
|
123
|
+
/** Exactly what your function returned — unchanged, uncanonicalized, your object. */
|
|
124
|
+
value: R;
|
|
125
|
+
/** The decision this execution was authorized under. Use it as your downstream idempotency key. */
|
|
126
|
+
decisionId: string;
|
|
127
|
+
/** What Decidio recorded. `reportRecorded` false means the execution happened and Decidio does
|
|
128
|
+
* NOT know; `executionConfirmed` false means it knows but could not attest a commit. */
|
|
129
|
+
confirmation: EvidenceOutcome;
|
|
130
|
+
}
|
|
110
131
|
export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig, opts?: {
|
|
111
132
|
/** Map YOUR function's natural return value to the evidence Decidio records. The gate awards
|
|
112
133
|
* its strongest wrapper tier (application_confirmed) only for a committed record id plus a
|
|
113
134
|
* success indication — this lets a real API keep its own shape instead of being forced into
|
|
114
135
|
* magic field names: `evidence: (r) => ({ recordId: r.transactionId, success: r.status === "committed" })`. */
|
|
115
136
|
evidence?: (result: R) => unknown;
|
|
116
|
-
|
|
137
|
+
/** Hand the action an `ExecutionContext` as its FIRST argument: `{ decisionId, attemptId,
|
|
138
|
+
* idempotencyKey }`. Opt-in, never inferred from the function's arity — guessing wrong means
|
|
139
|
+
* passing a context object as someone's first business argument. */
|
|
140
|
+
withContext?: boolean;
|
|
141
|
+
}): (...args: A) => Promise<ProtectedExecution<R>>;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { DecidioSuspendedError } from "./resume.js";
|
|
2
|
-
import { signBoundProof, signConfirm, assertSafeTransport } from "./signing.js";
|
|
2
|
+
import { signBoundProof, signConfirm, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
3
3
|
import { canonicalize, canonicalizeArgs } from "./canonical.js";
|
|
4
4
|
import { readConfirmResponse, buildConfirmBody } from "./evidence.js";
|
|
5
5
|
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
6
6
|
export { guard, Guard } from "./guard.js";
|
|
7
|
-
export { DecidioHttpError } from "./errors.js";
|
|
8
|
-
import { DecidioHttpError } from "./errors.js";
|
|
9
|
-
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport } from "./signing.js";
|
|
7
|
+
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
8
|
+
import { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
9
|
+
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
10
10
|
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
11
11
|
export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
|
|
12
12
|
export class DecidioBlockedError extends Error {
|
|
@@ -80,7 +80,7 @@ async function reach(f, url, op, init) {
|
|
|
80
80
|
}
|
|
81
81
|
async function post(cfg, path, body) {
|
|
82
82
|
const f = cfg.fetchImpl ?? fetch;
|
|
83
|
-
assertSafeTransport(cfg.apiUrl,
|
|
83
|
+
assertSafeTransport(cfg.apiUrl, carriesCredential(cfg));
|
|
84
84
|
const res = await reach(f, cfg.apiUrl.replace(/\/$/, "") + path, `POST ${path}`, {
|
|
85
85
|
method: "POST",
|
|
86
86
|
headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
@@ -103,7 +103,7 @@ async function post(cfg, path, body) {
|
|
|
103
103
|
}
|
|
104
104
|
async function get(cfg, path) {
|
|
105
105
|
const f = cfg.fetchImpl ?? fetch;
|
|
106
|
-
assertSafeTransport(cfg.apiUrl,
|
|
106
|
+
assertSafeTransport(cfg.apiUrl, carriesCredential(cfg));
|
|
107
107
|
const res = await reach(f, cfg.apiUrl.replace(/\/$/, "") + path, `GET ${path}`, {
|
|
108
108
|
headers: { ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
109
109
|
});
|
|
@@ -142,6 +142,12 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
142
142
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
143
143
|
: undefined;
|
|
144
144
|
canonicalize(ctx, "describe(...)");
|
|
145
|
+
if (typeof ctx?.action !== "string" || ctx.action.trim().length === 0) {
|
|
146
|
+
throw new Error(`[decidio] describe(...) must return an \`action\` string naming what is being asked (got ${JSON.stringify(ctx?.action ?? null)}). Nothing was routed or executed.`);
|
|
147
|
+
}
|
|
148
|
+
if (typeof ctx.amount !== "number" || !Number.isFinite(ctx.amount) || ctx.amount < 0) {
|
|
149
|
+
throw new Error(`[decidio] describe(...) must return a finite, non-negative \`amount\` — the policy decides on it, so a request without one is not a request Decidio can rule on (got ${JSON.stringify(ctx.amount ?? null)}). Use 0 for an action with no monetary value. Nothing was routed or executed.`);
|
|
150
|
+
}
|
|
145
151
|
const auth = await post(config, "/agent/authorize", {
|
|
146
152
|
requester: config.agentId, action: ctx.action, amount: ctx.amount,
|
|
147
153
|
scope: ctx.scope, sourceSystem: config.sourceSystem,
|
|
@@ -151,6 +157,9 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
151
157
|
});
|
|
152
158
|
if (auth.decision !== "proceed" && auth.decision !== "route" && auth.decision !== "block")
|
|
153
159
|
throw new DecidioBlockedError(`unrecognized gate verdict: ${String(auth.decision)}`, auth.decisionId);
|
|
160
|
+
const idProblem = decisionIdProblem(auth.decisionId);
|
|
161
|
+
if (idProblem)
|
|
162
|
+
throw new DecidioBlockedError(`the gate answered "${auth.decision}" with no usable decisionId (${JSON.stringify(auth.decisionId ?? null)} — ${idProblem}) — refusing to execute. Every authorized action must be addressable: without an id it cannot be polled, resumed, confirmed or audited.`, typeof auth.decisionId === "string" ? auth.decisionId : String(auth.decisionId ?? ""));
|
|
154
163
|
if (auth.decision === "block")
|
|
155
164
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
156
165
|
if (auth.decision === "route") {
|
|
@@ -163,7 +172,7 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
163
172
|
config.resume.agentId ??= config.agentId;
|
|
164
173
|
config.resume.agentKey ??= config.agentKey;
|
|
165
174
|
config.resume.agentDid ??= config.agentDid;
|
|
166
|
-
config.resume.register(ctx.action, fn);
|
|
175
|
+
config.resume.register(ctx.action, fn, opts?.evidence);
|
|
167
176
|
await config.resume.park({
|
|
168
177
|
decisionId: auth.decisionId, receiptId: auth.receiptId,
|
|
169
178
|
action: ctx.action, amount: ctx.amount, scope: ctx.scope, args,
|
|
@@ -186,21 +195,44 @@ export function withApproval(fn, describe, config, opts) {
|
|
|
186
195
|
await sleep(pollInterval);
|
|
187
196
|
}
|
|
188
197
|
}
|
|
189
|
-
const result =
|
|
198
|
+
const result = opts?.withContext
|
|
199
|
+
? await fn({ decisionId: auth.decisionId, attemptId: null, idempotencyKey: auth.decisionId }, ...args)
|
|
200
|
+
: await fn(...args);
|
|
201
|
+
let reported = null;
|
|
202
|
+
let evidenceProblem;
|
|
190
203
|
try {
|
|
191
|
-
const confirmIdentity = signConfirm(auth.decisionId, config);
|
|
192
204
|
const evidenceOf = (opts?.evidence ?? ((r) => r));
|
|
193
|
-
|
|
205
|
+
reported = canonicalize(evidenceOf(result) ?? null, "evidence(result)");
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
evidenceProblem = String(e?.message ?? e);
|
|
209
|
+
console.warn(`[decidio] ${auth.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 (e.g. evidence: (r) => ({ recordId: String(r.id), success: true })) to earn a confirming tier. The action was NOT re-run.`);
|
|
210
|
+
}
|
|
211
|
+
let confirmation;
|
|
212
|
+
try {
|
|
213
|
+
const confirmIdentity = signConfirm(auth.decisionId, config);
|
|
194
214
|
const confirmBody = await post(config, "/agent/confirm", buildConfirmBody(config, auth.decisionId, reported, confirmIdentity));
|
|
195
|
-
|
|
196
|
-
if (!
|
|
197
|
-
console.warn(`[decidio] ${auth.decisionId}: ${
|
|
198
|
-
else if (!
|
|
199
|
-
console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as ${
|
|
215
|
+
confirmation = readConfirmResponse(confirmBody, { signed: !!confirmIdentity, capturedResponse: reported, journalled: false });
|
|
216
|
+
if (!confirmation.reportRecorded)
|
|
217
|
+
console.warn(`[decidio] ${auth.decisionId}: ${confirmation.diagnostic}`);
|
|
218
|
+
else if (!confirmation.executionConfirmed || confirmation.executionReportVerified === false)
|
|
219
|
+
console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as ${confirmation.evidenceTier} — ${confirmation.diagnostic}`);
|
|
220
|
+
if (evidenceProblem)
|
|
221
|
+
console.warn(`[decidio] ${auth.decisionId}: the execution IS recorded, but with an empty captured response — see the evidence note above.`);
|
|
200
222
|
}
|
|
201
223
|
catch (e) {
|
|
202
|
-
|
|
224
|
+
const isHttp = e instanceof DecidioHttpError;
|
|
225
|
+
console.warn(`[decidio] confirmation post failed (action succeeded): ${e?.message ?? e}`);
|
|
226
|
+
confirmation = {
|
|
227
|
+
reportRecorded: false,
|
|
228
|
+
executionConfirmed: false,
|
|
229
|
+
confirmationRetryable: isHttp ? e.retryable : true,
|
|
230
|
+
reportJournalled: false,
|
|
231
|
+
diagnostic: `your action RAN and Decidio does NOT have its execution report (${e?.message ?? e}). This path journals nothing, so there is no local copy to re-send: record the outcome from your own system. Durable mode DOES journal reports, and its recovery path can re-send them — that is the mode to use if this matters to you. Do NOT re-run the action.`,
|
|
232
|
+
};
|
|
203
233
|
}
|
|
204
|
-
|
|
234
|
+
if (evidenceProblem)
|
|
235
|
+
confirmation.evidenceError = evidenceProblem;
|
|
236
|
+
return { value: result, decisionId: auth.decisionId, confirmation };
|
|
205
237
|
};
|
|
206
238
|
}
|
package/dist/inngest.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export declare function decidioGateStep<R>(args: {
|
|
|
45
45
|
ctx: ApprovalContext;
|
|
46
46
|
run: () => Promise<R>;
|
|
47
47
|
timeout?: string;
|
|
48
|
+
/** Map your action's return value to the evidence Decidio records — the same option the core
|
|
49
|
+
* path takes. This door's own confirm chokepoint tells callers to use it; until 0.2.0 no
|
|
50
|
+
* adapter door accepted one, so the advice was unfollowable everywhere it was printed. */
|
|
51
|
+
evidence?: (result: R) => unknown;
|
|
48
52
|
}): Promise<GateStepOutcome<R>>;
|
|
49
53
|
/**
|
|
50
54
|
* Decidio → Inngest bridge. Mount as an HTTP endpoint and register it as the agent's
|
package/dist/inngest.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function decidioGateStep(args) {
|
|
|
22
22
|
return { status: "blocked", decisionId: auth.decisionId };
|
|
23
23
|
}
|
|
24
24
|
const result = await step.run("agent-action", args.run);
|
|
25
|
-
await step.run("decidio-confirm", async () => { await gateConfirm(config, auth.decisionId, result); return true; });
|
|
25
|
+
await step.run("decidio-confirm", async () => { await gateConfirm(config, auth.decisionId, result, args.evidence); return true; });
|
|
26
26
|
return { status: "executed", decisionId: auth.decisionId, result };
|
|
27
27
|
}
|
|
28
28
|
export function createInngestResumeBridge(opts) {
|