ovrule-lab 0.2.1 → 0.3.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 CHANGED
@@ -8,7 +8,7 @@ TypeScript client for Ovrule case-file classification, guardrails, and receipt v
8
8
  npm install ovrule-lab
9
9
  ```
10
10
 
11
- On the first successful `classify()` or `guard()` call in a process, the SDK prints a one-line ready message plus a permalink to the created case. Later successful audits print only the case permalink. Set `OVRULE_QUIET=1` to suppress those logs.
11
+ On the first successful `classify()` or `guard()` call in a process, the SDK prints the full Ovrule ASCII banner plus a permalink to the created case. Later successful audits print only the case permalink. Set `OVRULE_QUIET=1` to suppress all SDK logs. You can also run `npx ovrule-lab` at any time to print the banner manually.
12
12
 
13
13
  ## 1. Simple classify
14
14
 
@@ -27,16 +27,15 @@ console.log(receipt.summary);
27
27
  import { guard } from "ovrule-lab";
28
28
 
29
29
  const result = await guard({
30
- scenario: "Finance agent wants to wire $18,000 to a new vendor after bank details changed.",
31
- policyPack: "finance",
30
+ scenario: "An AI coding agent wants to run DROP TABLE users on the production database.",
32
31
  });
33
32
 
34
33
  if (!result.allowed) {
35
- console.error(result.decision, result.suggestedFixes);
36
- throw new Error("Tool call blocked by Ovrule.");
34
+ console.error(result.decision, result.reason); // "REFUSED" — irreversible, production-wide
35
+ throw new Error("Action blocked by Ovrule.");
37
36
  }
38
37
 
39
- await issueWireTransfer();
38
+ await runCommand();
40
39
  ```
41
40
 
42
41
  ## 3. LangChain middleware integration
