@decidio/sdk 0.1.9 → 0.1.10
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 +9 -4
- package/dist/adapters/_gate.js +9 -20
- package/dist/adapters/langgraph.js +0 -14
- package/dist/adapters/openai.js +0 -4
- package/dist/adapters/temporal.js +1 -17
- package/dist/cli.js +32 -254
- package/dist/guard.d.ts +9 -7
- package/dist/guard.js +12 -26
- package/dist/index.d.ts +7 -9
- package/dist/index.js +17 -98
- package/dist/inngest.js +0 -26
- package/dist/resume.d.ts +11 -12
- package/dist/resume.js +31 -118
- package/dist/signing.d.ts +16 -6
- package/dist/signing.js +23 -40
- package/openapi.yaml +1 -1
- package/package.json +3 -9
package/README.md
CHANGED
|
@@ -126,11 +126,14 @@ becomes the record's execution evidence.
|
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
128
|
npx @decidio/sdk receipt <decisionId>
|
|
129
|
-
npx @decidio/sdk verify decidio-receipt-<decisionId>.json
|
|
129
|
+
npx @decidio/sdk verify --issuer <did:key:...> decidio-receipt-<decisionId>.json
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
Lead with the pin: `--issuer` binds the check to YOUR workspace's DID (the `receipt` command
|
|
133
|
+
prints it from the authenticated response, so it is already on your screen). An UNPINNED
|
|
134
|
+
`verify` proves internal consistency only — any keyholder could have issued such a file — and
|
|
135
|
+
says so; pinned verification proves *who*. The download is an authenticated fetch from your
|
|
136
|
+
workspace; the *verification* is fully offline — the verifier ships inside this package (no second download, no registry probe, same
|
|
134
137
|
implementation as the standalone `@decidio/verify`). The receipt is a W3C Verifiable
|
|
135
138
|
Credential: signature and content binding verify with no Decidio account and no network — the
|
|
136
139
|
evidence is yours, not ours. To also prove *who* issued it, pin your workspace's issuer:
|
|
@@ -278,7 +281,9 @@ If your agent runs on Inngest/Temporal/LangGraph, the engine *is* the durable st
|
|
|
278
281
|
`@decidio/sdk/inngest` maps the gate onto `step.waitForEvent`; `@decidio/sdk/langgraph` is a
|
|
279
282
|
drop-in via `interrupt()`; `@decidio/sdk/temporal` uses `condition` + signal;
|
|
280
283
|
`@decidio/sdk/openai` gates RunState approvals. All optional peer dependencies — the core SDK
|
|
281
|
-
imports none of them
|
|
284
|
+
imports none of them — install the engine you use alongside the SDK (`npm i inngest`,
|
|
285
|
+
`@langchain/langgraph`, `@temporalio/workflow`, or `@openai/agents`); nothing is downloaded
|
|
286
|
+
for engines you don't. Each adapter's shipped `.d.ts` header documents its wiring; `npx
|
|
282
287
|
@decidio/sdk quickstart` is the fastest runnable end-to-end.
|
|
283
288
|
|
|
284
289
|
## One surface, every runtime — and Python too
|
package/dist/adapters/_gate.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
// reuses these gate calls + verdict handling; only the suspend/resume translation
|
|
3
|
-
// differs. Pure/testable so the offline smoke covers our logic; the engine-primitive
|
|
4
|
-
// call in each adapter is a thin one-liner verified by typecheck against real types.
|
|
5
|
-
import { DecidioBlockedError, DecidioRejectedError, signBoundProof, signConfirm, assertJsonSafe, assertConfirmRecorded } from "../index.js";
|
|
1
|
+
import { DecidioBlockedError, DecidioRejectedError, signBoundProof, signConfirm, assertJsonSafe, assertConfirmRecorded, assertSafeTransport } from "../index.js";
|
|
6
2
|
export async function gateAuthorize(config, ctx) {
|
|
7
|
-
// SAME JSON-safety gate as the core wrapper (guard equivalent-path catch: the adapter door
|
|
8
|
-
// skipped it, and the server's amount coercion turns a drifted NaN→null into a $0 request —
|
|
9
|
-
// a human would approve an amount that meant something else entirely).
|
|
10
3
|
assertJsonSafe(ctx, "describe(...)");
|
|
4
|
+
assertSafeTransport(config.apiUrl, !!config.apiToken);
|
|
11
5
|
const f = config.fetchImpl ?? fetch;
|
|
12
|
-
// Fall back to the resume controller's URL (matches the core wrapper) so an adapter
|
|
13
|
-
// decision carries a callback URL even when only resume.resumeUrl was set.
|
|
14
6
|
const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
|
|
15
|
-
// Sign the bound identity proof like the core wrapper, so an adapter-path agent can PROVE
|
|
16
|
-
// its identity and auto-approve (not just route). No key → unsigned → server routes.
|
|
17
7
|
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
18
8
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
19
9
|
: undefined;
|
|
@@ -33,10 +23,14 @@ export async function gateAuthorize(config, ctx) {
|
|
|
33
23
|
export async function gateConfirm(config, decisionId, result) {
|
|
34
24
|
const f = config.fetchImpl ?? fetch;
|
|
35
25
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
assertSafeTransport(config.apiUrl, !!config.apiToken);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
38
33
|
const requesterIdentity = signConfirm(decisionId, config);
|
|
39
|
-
// Same no-silent-downgrade rule as the inline wrap (guard equivalent-path catch).
|
|
40
34
|
if (!requesterIdentity)
|
|
41
35
|
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.`);
|
|
42
36
|
const res = await f(config.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
|
|
@@ -44,24 +38,19 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
44
38
|
headers: { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) },
|
|
45
39
|
body: JSON.stringify({ decisionId, capturedResponse: result ?? null, source: "wrapper", ...(requesterIdentity ? { requesterIdentity } : {}) }),
|
|
46
40
|
});
|
|
47
|
-
// fetch only rejects on network error — surface a non-2xx (auth/500) too. Best-effort:
|
|
48
|
-
// log, don't throw (the action already happened).
|
|
49
41
|
if (!res.ok) {
|
|
50
42
|
console.warn(`[decidio] confirm returned ${res.status} (action succeeded)`);
|
|
51
43
|
return;
|
|
52
44
|
}
|
|
53
|
-
// The ONE confirm-body rule, on this door too (guard equivalent-path catch).
|
|
54
45
|
assertConfirmRecorded(await res.json().catch(() => null), decisionId, !!requesterIdentity);
|
|
55
46
|
}
|
|
56
47
|
catch (e) {
|
|
57
48
|
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
58
49
|
}
|
|
59
50
|
}
|
|
60
|
-
/** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
|
|
61
51
|
export function verdictFrom(payload) {
|
|
62
52
|
return (payload && typeof payload === "object" ? payload.verdict : payload) ?? "";
|
|
63
53
|
}
|
|
64
|
-
/** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
|
|
65
54
|
export function assertVerdict(verdict, decisionId, reason) {
|
|
66
55
|
if (verdict === "approved" || verdict === "auto_approved")
|
|
67
56
|
return;
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import { interrupt, Command } from "@langchain/langgraph";
|
|
2
2
|
import { DecidioBlockedError } from "../index.js";
|
|
3
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
|
|
4
|
-
/**
|
|
5
|
-
* @decidio/sdk/langgraph — durable suspend via LangGraph's native `interrupt()`.
|
|
6
|
-
* `interrupt` finds the running graph itself (no handle needed), so the gate drops into
|
|
7
|
-
* a node: authorize → on route, interrupt() durably pauses (the checkpointer persists;
|
|
8
|
-
* the process can die) → on resume (Command(resume={verdict})) run the agent's own
|
|
9
|
-
* action + confirm. Decidio is the approval system; the host bridges its webhook to the
|
|
10
|
-
* graph resume (decidioResumeCommand). Requires @langchain/langgraph (optional peer dep).
|
|
11
|
-
*
|
|
12
|
-
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
13
|
-
*/
|
|
14
|
-
/** Gate `run()` behind Decidio inside a LangGraph node. */
|
|
15
4
|
export async function decidioGate(config, ctx, run) {
|
|
16
5
|
const auth = await gateAuthorize(config, ctx);
|
|
17
6
|
if (auth.decision === "block")
|
|
18
7
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
19
8
|
if (auth.decision === "route") {
|
|
20
|
-
// Durable pause. On resume, interrupt() returns the Command(resume=...) payload.
|
|
21
9
|
const resumed = interrupt({ decidio: { decisionId: auth.decisionId, action: ctx.action, amount: ctx.amount, scope: ctx.scope } });
|
|
22
10
|
assertVerdict(verdictFrom(resumed), auth.decisionId, resumed?.reason);
|
|
23
11
|
}
|
|
@@ -25,8 +13,6 @@ export async function decidioGate(config, ctx, run) {
|
|
|
25
13
|
await gateConfirm(config, auth.decisionId, result);
|
|
26
14
|
return result;
|
|
27
15
|
}
|
|
28
|
-
/** Translate Decidio's signed resume webhook into the LangGraph resume Command. The host
|
|
29
|
-
* maps decisionId → thread_id and passes this to graph.invoke/stream. */
|
|
30
16
|
export function decidioResumeCommand(body) {
|
|
31
17
|
return new Command({ resume: { verdict: body.verdict, reason: body.reason } });
|
|
32
18
|
}
|
package/dist/adapters/openai.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { gateAuthorize } from "./_gate.js";
|
|
2
|
-
/** Ask Decidio for each pending tool-approval on a RunState.
|
|
3
|
-
* proceed → state.approve(item); block → state.reject(item); route → pending (a human
|
|
4
|
-
* decides via Decidio; the host parks state.toString() + resumes via applyResume). */
|
|
5
2
|
export async function gateInterruptions(config, state, describe) {
|
|
6
3
|
const resolved = [];
|
|
7
4
|
const pending = [];
|
|
@@ -20,7 +17,6 @@ export async function gateInterruptions(config, state, describe) {
|
|
|
20
17
|
}
|
|
21
18
|
return { resolved, pending };
|
|
22
19
|
}
|
|
23
|
-
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
|
|
24
20
|
export function applyResume(state, item, verdict) {
|
|
25
21
|
if (verdict === "approved" || verdict === "auto_approved")
|
|
26
22
|
state.approve(item);
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { condition, defineSignal, setHandler } from "@temporalio/workflow";
|
|
2
2
|
import { DecidioBlockedError } from "../index.js";
|
|
3
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
|
|
4
|
-
/**
|
|
5
|
-
* @decidio/sdk/temporal — durable suspend via Temporal's native `condition()` + a
|
|
6
|
-
* Decidio resume signal. Strongest durability (the workflow survives worker restarts for
|
|
7
|
-
* days); the agent's action runs on resume. Requires @temporalio/workflow (optional peer).
|
|
8
|
-
*
|
|
9
|
-
* Inside your workflow:
|
|
10
|
-
* const verdicts = installDecidioSignal(); // wires the resume signal
|
|
11
|
-
* const out = await decidioGate(config, ctx, () => createOpp(o), verdicts, {
|
|
12
|
-
* authorize: () => executeActivity(authorizeActivity, ...), // HTTP belongs in activities
|
|
13
|
-
* confirm: (id, r) => executeActivity(confirmActivity, ...),
|
|
14
|
-
* });
|
|
15
|
-
* Decidio's webhook bridges to: handle.signal(decidioResumeSignal, decisionId, verdict, reason)
|
|
16
|
-
*
|
|
17
|
-
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
18
|
-
*/
|
|
19
4
|
export const decidioResumeSignal = defineSignal("decidioResume");
|
|
20
|
-
/** Call inside the workflow to wire the resume signal; returns the verdict store. */
|
|
21
5
|
export function installDecidioSignal() {
|
|
22
6
|
const verdicts = new Map();
|
|
23
7
|
setHandler(decidioResumeSignal, (decisionId, verdict, reason) => { verdicts.set(decisionId, { verdict, reason }); });
|
|
@@ -28,7 +12,7 @@ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
|
28
12
|
if (auth.decision === "block")
|
|
29
13
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
30
14
|
if (auth.decision === "route") {
|
|
31
|
-
await condition(() => verdicts.has(auth.decisionId));
|
|
15
|
+
await condition(() => verdicts.has(auth.decisionId));
|
|
32
16
|
const v = verdicts.get(auth.decisionId);
|
|
33
17
|
assertVerdict(verdictFrom(v), auth.decisionId, v.reason);
|
|
34
18
|
}
|