@decidio/sdk 0.1.6 → 0.1.8

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/dist/guard.d.ts CHANGED
@@ -17,12 +17,14 @@ export interface GuardFacadeConfig {
17
17
  export interface ProtectOptions {
18
18
  adapter?: "generic" | "langgraph" | "openai" | "temporal" | "inngest" | "mcp";
19
19
  mode?: "durable" | "blocking";
20
+ action?: string;
20
21
  }
21
22
  export declare class Guard {
22
23
  config: GuardFacadeConfig;
23
24
  resume?: ResumeController;
24
25
  constructor(config: GuardFacadeConfig, resume?: ResumeController);
25
26
  static fromEnv(): Guard;
27
+ register(action: string, fn: (...a: any[]) => Promise<any>): void;
26
28
  resumeHandler(): import("http").RequestListener;
27
29
  resumeFetchHandler(): (req: Request) => Promise<Response>;
28
30
  worker(opts?: {
package/dist/guard.js CHANGED
@@ -22,6 +22,7 @@ export class Guard {
22
22
  appUrl: process.env.DECIDIO_APP_URL,
23
23
  });
24
24
  }
25
+ register(action, fn) { this.ensureResume().register(action, fn); }
25
26
  resumeHandler() { return this.ensureResume().webhookHandler(); }
26
27
  resumeFetchHandler() { return this.ensureResume().fetchHandler(); }
27
28
  worker(opts) {
@@ -53,7 +54,10 @@ export class Guard {
53
54
  "Omit mode:\"blocking\" for durable async resume on real (minutes-to-days) approvals.");
54
55
  return withApproval(fn, describe, base);
55
56
  }
56
- return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume: this.ensureResume() });
57
+ const resume = this.ensureResume();
58
+ if (opts?.action)
59
+ resume.register(opts.action, fn);
60
+ return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume });
57
61
  }
58
62
  }
59
63
  export const guard = Guard.fromEnv();
package/dist/index.d.ts CHANGED
@@ -45,6 +45,12 @@ export declare class DecidioTimeoutError extends Error {
45
45
  decisionId: string;
46
46
  constructor(decisionId: string);
47
47
  }
48
+ export declare class DecidioRateLimitError extends Error {
49
+ limit: number | undefined;
50
+ remaining: number | undefined;
51
+ resetAt: string | undefined;
52
+ constructor(limit: number | undefined, remaining: number | undefined, resetAt: string | undefined, serverMessage?: string);
53
+ }
48
54
  export declare class DecidioNetworkError extends Error {
49
55
  endpoint: string;
50
56
  op: string;
package/dist/index.js CHANGED
@@ -32,12 +32,28 @@ export class DecidioTimeoutError extends Error {
32
32
  }
33
33
  }