@@ -55,8 +54,8 @@ export async function beforeToolInvoke(input: string) {
55
54
  if (!guard.allowed) {
56
55
  return {
57
56
  blocked: true,
58
- receipt: guard.receipt,
59
- fixes: guard.suggestedFixes,
57
+ decision: guard.decision,
58
+ reason: guard.reason,
60
59
  };
61
60
  }
62
61
 
package/bin/ovrule-lab ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "./welcome.js";
package/bin/welcome.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { logBanner } from "../dist/banner.js";
4
+
5
+ logBanner();
@@ -0,0 +1,5 @@
1
+ export declare const SDK_VERSION = "0.2.2";
2
+ export declare function isQuietModeEnabled(): boolean;
3
+ export declare function renderBanner(): string;
4
+ export declare function logBanner(): void;
5
+ export declare function getDefaultBaseUrl(): string;
package/dist/banner.js ADDED
@@ -0,0 +1,47 @@
1
+ const DEFAULT_OVRULE_BASE_URL = "https://decision-receipt-lab.vercel.app";
2
+ export const SDK_VERSION = "0.2.2";
3
+ export function isQuietModeEnabled() {
4
+ return (typeof process !== "undefined" &&
5
+ typeof process.env === "object" &&
6
+ typeof process.env.OVRULE_QUIET !== "undefined" &&
7
+ process.env.OVRULE_QUIET !== "");
8
+ }
9
+ export function renderBanner() {
10
+ const reset = "\x1b[0m";
11
+ const dim = "\x1b[2m";
12
+ const bold = "\x1b[1m";
13
+ const cyan = "\x1b[36m";
14
+ const magenta = "\x1b[35m";
15
+ const yellow = "\x1b[33m";
16
+ return [
17
+ "",
18
+ `${bold}${cyan} ╔═══════════════════════════════════════════════╗${reset}`,
19
+ `${bold}${cyan} ║${reset} ${bold}O${reset}${magenta}━━${reset}${bold}V${reset} ${bold}Ovrule${reset} · SDK v${SDK_VERSION} ${bold}${cyan}║${reset}`,
20
+ `${bold}${cyan} ║${reset} ${dim}Auditable receipts for AI agents${reset} ${bold}${cyan}║${reset}`,
21
+ `${bold}${cyan} ╚═══════════════════════════════════════════════╝${reset}`,
22
+ "",
23
+ ` ${bold}Quickstart${reset}`,
24
+ ` ${dim}import { classify } from "ovrule-lab";${reset}`,
25
+ ` ${dim}const receipt = await classify("your agent action", {${reset}`,
26
+ ` ${dim} baseUrl: "${DEFAULT_OVRULE_BASE_URL}"${reset}`,
27
+ ` ${dim}});${reset}`,
28
+ "",
29
+ ` ${bold}Methods${reset}`,
30
+ ` ${dim}classify(action, opts)${reset} — full case file audit`,
31
+ ` ${dim}guard(action, opts)${reset} — returns { allowed, decision, ... }`,
32
+ ` ${dim}verify(receipt, sig, opts)${reset} — verify a signed receipt`,
33
+ "",
34
+ ` ${bold}Docs${reset} ${yellow}${DEFAULT_OVRULE_BASE_URL}/docs${reset}`,
35
+ ` ${bold}GitHub${reset} ${yellow}https://github.com/elakumuk/decision-receipt-lab${reset}`,
36
+ "",
37
+ ].join("\n");
38
+ }
39
+ export function logBanner() {
40
+ if (isQuietModeEnabled()) {
41
+ return;
42
+ }
43
+ console.info(renderBanner());
44
+ }
45
+ export function getDefaultBaseUrl() {
46
+ return DEFAULT_OVRULE_BASE_URL;
47
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type Decision = "ADMISSIBLE" | "AMBIGUOUS" | "REFUSED";
2
2
  export type Verdict = "PASS" | "WARN" | "FAIL";
3
- export type PolicyPackId = "general" | "customer_support" | "healthcare" | "finance";
3
+ export type PolicyPackId = "general" | "customer_support" | "healthcare" | "finance" | "payments" | "code";
4
4
  export type RuleName = "SAFETY" | "AUTHORIZATION" | "CAUSAL VALIDITY" | "REVERSIBILITY" | "IMPACT SCOPE" | "CONSENT";
5
5
  export type RuleTraceItem = {
6
6
  rule: RuleName;
@@ -77,8 +77,10 @@ export type ProposedAction = {
77
77
  export type GuardResult = {
78
78
  allowed: boolean;
79
79
  decision: Decision;
80
- suggestedFixes: FixSuggestion[];
81
- receipt: CaseFileReceipt;
80
+ reason: string;
81
+ ruleTrace: RuleTraceItem[];
82
+ receiptId: string;
83
+ signature?: string;
82
84
  };
83
85
  export type OvruleClientOptions = {
84
86
  baseUrl?: string;
@@ -103,4 +105,18 @@ export declare function guard(action: string | ProposedAction, options?: Request
103
105
  export declare function verify(receipt: CaseFileReceipt, signature: string, options?: {
104
106
  signal?: AbortSignal;
105
107
  } & OvruleClientOptions): Promise<boolean>;
108
+ /** Thrown by a guarded tool when Ovrule refuses the action. `.result` holds the decision. */
109
+ export declare class GuardBlockedError extends Error {
110
+ readonly result: GuardResult;
111
+ constructor(result: GuardResult);
112
+ }
113
+ /**
114
+ * Wrap any agent tool so it is audited by Ovrule BEFORE it runs. Works with any
115
+ * framework (LangChain, OpenAI Agents, MCP) because tools are just functions.
116
+ * A refused action throws GuardBlockedError and the tool never executes.
117
+ *
118
+ * const safeExec = guardTool(runCommand, (cmd) => `An AI agent wants to run: ${cmd}`, { policyPack: "code" });
119
+ * await safeExec("DROP TABLE users;"); // throws — never runs
120
+ */
121
+ export declare function guardTool<A extends unknown[], R>(tool: (...args: A) => Promise<R>, describe: (...args: A) => string, options?: RequestOptions & OvruleClientOptions): (...args: A) => Promise<R>;
106
122
  export {};
package/dist/index.js CHANGED
@@ -1,12 +1,5 @@
1
- const DEFAULT_OVRULE_BASE_URL = "https://decision-receipt-lab.vercel.app";
2
- const SDK_VERSION = "0.2.1";
1
+ import { getDefaultBaseUrl, isQuietModeEnabled, logBanner } from "./banner.js";
3
2
  let hasShownReadyMessage = false;
4
- function isQuietModeEnabled() {
5
- return (typeof process !== "undefined" &&
6
- typeof process.env === "object" &&
7
- typeof process.env.OVRULE_QUIET !== "undefined" &&
8
- process.env.OVRULE_QUIET !== "");
9
- }
10
3
  function getConsoleBaseUrl(baseUrl) {
11
4
  if (baseUrl) {
12
5
  return baseUrl;
@@ -14,7 +7,7 @@ function getConsoleBaseUrl(baseUrl) {
14
7
  if (typeof window !== "undefined" && window.location?.origin) {
15
8
  return window.location.origin;
16
9
  }
17
- return DEFAULT_OVRULE_BASE_URL;
10
+ return getDefaultBaseUrl();
18
11
  }
19
12
  function logSuccessfulAudit(receipt, baseUrl) {
20
13
  if (isQuietModeEnabled()) {
@@ -22,7 +15,7 @@ function logSuccessfulAudit(receipt, baseUrl) {
22
15
  }
23
16
  const caseUrl = `${getConsoleBaseUrl(baseUrl)}/case/${receipt.receiptId}`;
24
17
  if (!hasShownReadyMessage) {
25
- console.info(`[ovrule-lab v${SDK_VERSION}] ready · docs: ${DEFAULT_OVRULE_BASE_URL}/docs`);
18
+ logBanner();
26
19
  hasShownReadyMessage = true;
27
20
  }
28
21
  console.info(`[ovrule-lab] ✓ audited via ${caseUrl}`);
@@ -77,7 +70,7 @@ export class OvruleClient {
77
70
  baseUrl;
78
71
  fetchImpl;
79
72
  constructor(options = {}) {
80
- this.baseUrl = options.baseUrl?.replace(/\/$/, "") ?? DEFAULT_OVRULE_BASE_URL;
73
+ this.baseUrl = options.baseUrl?.replace(/\/$/, "") ?? getDefaultBaseUrl();
81
74
  this.fetchImpl = options.fetch ?? globalThis.fetch;
82
75
  if (!this.fetchImpl) {
83
76
  throw new Error("No fetch implementation available. Pass one in OvruleClient options.");
@@ -136,14 +129,26 @@ export class OvruleClient {
136
129
  return finalReceipt;
137
130
  }
138
131
  async guard(action, options = {}) {
139
- const receipt = await this.classify(action, options);
140
- const allowed = receipt.decision === "ADMISSIBLE";
141
- const suggestedFixes = receipt.suggestedFixes ?? [];
132
+ const payload = normalizeAction(action, options);
133
+ const response = await this.fetchImpl(`${this.baseUrl}/api/guard`, {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/json" },
136
+ body: JSON.stringify({ action: payload.scenario, policyPack: payload.policyPack }),
137
+ signal: options.signal,
138
+ });
139
+ // A blocked (REFUSED) action returns HTTP 403 with a full decision body, so we
140
+ // read the body regardless of status and only fail on a missing decision.
141
+ const data = (await response.json().catch(() => null));
142
+ if (!data || data.decision === undefined) {
143
+ throw new Error((data && data.error) || `Ovrule guard failed with status ${response.status}.`);
144
+ }
142
145
  return {
143
- allowed,
144
- decision: receipt.decision,
145
- suggestedFixes,
146
- receipt,
146
+ allowed: Boolean(data.allowed),
147
+ decision: data.decision,
148
+ reason: data.reason ?? "",
149
+ ruleTrace: data.ruleTrace ?? [],
150
+ receiptId: data.receiptId ?? "",
151
+ signature: data.signature,
147
152
  };
148
153
  }
149
154
  async verify(receipt, signature, options = {}) {
@@ -178,3 +183,29 @@ export async function verify(receipt, signature, options) {
178
183
  }
179
184
  return defaultClient.verify(receipt, signature, options);
180
185
  }
186
+ /** Thrown by a guarded tool when Ovrule refuses the action. `.result` holds the decision. */
187
+ export class GuardBlockedError extends Error {
188
+ result;
189
+ constructor(result) {
190
+ super(`Ovrule blocked action (${result.decision}): ${result.reason}`);
191
+ this.result = result;
192
+ this.name = "GuardBlockedError";
193
+ }
194
+ }
195
+ /**
196
+ * Wrap any agent tool so it is audited by Ovrule BEFORE it runs. Works with any
197
+ * framework (LangChain, OpenAI Agents, MCP) because tools are just functions.
198
+ * A refused action throws GuardBlockedError and the tool never executes.
199
+ *
200
+ * const safeExec = guardTool(runCommand, (cmd) => `An AI agent wants to run: ${cmd}`, { policyPack: "code" });
201
+ * await safeExec("DROP TABLE users;"); // throws — never runs
202
+ */
203
+ export function guardTool(tool, describe, options) {
204
+ return async (...args) => {
205
+ const result = await guard(describe(...args), options);
206
+ if (!result.allowed) {
207
+ throw new GuardBlockedError(result);
208
+ }
209
+ return tool(...args);
210
+ };
211
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ovrule-lab",
3
- "version": "0.2.1",
4
- "description": "TypeScript SDK for Ovrule case-file classification, guardrails, and receipt verification.",
3
+ "version": "0.3.0",
4
+ "description": "TypeScript SDK for Ovrule accountability guardrails for AI agents: classify an action, guard a tool call (incl. the accounts-payable / payment-fraud pack), and verify signed receipts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -14,8 +14,13 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
- "README.md"
17
+ "README.md",
18
+ "bin/ovrule-lab",
19
+ "bin/welcome.js"
18
20
  ],
21
+ "bin": {
22
+ "ovrule-lab": "./bin/ovrule-lab"
23
+ },
19
24
  "scripts": {
20
25
  "build": "tsc -p tsconfig.json",
21
26
  "clean": "rm -rf dist",