@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/README.md +72 -22
- package/dist/cli.js +512 -110
- package/dist/guard.d.ts +2 -0
- package/dist/guard.js +5 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +20 -0
- package/dist/resume.js +38 -8
- package/openapi.yaml +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
217
|
+
let confirmed = true;
|
|
198
218
|
try {
|
|
199
219
|
await confirm(ctrl, decisionId, result);
|
|
200
220
|
}
|
|
201
221
|
catch (e) {
|
|
202
|
-
|
|
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.
|
|
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.
|
|
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": "
|
|
84
|
+
"@decidio/verify": "0.1.7"
|
|
85
85
|
},
|
|
86
86
|
"author": "OmniTwin Technologies Inc.",
|
|
87
87
|
"bugs": {
|