34
34
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
35
+ export class DecidioRateLimitError extends Error {
36
+ limit;
37
+ remaining;
38
+ resetAt;
39
+ constructor(limit, remaining, resetAt, serverMessage) {
40
+ super(`[decidio] governed-action rate limit reached${limit != null ? ` (${limit}/hour on this workspace)` : ""}${resetAt ? ` — resets ${resetAt}` : ""}. ` +
41
+ `${serverMessage ? serverMessage + " " : ""}Nothing was routed or executed; retry after the reset.`);
42
+ this.limit = limit;
43
+ this.remaining = remaining;
44
+ this.resetAt = resetAt;
45
+ this.name = "DecidioRateLimitError";
46
+ }
47
+ }
35
48
  export class DecidioNetworkError extends Error {
36
49
  endpoint;
37
50
  op;
38
51
  code;
39
52
  constructor(endpoint, op, code, cause) {
40
53
  super(`[decidio] could not reach ${endpoint} during ${op}${code ? ` (${code})` : ""}. ` +
54
+ (/localhost|127\.0\.0\.1/.test(endpoint) && code === "ECONNREFUSED"
55
+ ? `A refused localhost connection usually means the SDK never saw your config: if your settings live in .env, run node with --env-file=.env (or export DECIDIO_API_URL — the hosted sandbox is https://decidio-api.onrender.com). `
56
+ : "") +
41
57
  `Nothing was executed locally and no verdict was received — if a request reached Decidio it is ` +
42
58
  `visible in your queue. Check the network/URL (DECIDIO_API_URL) and retry; retrying is safe ` +
43
59
  `(a retry creates a new request — it can never double-execute).`);
@@ -63,6 +79,10 @@ async function post(cfg, path, body) {
63
79
  headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
64
80
  body: JSON.stringify(body),
65
81
  });
82
+ if (res.status === 429) {
83
+ const b = await res.json().catch(() => null);
84
+ throw new DecidioRateLimitError(b?.limit, b?.remaining, b?.resetAt, b?.message);
85
+ }
66
86
  if (!res.ok)
67
87
  throw new Error(`decidio ${path} ${res.status}: ${await res.text().catch(() => "")}`);
68
88
  return res.json();
package/dist/resume.js CHANGED
@@ -57,7 +57,7 @@ export class FilePendingStore {
57
57
  key(id) { return id.replace(/[^\w.-]/g, "_"); }
58
58
  path(id) { return join(this.dir, `${this.key(id)}.json`); }
59
59
  claimedPath(id) { return join(this.dir, `${this.key(id)}.claimed`); }
60
- async put(p) { writeFileSync(this.path(p.decisionId), JSON.stringify(p, null, 2)); }
60
+ async put(p) { writeFileSync(this.path(p.decisionId), JSON.stringify(p, null, 2), { mode: 0o600 }); }
61
61
  async get(id) {
62
62
  for (const f of [this.path(id), this.claimedPath(id)])
63
63
  if (existsSync(f))
@@ -70,8 +70,16 @@ export class FilePendingStore {
70
70
  async list() {
71
71
  if (!existsSync(this.dir))
72
72
  return [];
73
- return readdirSync(this.dir).filter((f) => f.endsWith(".json"))
74
- .map((f) => JSON.parse(readFileSync(join(this.dir, f), "utf8")));
73
+ const out = [];
74
+ for (const f of readdirSync(this.dir).filter((f) => f.endsWith(".json"))) {
75
+ try {
76
+ out.push(JSON.parse(readFileSync(join(this.dir, f), "utf8")));
77
+ }
78
+ catch (e) {
79
+ console.error(`[decidio] skipping unreadable parked entry ${f} (${e.message}) — the other parked actions still resume; inspect or delete this file.`);
80
+ }
81
+ }
82
+ return out;
75
83
  }
76
84
  async claim(id) {
77
85
  try {
@@ -143,7 +151,12 @@ export function createDecidioResume(opts) {
143
151
  workspaceId: opts.workspaceId ?? process.env.DECIDIO_WORKSPACE_ID,
144
152
  webhookSecret: secret,
145
153
  store: opts.store,
146
- register(action, fn) { registry.set(action, fn); },
154
+ register(action, fn) {
155
+ const prev = registry.get(action);
156
+ if (prev && prev !== fn)
157
+ console.warn(`[decidio] action "${action}" is being registered again with a DIFFERENT function — the latest one wins, and a resumed decision would run IT. Give each protected function its own action name.`);
158
+ registry.set(action, fn);
159
+ },
147
160
  async park(p) {
148
161
  const argsSig = signArgs(p, ctrl.agentKey, ctrl.webhookSecret);
149
162
  if (!argsSig)
@@ -187,19 +200,36 @@ export function createDecidioResume(opts) {
187
200
  if (integrity === "unbound")
188
201
  console.warn(`[decidio] executing ${decisionId} from an UNBOUND parked entry (this agent has no key material to bind or verify payloads) — its arguments cannot be integrity-checked. Run \`npx @decidio/sdk init\` to enable binding.`);
189
202
  const fn = registry.get(pending.action);
190
- if (!fn)
191
- return finish({ status: "no_handler", decisionId, action: pending.action, reason: `no fn registered for action "${pending.action}" — call resume.register(...) at startup` });
203
+ if (!fn) {
204
+ console.error(`[decidio] cannot resume ${decisionId}: no function is registered for action "${pending.action}" in THIS process.`);
205
+ console.error(` A worker only re-executes actions registered where it runs. At startup, either declare the action on the wrapper:`);
206
+ console.error(` const doIt = guard.protect(doItRaw, describe, { action: "${pending.action}" });`);
207
+ console.error(` or register the raw function directly:`);
208
+ console.error(` guard.register("${pending.action}", doItRaw);`);
209
+ console.error(` then start the worker. The approved action stays parked until then — nothing is lost.`);
210
+ return finish({ status: "no_handler", decisionId, action: pending.action, reason: `no fn registered for action "${pending.action}" in this process — declare { action } on protect(), or call guard.register("${pending.action}", fn) at startup` });
211
+ }
192
212
  const claimed = ctrl.store.claim ? await ctrl.store.claim(decisionId) : (!inFlight.has(decisionId) && (inFlight.add(decisionId), true));
193
213
  if (!claimed)
194
214
  return { status: "unknown", decisionId };
195
215
  try {
196
216
  const result = await fn(...pending.args);
197
- await ctrl.store.delete(decisionId);
217
+ let confirmed = true;
198
218
  try {
199
219
  await confirm(ctrl, decisionId, result);
200
220
  }
201
221
  catch (e) {
202
- console.warn(`[decidio] resume confirm failed (action succeeded): ${e.message}`);
222
+ confirmed = false;
223
+ console.warn(`[decidio] resume confirm failed (your action DID succeed): ${e.message}`);
224
+ }
225
+ const retainedIsSafe = typeof ctrl.store.claim === "function";
226
+ if (confirmed || !retainedIsSafe)
227
+ await ctrl.store.delete(decisionId);
228
+ if (!confirmed) {
229
+ if (retainedIsSafe)
230
+ console.warn(` The parked entry for ${decisionId} is kept as local evidence that it executed — it stays CLAIMED, so it cannot re-run. Retry the confirmation, then delete it.`);
231
+ else
232
+ console.warn(` This store provides no claim() primitive, so the entry was DELETED to guarantee the action cannot execute twice. ${decisionId} executed, but its confirmation never reached Decidio — re-confirm it from your own records.`);
203
233
  }
204
234
  return finish({ status: "executed", decisionId, action: pending.action, result });
205
235
  }
package/openapi.yaml CHANGED
@@ -5,7 +5,7 @@
5
5
  openapi: 3.1.0
6
6
  info:
7
7
  title: Decidio Agent Gate
8
- version: 0.1.6
8
+ version: 0.1.8
9
9
  description: >
10
10
  Authorize an AI-agent action (proceed | route | block), confirm the captured outcome, and
11
11
  receive the signed resume webhook when a routed action is decided. Every outcome seals a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decidio/sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "One-line approval gate for AI-agent actions. Wrap a risky tool call with guard.protect(); Decidio's policy engine decides proceed | route | block, a human approves routed actions, and every outcome is sealed as a verifiable Authority Receipt.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -81,7 +81,7 @@
81
81
  "prepublishOnly": "npm run typecheck && npm run build"
82
82
  },
83
83
  "dependencies": {
84
- "@decidio/verify": "^0.1.3"
84
+ "@decidio/verify": "0.1.7"
85
85
  },
86
86
  "author": "OmniTwin Technologies Inc.",
87
87
  "bugs": {