invinoveritas-governance-gate-core 0.1.0
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 +98 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +132 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# invinoveritas-governance-gate-core
|
|
2
|
+
|
|
3
|
+
The framework-agnostic core for an **independent pre-execution governance gate**.
|
|
4
|
+
|
|
5
|
+
An agent can self-serve memory, tools, reasoning, even a wallet. The one thing it cannot self-serve
|
|
6
|
+
is an **independent second opinion on its own proposed action** — you can't self-issue a verdict that
|
|
7
|
+
a third party trusts. This core calls invinoveritas [`/review`](https://api.babyblueviper.com) for
|
|
8
|
+
that verdict and returns a normalized gate decision plus a **portable, recomputable proof**: a
|
|
9
|
+
BIP-340-signed Nostr event, Bitcoin-anchored on the public [`/ledger`](https://api.babyblueviper.com/ledger).
|
|
10
|
+
Anyone can re-verify the proof offline via `/verify-proof` **without trusting the presenter or us.**
|
|
11
|
+
|
|
12
|
+
This is the shared primitive. Thin per-surface adapters wrap it — see
|
|
13
|
+
[`invinoveritas-metamask-snap`](../metamask-snap) (verdict before you sign a transaction), and the
|
|
14
|
+
LangGraph / OpenAI-Agents / GOAT patterns.
|
|
15
|
+
|
|
16
|
+
## Why this and not a self-signed receipt
|
|
17
|
+
|
|
18
|
+
Most "action receipt" schemes sign the record with **the agent's own key** — that proves *the agent
|
|
19
|
+
says it did X*, not *an independent party judged X sound before it ran*. And a signed hash-chain
|
|
20
|
+
proves internal ordering but can be regenerated wholesale. This gate is built around the two things a
|
|
21
|
+
self-signed receipt structurally lacks:
|
|
22
|
+
|
|
23
|
+
1. **Independence** — the verdict comes from a party that isn't the actor.
|
|
24
|
+
2. **External time anchor** — every verdict is Bitcoin-anchored (OpenTimestamps), so "judged before
|
|
25
|
+
the outcome" is checkable against a clock the operator doesn't control. Omission becomes as
|
|
26
|
+
legible as publication.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install invinoveritas-governance-gate-core
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { reviewAction } from "invinoveritas-governance-gate-core";
|
|
38
|
+
|
|
39
|
+
const result = await reviewAction(
|
|
40
|
+
{
|
|
41
|
+
artifact: JSON.stringify({ tool: "transfer", to: "0xabc…", amount: "1000 USDC" }),
|
|
42
|
+
artifactType: "onchain_action", // or "trade", "tool_call", "general"
|
|
43
|
+
context: "Agent is about to move treasury funds.",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
apiKey: process.env.INVINOVERITAS_API_KEY, // free: POST /register {"label":"your-app"}
|
|
47
|
+
failMode: "closed", // irreversible action -> BLOCK if the gate is unavailable
|
|
48
|
+
sign: true, // attach the recomputable proof (default)
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// result.decision : "ALLOW" | "BLOCK" | "ESCALATE"
|
|
53
|
+
// result.verdict : "approve" | "approve_with_concerns" | "reject" | "review_unavailable"
|
|
54
|
+
// result.proof : portable signed proof — hand it downstream
|
|
55
|
+
// result.recomputeProofAt : where anyone re-verifies it (free, no auth)
|
|
56
|
+
|
|
57
|
+
if (result.decision === "BLOCK") {
|
|
58
|
+
// hold the action; surface result.summary / result.issues
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Fail mode is explicit, per surface
|
|
63
|
+
|
|
64
|
+
The gate is **advisory by default** (`failMode: "open"`) — a slow or erroring gate never blocks the
|
|
65
|
+
host. For **irreversible / side-effecting** actions, set `failMode: "closed"` so gate-unavailability
|
|
66
|
+
blocks instead of silently passing, or `"escalate"` to route to a human/confirmation path. This is the
|
|
67
|
+
property a pure observability hook lacks: it can be made *authority*, not just a log.
|
|
68
|
+
|
|
69
|
+
### Receiving half of the handshake
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { verifyProof } from "invinoveritas-governance-gate-core";
|
|
73
|
+
|
|
74
|
+
// A proof another agent handed you — verify it without trusting them OR us.
|
|
75
|
+
const v = await verifyProof({ event: someProof.proof_payload });
|
|
76
|
+
// v.valid === true => the verdict is authentic and unmodified
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API
|
|
80
|
+
|
|
81
|
+
- `reviewAction(action, config) => Promise<GateResult>` — never throws; failures resolve per `failMode`.
|
|
82
|
+
- `verifyProof({ event | proofId }, config) => Promise<{ valid }>` — free, no auth.
|
|
83
|
+
|
|
84
|
+
| `GovernanceGateConfig` | default | |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `apiKey` | — | Bearer key (free via `/register`). Omitted ⇒ unfunded ⇒ `failMode` applies. |
|
|
87
|
+
| `baseUrl` | `https://api.babyblueviper.com` | |
|
|
88
|
+
| `timeoutMs` | `5000` | a gate must never hang the host |
|
|
89
|
+
| `failMode` | `"open"` | `"open"` ALLOW / `"closed"` BLOCK / `"escalate"` ESCALATE on gate-unavailable |
|
|
90
|
+
| `sign` | `true` | attach the recomputable proof |
|
|
91
|
+
|
|
92
|
+
`ProposedAction.stateHash` (optional): SHA-256 hex of the caller's state at request time — binds the
|
|
93
|
+
proof to both the action AND the state, so a verifier confirms the decision was made against the exact
|
|
94
|
+
state the caller had.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* invinoveritas-governance-gate-core
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic core for an INDEPENDENT pre-execution governance gate.
|
|
5
|
+
*
|
|
6
|
+
* The one thing an agent (or a wallet, or a middleware) cannot self-serve is an independent
|
|
7
|
+
* second opinion on its own proposed action — you can't self-issue a verdict that a third party
|
|
8
|
+
* trusts. This core calls invinoveritas `/review` for that verdict and returns a normalized
|
|
9
|
+
* gate decision plus a PORTABLE, RECOMPUTABLE proof (a BIP-340-signed Nostr event, Bitcoin-anchored
|
|
10
|
+
* on the public /ledger). Any party can re-verify the proof offline via `/verify-proof` without
|
|
11
|
+
* trusting the presenter OR us.
|
|
12
|
+
*
|
|
13
|
+
* Design invariants:
|
|
14
|
+
* - Advisory by default (failMode="open"): a slow/erroring gate NEVER blocks execution.
|
|
15
|
+
* - But fail mode is EXPLICIT and per-surface: side-effecting / irreversible surfaces should set
|
|
16
|
+
* failMode="closed" so gate-unavailability blocks rather than silently passing.
|
|
17
|
+
* - The verdict is from a party that ISN'T the actor (independence). The proof carries its own
|
|
18
|
+
* pubkey + verify URL (self-describing); checking it needs nothing out-of-band.
|
|
19
|
+
*
|
|
20
|
+
* This is the shared primitive; thin per-surface adapters (MetaMask Snap, LangGraph ToolNode,
|
|
21
|
+
* OpenAI Agents on_tool_start, etc.) wrap it. See integrations/metamask-snap for the first adapter.
|
|
22
|
+
*/
|
|
23
|
+
export type Verdict = "approve" | "approve_with_concerns" | "reject" | "review_unavailable";
|
|
24
|
+
/** What the host should do with the action. */
|
|
25
|
+
export type GateDecision = "ALLOW" | "BLOCK" | "ESCALATE";
|
|
26
|
+
/**
|
|
27
|
+
* Behavior when the gate itself is unavailable (timeout, error, 402-unfunded, malformed response).
|
|
28
|
+
* - "open": ALLOW (advisory — the default; the gate is a second opinion, not a blocker)
|
|
29
|
+
* - "closed": BLOCK (fail-safe — for irreversible / side-effecting surfaces)
|
|
30
|
+
* - "escalate": ESCALATE (hand to a human / confirmation path)
|
|
31
|
+
*/
|
|
32
|
+
export type FailMode = "open" | "closed" | "escalate";
|
|
33
|
+
export interface GovernanceGateConfig {
|
|
34
|
+
/** Bearer API key. Get one free: POST {baseUrl}/register {"label":"your-app"}. Omitted => unfunded => failMode applies. */
|
|
35
|
+
apiKey?: string;
|
|
36
|
+
/** API base. Default https://api.babyblueviper.com */
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
/** Per-call timeout. Default 5000ms — a gate must never hang the host. */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
/** What to do when the gate is unavailable. Default "open" (advisory). Set "closed" for irreversible actions. */
|
|
41
|
+
failMode?: FailMode;
|
|
42
|
+
/** Attach a portable recomputable proof to the verdict. Default true (the whole point — trust the math). */
|
|
43
|
+
sign?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface ProposedAction {
|
|
46
|
+
/** The thing to judge: a decoded tx, a tool call, a trade, a plan. For onchain_action, a human/JSON description of the tx. */
|
|
47
|
+
artifact: string;
|
|
48
|
+
/** Tailors the review. Use "onchain_action" for a prepared tx (scam/drainer/approval/poisoning checks), "trade", "tool_call", etc. */
|
|
49
|
+
artifactType?: string;
|
|
50
|
+
/** What this is trying to do / why now — helps the verdict judge intent vs. action. */
|
|
51
|
+
context?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Optional SHA-256 hex of the caller's state at request time. When set, the signed proof binds to
|
|
54
|
+
* BOTH the action AND the state — so a verifier confirms the decision was made against the exact
|
|
55
|
+
* state the caller had, not just the action proposed. Changing state after invalidates the proof.
|
|
56
|
+
*/
|
|
57
|
+
stateHash?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface GateResult {
|
|
60
|
+
/** Normalized host action. */
|
|
61
|
+
decision: GateDecision;
|
|
62
|
+
/** Raw verdict from the independent reviewer. */
|
|
63
|
+
verdict: Verdict;
|
|
64
|
+
confidence?: number;
|
|
65
|
+
summary?: string;
|
|
66
|
+
issues?: unknown[];
|
|
67
|
+
/** Deterministic on-chain risk findings (decode-the-calldata-yourself), present for onchain_action. */
|
|
68
|
+
onchainRisk?: unknown;
|
|
69
|
+
/** Portable signed proof — hand it downstream; anyone re-verifies it via recomputeProofAt. */
|
|
70
|
+
proof?: unknown;
|
|
71
|
+
/** Where to recompute/verify the proof (free, no auth). */
|
|
72
|
+
recomputeProofAt?: string;
|
|
73
|
+
/** Human-readable note when the gate was unavailable. */
|
|
74
|
+
reason?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get an independent, recomputable verdict on a proposed action before it executes.
|
|
78
|
+
* Never throws — on any failure it returns a `review_unavailable` result resolved per `failMode`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function reviewAction(action: ProposedAction, config?: GovernanceGateConfig): Promise<GateResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Verify a signed invinoveritas proof someone handed you — the receiving half of the handshake.
|
|
83
|
+
* FREE, no auth. Recomputes the Nostr event id + checks the BIP-340 signature against the published
|
|
84
|
+
* key, so you trust neither the presenter nor us. Pass the signed `event` object or a `proofId`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function verifyProof(input: {
|
|
87
|
+
event?: Record<string, unknown>;
|
|
88
|
+
proofId?: string;
|
|
89
|
+
}, config?: GovernanceGateConfig): Promise<{
|
|
90
|
+
valid: boolean;
|
|
91
|
+
[k: string]: unknown;
|
|
92
|
+
}>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* invinoveritas-governance-gate-core
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic core for an INDEPENDENT pre-execution governance gate.
|
|
5
|
+
*
|
|
6
|
+
* The one thing an agent (or a wallet, or a middleware) cannot self-serve is an independent
|
|
7
|
+
* second opinion on its own proposed action — you can't self-issue a verdict that a third party
|
|
8
|
+
* trusts. This core calls invinoveritas `/review` for that verdict and returns a normalized
|
|
9
|
+
* gate decision plus a PORTABLE, RECOMPUTABLE proof (a BIP-340-signed Nostr event, Bitcoin-anchored
|
|
10
|
+
* on the public /ledger). Any party can re-verify the proof offline via `/verify-proof` without
|
|
11
|
+
* trusting the presenter OR us.
|
|
12
|
+
*
|
|
13
|
+
* Design invariants:
|
|
14
|
+
* - Advisory by default (failMode="open"): a slow/erroring gate NEVER blocks execution.
|
|
15
|
+
* - But fail mode is EXPLICIT and per-surface: side-effecting / irreversible surfaces should set
|
|
16
|
+
* failMode="closed" so gate-unavailability blocks rather than silently passing.
|
|
17
|
+
* - The verdict is from a party that ISN'T the actor (independence). The proof carries its own
|
|
18
|
+
* pubkey + verify URL (self-describing); checking it needs nothing out-of-band.
|
|
19
|
+
*
|
|
20
|
+
* This is the shared primitive; thin per-surface adapters (MetaMask Snap, LangGraph ToolNode,
|
|
21
|
+
* OpenAI Agents on_tool_start, etc.) wrap it. See integrations/metamask-snap for the first adapter.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_BASE_URL = "https://api.babyblueviper.com";
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
25
|
+
function verdictToDecision(verdict, failMode) {
|
|
26
|
+
switch (verdict) {
|
|
27
|
+
case "reject":
|
|
28
|
+
return "BLOCK";
|
|
29
|
+
case "approve_with_concerns":
|
|
30
|
+
return "ESCALATE";
|
|
31
|
+
case "approve":
|
|
32
|
+
return "ALLOW";
|
|
33
|
+
case "review_unavailable":
|
|
34
|
+
default:
|
|
35
|
+
return failMode === "closed" ? "BLOCK" : failMode === "escalate" ? "ESCALATE" : "ALLOW";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function postJson(url, body, opts) {
|
|
39
|
+
const ctrl = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
|
|
41
|
+
try {
|
|
42
|
+
const headers = { "Content-Type": "application/json" };
|
|
43
|
+
if (opts.apiKey)
|
|
44
|
+
headers["Authorization"] = `Bearer ${opts.apiKey}`;
|
|
45
|
+
const res = await fetch(url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers,
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
signal: ctrl.signal,
|
|
50
|
+
});
|
|
51
|
+
let data = null;
|
|
52
|
+
try {
|
|
53
|
+
data = await res.json();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
data = null;
|
|
57
|
+
}
|
|
58
|
+
return { ok: res.ok, status: res.status, data };
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get an independent, recomputable verdict on a proposed action before it executes.
|
|
66
|
+
* Never throws — on any failure it returns a `review_unavailable` result resolved per `failMode`.
|
|
67
|
+
*/
|
|
68
|
+
export async function reviewAction(action, config = {}) {
|
|
69
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
70
|
+
const failMode = config.failMode ?? "open";
|
|
71
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
72
|
+
const sign = config.sign ?? true;
|
|
73
|
+
const unavailable = (reason) => ({
|
|
74
|
+
decision: verdictToDecision("review_unavailable", failMode),
|
|
75
|
+
verdict: "review_unavailable",
|
|
76
|
+
reason,
|
|
77
|
+
});
|
|
78
|
+
if (!config.apiKey) {
|
|
79
|
+
return unavailable(`No invinoveritas API key. Get one free: POST ${baseUrl}/register {"label":"your-app"}. ` +
|
|
80
|
+
`Gate resolved by failMode="${failMode}".`);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const { ok, status, data } = await postJson(`${baseUrl}/review`, {
|
|
84
|
+
artifact: action.artifact,
|
|
85
|
+
artifact_type: action.artifactType ?? "general",
|
|
86
|
+
context: action.context,
|
|
87
|
+
state_hash: action.stateHash,
|
|
88
|
+
sign,
|
|
89
|
+
}, { apiKey: config.apiKey, timeoutMs });
|
|
90
|
+
if (status === 402) {
|
|
91
|
+
return unavailable(`Gate skipped — unfunded key (HTTP 402). Resolved by failMode="${failMode}".`);
|
|
92
|
+
}
|
|
93
|
+
if (!ok || !data || typeof data.verdict !== "string") {
|
|
94
|
+
return unavailable(`Review unavailable (HTTP ${status}). Resolved by failMode="${failMode}".`);
|
|
95
|
+
}
|
|
96
|
+
const verdict = data.verdict;
|
|
97
|
+
return {
|
|
98
|
+
decision: verdictToDecision(verdict, failMode),
|
|
99
|
+
verdict,
|
|
100
|
+
confidence: data.confidence,
|
|
101
|
+
summary: data.summary,
|
|
102
|
+
issues: data.issues ?? [],
|
|
103
|
+
onchainRisk: data.onchain_risk,
|
|
104
|
+
proof: data.proof,
|
|
105
|
+
recomputeProofAt: data.proof ? `${baseUrl}/verify-proof` : undefined,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return unavailable(`Review timed out or errored. Resolved by failMode="${failMode}".`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Verify a signed invinoveritas proof someone handed you — the receiving half of the handshake.
|
|
114
|
+
* FREE, no auth. Recomputes the Nostr event id + checks the BIP-340 signature against the published
|
|
115
|
+
* key, so you trust neither the presenter nor us. Pass the signed `event` object or a `proofId`.
|
|
116
|
+
*/
|
|
117
|
+
export async function verifyProof(input, config = {}) {
|
|
118
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
119
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
120
|
+
if (!input.event && !input.proofId) {
|
|
121
|
+
return { valid: false, error: "Provide `event` (the signed proof object) or `proofId`." };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const { ok, status, data } = await postJson(`${baseUrl}/verify-proof`, { event: input.event, proof_id: input.proofId }, { timeoutMs });
|
|
125
|
+
if (!ok || !data)
|
|
126
|
+
return { valid: false, error: `verify-proof unavailable (HTTP ${status})` };
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return { valid: false, error: "verify-proof timed out or errored." };
|
|
131
|
+
}
|
|
132
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "invinoveritas-governance-gate-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic core for an independent pre-execution governance gate — calls invinoveritas /review for a verdict an agent can't self-issue, and returns a portable, recomputable, Bitcoin-anchored proof anyone can re-verify offline. Advisory by default; explicit fail-mode per surface. Thin adapters (MetaMask Snap, LangGraph, OpenAI Agents) wrap it.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agent",
|
|
7
|
+
"governance",
|
|
8
|
+
"verification",
|
|
9
|
+
"pre-execution",
|
|
10
|
+
"tool-call",
|
|
11
|
+
"onchain",
|
|
12
|
+
"guardrail",
|
|
13
|
+
"recomputable-proof",
|
|
14
|
+
"independent-verifier",
|
|
15
|
+
"second-opinion",
|
|
16
|
+
"x402"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.4.0"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://api.babyblueviper.com",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/babyblueviper1/invinoveritas-integrations"
|
|
41
|
+
}
|
|
42
|
+
}
|