@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 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
- The download is an authenticated fetch from your workspace; the *verification* is fully
133
- offline — the verifier ships inside this package (no second download, no registry probe, same
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. Each adapter's shipped `.d.ts` header documents its wiring; `npx
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
@@ -1,19 +1,9 @@
1
- // Shared adapter gate logic (TS) — the thin-adapter rule in code. Every engine adapter
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
- // Sign the confirm too (bound to {action:"confirm", scope:decisionId}) so the adapter path
37
- // earns application_confirmed; without a key the server caps it at agent_asserted.
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
  }
@@ -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)); // durable wait on the signal
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
  }