@decidio/sdk 0.1.7 → 0.1.9
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 +59 -20
- package/dist/adapters/_gate.d.ts +2 -0
- package/dist/adapters/_gate.js +25 -2
- package/dist/adapters/langgraph.d.ts +13 -0
- package/dist/adapters/langgraph.js +14 -0
- package/dist/adapters/openai.d.ts +24 -0
- package/dist/adapters/openai.js +4 -0
- package/dist/adapters/temporal.d.ts +16 -0
- package/dist/adapters/temporal.js +17 -1
- package/dist/cli.js +728 -213
- package/dist/guard.d.ts +29 -0
- package/dist/guard.js +25 -0
- package/dist/index.d.ts +54 -2
- package/dist/index.js +123 -7
- package/dist/inngest.d.ts +20 -0
- package/dist/inngest.js +26 -0
- package/dist/resume.d.ts +89 -0
- package/dist/resume.js +157 -18
- package/dist/signing.d.ts +20 -0
- package/dist/signing.js +73 -0
- package/openapi.yaml +1 -1
- package/package.json +8 -2
package/dist/signing.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
// Client-side agent identity (§13 P0-8). The agent generates its Ed25519 keypair LOCALLY
|
|
2
|
+
// (the private key never crosses the wire — `init` registers only the public did:key) and
|
|
3
|
+
// signs a REQUEST-BOUND proof on each authorize: a detached Ed25519 JWS over
|
|
4
|
+
// canonical({agentId,did,action,amount,scope,nonce,exp}). Decidio's floor verifies the
|
|
5
|
+
// signature, binds it to the exact request, burns the single-use nonce, and checks the
|
|
6
|
+
// did against the agent registry — so the gate PROVES who is calling instead of trusting a
|
|
7
|
+
// label. This canonicalize + proof shape is byte-identical to apps/api (canonical.ts +
|
|
8
|
+
// a2a/identity.ts), so a proof signed here verifies there.
|
|
1
9
|
import { generateKeyPairSync, createPrivateKey, sign as edSign, randomUUID } from "node:crypto";
|
|
10
|
+
// base58btc (Bitcoin alphabet) — for the did:key multibase 'z' prefix.
|
|
2
11
|
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
3
12
|
function b58encode(buf) {
|
|
4
13
|
const digits = [0];
|
|
@@ -25,6 +34,7 @@ function b58encode(buf) {
|
|
|
25
34
|
str += B58[digits[i]];
|
|
26
35
|
return str;
|
|
27
36
|
}
|
|
37
|
+
/** Sorted-key JCS — must match apps/api/src/canonical.ts byte-for-byte. */
|
|
28
38
|
export function canonicalize(v) {
|
|
29
39
|
if (v === null || typeof v !== "object")
|
|
30
40
|
return JSON.stringify(v);
|
|
@@ -33,6 +43,9 @@ export function canonicalize(v) {
|
|
|
33
43
|
const o = v;
|
|
34
44
|
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
|
|
35
45
|
}
|
|
46
|
+
/** Generate a fresh Ed25519 keypair LOCALLY. Returns the public `did:key` (register this)
|
|
47
|
+
* and the PKCS8 private key base64 (keep this in the agent host's env as DECIDIO_AGENT_KEY —
|
|
48
|
+
* it never goes back to Decidio). */
|
|
36
49
|
export function generateKeypair() {
|
|
37
50
|
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
38
51
|
const jwk = publicKey.export({ format: "jwk" });
|
|
@@ -41,18 +54,78 @@ export function generateKeypair() {
|
|
|
41
54
|
const privateKeyBase64 = privateKey.export({ format: "der", type: "pkcs8" }).toString("base64");
|
|
42
55
|
return { did, privateKeyBase64 };
|
|
43
56
|
}
|
|
57
|
+
/** Sign the CONFIRM call (bound to {action:"confirm", scope:decisionId}) so the gate can
|
|
58
|
+
* prove the caller is the receipt's original requester before granting the wrapper tier
|
|
59
|
+
* (application_confirmed). Returns undefined when no signing key is configured (the server
|
|
60
|
+
* then caps the report at agent_asserted). Used by EVERY confirm path — inline, durable
|
|
61
|
+
* resume, and the engine adapters — so the production paths confirm with a proof too. Also
|
|
62
|
+
* binds `workspaceId` (D-B3.6) when configured, so the confirm proof is tenant-scoped too. */
|
|
44
63
|
export function signConfirm(decisionId, cfg) {
|
|
45
64
|
if (!cfg.agentKey || !cfg.agentDid || !cfg.agentId)
|
|
46
65
|
return undefined;
|
|
47
66
|
return signBoundProof(cfg.agentKey, { agentId: cfg.agentId, did: cfg.agentDid, action: "confirm", amount: 0, scope: decisionId, workspaceId: cfg.workspaceId });
|
|
48
67
|
}
|
|
68
|
+
/** Sign a request-bound identity proof. The returned object is sent verbatim as the
|
|
69
|
+
* `requesterIdentity` field of /agent/authorize. */
|
|
49
70
|
export function signBoundProof(privateKeyBase64, fields) {
|
|
50
71
|
const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, "base64"), format: "der", type: "pkcs8" });
|
|
51
72
|
const nonce = fields.nonce ?? randomUUID();
|
|
52
73
|
const exp = (fields.now ?? Date.now()) + (fields.ttlMs ?? 120_000);
|
|
74
|
+
// OMIT scope/workspaceId when absent so the canonical bytes match the server + the Python SDK — a key
|
|
75
|
+
// with an undefined value would serialize as literal "undefined", which Python can't reproduce, and a
|
|
76
|
+
// legacy (no-workspace) proof must stay byte-identical. The server's bound-payload reconstruction
|
|
77
|
+
// omits them the same way (a2a/identity.ts).
|
|
53
78
|
const payloadObj = { agentId: fields.agentId, did: fields.did, action: fields.action, amount: fields.amount, ...(fields.scope !== undefined ? { scope: fields.scope } : {}), ...(fields.workspaceId !== undefined ? { workspaceId: fields.workspaceId } : {}), nonce, exp };
|
|
54
79
|
const payload = Buffer.from(canonicalize(payloadObj), "utf8").toString("base64url");
|
|
55
80
|
const protectedHdr = Buffer.from(JSON.stringify({ alg: "EdDSA", kid: fields.did }), "utf8").toString("base64url");
|
|
56
81
|
const signature = Buffer.from(edSign(null, Buffer.from(`${protectedHdr}.${payload}`, "utf8"), key)).toString("base64url");
|
|
57
82
|
return { agentId: fields.agentId, did: fields.did, action: fields.action, amount: fields.amount, scope: fields.scope, workspaceId: fields.workspaceId, nonce, exp, proof: { protected: protectedHdr, signature } };
|
|
58
83
|
}
|
|
84
|
+
/** The values that do not survive `JSON.stringify → JSON.parse` with their meaning intact.
|
|
85
|
+
* Refused at the GATE, before anything is routed, signed, or parked (prodsec F-01: `NaN`
|
|
86
|
+
* canonicalized to `null` and a `Map` to `{}`, so the integrity signature matched the drifted
|
|
87
|
+
* bytes and the function EXECUTED with arguments that meant something else — the exact wrong
|
|
88
|
+
* execution the parked-payload signature exists to refuse). Throws naming the offending path;
|
|
89
|
+
* returns nothing. The supported data model is plain JSON: null, boolean, string, finite
|
|
90
|
+
* number, plain array, plain object. */
|
|
91
|
+
export function assertJsonSafe(value, path = "args") {
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const walk = (v, p) => {
|
|
94
|
+
if (v === null || typeof v === "boolean" || typeof v === "string")
|
|
95
|
+
return;
|
|
96
|
+
if (typeof v === "number") {
|
|
97
|
+
if (!Number.isFinite(v))
|
|
98
|
+
throw new Error(`[decidio] ${p} is ${String(v)} — a non-finite number does not survive JSON (it becomes null), so what a human approves would not be what executes. The decision was NOT routed; pass a finite number or a string.`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (typeof v === "undefined")
|
|
102
|
+
throw new Error(`[decidio] ${p} is undefined — JSON drops or nullifies it, so what a human approves would not be what executes. The decision was NOT routed; use null explicitly or omit the field.`);
|
|
103
|
+
if (typeof v === "bigint")
|
|
104
|
+
throw new Error(`[decidio] ${p} is a BigInt — JSON cannot represent it. The decision was NOT routed; pass it as a string.`);
|
|
105
|
+
if (typeof v === "function" || typeof v === "symbol")
|
|
106
|
+
throw new Error(`[decidio] ${p} is a ${typeof v} — it cannot be serialized for approval. The decision was NOT routed.`);
|
|
107
|
+
if (typeof v === "object") {
|
|
108
|
+
if (seen.has(v))
|
|
109
|
+
throw new Error(`[decidio] ${p} is part of a circular structure — it cannot be serialized for approval. The decision was NOT routed.`);
|
|
110
|
+
seen.add(v);
|
|
111
|
+
if (Array.isArray(v)) {
|
|
112
|
+
v.forEach((x, i) => walk(x, `${p}[${i}]`));
|
|
113
|
+
seen.delete(v);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const proto = Object.getPrototypeOf(v);
|
|
117
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
118
|
+
const name = v?.constructor?.name ?? "a class instance";
|
|
119
|
+
throw new Error(`[decidio] ${p} is ${name === "Object" ? "an exotic object" : `a ${name}`} — only plain JSON objects survive serialization with their meaning intact (a Map, for example, becomes {}). The decision was NOT routed; convert it to plain JSON first.`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof v.toJSON === "function")
|
|
122
|
+
throw new Error(`[decidio] ${p} defines toJSON() — its serialized form would differ from the value the signature covers. The decision was NOT routed; pass the plain serialized form instead.`);
|
|
123
|
+
for (const [k, x] of Object.entries(v))
|
|
124
|
+
walk(x, `${p}.${k}`);
|
|
125
|
+
seen.delete(v);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`[decidio] ${p} has unsupported type ${typeof v}. The decision was NOT routed.`);
|
|
129
|
+
};
|
|
130
|
+
walk(value, path);
|
|
131
|
+
}
|
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.9
|
|
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.9",
|
|
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,10 +81,16 @@
|
|
|
81
81
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@decidio/verify": "
|
|
84
|
+
"@decidio/verify": "0.1.8"
|
|
85
85
|
},
|
|
86
86
|
"author": "OmniTwin Technologies Inc.",
|
|
87
87
|
"bugs": {
|
|
88
88
|
"url": "https://decidioai.com/support/"
|
|
89
|
+
},
|
|
90
|
+
"peerDependencies": {
|
|
91
|
+
"inngest": ">=3 <4",
|
|
92
|
+
"@langchain/langgraph": ">=0.2 <2",
|
|
93
|
+
"@temporalio/workflow": ">=1 <2",
|
|
94
|
+
"@openai/agents": ">=0.0.1 <2"
|
|
89
95
|
}
|
|
90
96
|
}
|