@consensus-tools/universal 0.9.0 → 0.9.1

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.
Files changed (42) hide show
  1. package/dist/consensus-llm.test.d.ts +2 -0
  2. package/dist/consensus-llm.test.d.ts.map +1 -0
  3. package/dist/consensus-llm.test.js +244 -0
  4. package/dist/consensus-llm.test.js.map +1 -0
  5. package/dist/defaults.d.ts +10 -0
  6. package/dist/defaults.d.ts.map +1 -1
  7. package/dist/defaults.js +63 -2
  8. package/dist/defaults.js.map +1 -1
  9. package/dist/index.d.ts +13 -11
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +130 -49
  12. package/dist/index.js.map +1 -1
  13. package/dist/persona-reviewer-factory.d.ts +22 -0
  14. package/dist/persona-reviewer-factory.d.ts.map +1 -0
  15. package/dist/persona-reviewer-factory.js +318 -0
  16. package/dist/persona-reviewer-factory.js.map +1 -0
  17. package/dist/reputation-manager.d.ts +38 -0
  18. package/dist/reputation-manager.d.ts.map +1 -0
  19. package/dist/reputation-manager.js +154 -0
  20. package/dist/reputation-manager.js.map +1 -0
  21. package/dist/reputation-manager.test.d.ts +2 -0
  22. package/dist/reputation-manager.test.d.ts.map +1 -0
  23. package/dist/reputation-manager.test.js +111 -0
  24. package/dist/reputation-manager.test.js.map +1 -0
  25. package/dist/risk-tiers.d.ts +10 -0
  26. package/dist/risk-tiers.d.ts.map +1 -0
  27. package/dist/risk-tiers.js +46 -0
  28. package/dist/risk-tiers.js.map +1 -0
  29. package/dist/risk-tiers.test.d.ts +2 -0
  30. package/dist/risk-tiers.test.d.ts.map +1 -0
  31. package/dist/risk-tiers.test.js +40 -0
  32. package/dist/risk-tiers.test.js.map +1 -0
  33. package/dist/types.d.ts +59 -6
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +9 -9
  36. package/src/consensus-llm.test.ts +23 -4
  37. package/src/defaults.ts +10 -4
  38. package/src/index.ts +22 -18
  39. package/src/persona-reviewer-factory.ts +90 -70
  40. package/src/reputation-manager.ts +46 -31
  41. package/src/risk-tiers.test.ts +8 -0
  42. package/src/risk-tiers.ts +7 -5
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reputation-manager.test.js","sourceRoot":"","sources":["../src/reputation-manager.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { RiskTier, RiskTierMap } from "./types.js";
2
+ /**
3
+ * Classify a tool name into a risk tier.
4
+ *
5
+ * Priority: user overrides > high-risk patterns > low-risk patterns > default high.
6
+ * High-risk checked FIRST to prevent bypass via naming (e.g., "get_and_delete_user").
7
+ * Unknown tools default to high-risk (safe by default).
8
+ */
9
+ export declare function classifyTool(toolName: string, overrides?: RiskTierMap): RiskTier;
10
+ //# sourceMappingURL=risk-tiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"risk-tiers.d.ts","sourceRoot":"","sources":["../src/risk-tiers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA2BxD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,WAAW,GAAG,QAAQ,CAgBhF"}
@@ -0,0 +1,46 @@
1
+ // ── Default Risk Classification ──────────────────────────────────────
2
+ // Tools are classified by name prefix/pattern. Write/send/delete/mutate
3
+ // operations get full LLM deliberation. Read-only operations fast-path
4
+ // through regex only.
5
+ //
6
+ // Users override via config.riskTiers: { "my_tool": "low" }
7
+ const HIGH_RISK_PATTERNS = [
8
+ /^(send|email|mail)/i,
9
+ /^(delete|remove|drop|destroy)/i,
10
+ /^(write|update|patch|put|post|create|insert)/i,
11
+ /^(deploy|release|publish|push)/i,
12
+ /^(merge|commit|approve)/i,
13
+ /^(grant|revoke|escalate|permission)/i,
14
+ /^(execute|run|eval|exec)/i,
15
+ /^(transfer|pay|charge|refund)/i,
16
+ ];
17
+ const LOW_RISK_PATTERNS = [
18
+ /^(get|fetch|read|list|search|query|find|lookup)/i,
19
+ /^(check|verify|validate|inspect|describe)/i,
20
+ /^(count|sum|aggregate|stats)/i,
21
+ /^(view|show|display|render|preview)/i,
22
+ ];
23
+ /**
24
+ * Classify a tool name into a risk tier.
25
+ *
26
+ * Priority: user overrides > high-risk patterns > low-risk patterns > default high.
27
+ * High-risk checked FIRST to prevent bypass via naming (e.g., "get_and_delete_user").
28
+ * Unknown tools default to high-risk (safe by default).
29
+ */
30
+ export function classifyTool(toolName, overrides) {
31
+ if (overrides?.[toolName]) {
32
+ return overrides[toolName];
33
+ }
34
+ // Check high-risk FIRST to prevent bypass via compound names
35
+ for (const pattern of HIGH_RISK_PATTERNS) {
36
+ if (pattern.test(toolName))
37
+ return "high";
38
+ }
39
+ for (const pattern of LOW_RISK_PATTERNS) {
40
+ if (pattern.test(toolName))
41
+ return "low";
42
+ }
43
+ // Unknown tools default to high-risk (safe by default)
44
+ return "high";
45
+ }
46
+ //# sourceMappingURL=risk-tiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"risk-tiers.js","sourceRoot":"","sources":["../src/risk-tiers.ts"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,wEAAwE;AACxE,uEAAuE;AACvE,sBAAsB;AACtB,EAAE;AACF,4DAA4D;AAE5D,MAAM,kBAAkB,GAAG;IACzB,qBAAqB;IACrB,gCAAgC;IAChC,+CAA+C;IAC/C,iCAAiC;IACjC,0BAA0B;IAC1B,sCAAsC;IACtC,2BAA2B;IAC3B,gCAAgC;CACjC,CAAC;AAEF,MAAM,iBAAiB,GAAG;IACxB,kDAAkD;IAClD,4CAA4C;IAC5C,+BAA+B;IAC/B,sCAAsC;CACvC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,SAAuB;IACpE,IAAI,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IAED,6DAA6D;IAC7D,KAAK,MAAM,OAAO,IAAI,kBAAkB,EAAE,CAAC;QACzC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,MAAM,CAAC;IAC5C,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,iBAAiB,EAAE,CAAC;QACxC,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;IAC3C,CAAC;IAED,uDAAuD;IACvD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=risk-tiers.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"risk-tiers.test.d.ts","sourceRoot":"","sources":["../src/risk-tiers.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { classifyTool } from "./risk-tiers.js";
3
+ describe("classifyTool", () => {
4
+ it("classifies read-only tools as low risk", () => {
5
+ expect(classifyTool("get_weather")).toBe("low");
6
+ expect(classifyTool("list_files")).toBe("low");
7
+ expect(classifyTool("search_documents")).toBe("low");
8
+ expect(classifyTool("fetch_user")).toBe("low");
9
+ expect(classifyTool("read_config")).toBe("low");
10
+ expect(classifyTool("query_database")).toBe("low");
11
+ expect(classifyTool("check_status")).toBe("low");
12
+ expect(classifyTool("view_dashboard")).toBe("low");
13
+ });
14
+ it("classifies write/send/delete tools as high risk", () => {
15
+ expect(classifyTool("send_email")).toBe("high");
16
+ expect(classifyTool("delete_user")).toBe("high");
17
+ expect(classifyTool("write_file")).toBe("high");
18
+ expect(classifyTool("deploy_service")).toBe("high");
19
+ expect(classifyTool("merge_branch")).toBe("high");
20
+ expect(classifyTool("grant_permission")).toBe("high");
21
+ expect(classifyTool("execute_command")).toBe("high");
22
+ expect(classifyTool("transfer_funds")).toBe("high");
23
+ });
24
+ it("defaults unknown tools to high risk", () => {
25
+ expect(classifyTool("some_unknown_tool")).toBe("high");
26
+ expect(classifyTool("foobar")).toBe("high");
27
+ });
28
+ it("respects user overrides", () => {
29
+ expect(classifyTool("send_email", { send_email: "low" })).toBe("low");
30
+ expect(classifyTool("get_weather", { get_weather: "high" })).toBe("high");
31
+ });
32
+ it("prevents bypass via compound names (high-risk checked first)", () => {
33
+ // These start with read-like prefixes but contain destructive operations
34
+ expect(classifyTool("execute_and_log")).toBe("high");
35
+ expect(classifyTool("run_cleanup")).toBe("high");
36
+ expect(classifyTool("delete_then_verify")).toBe("high");
37
+ expect(classifyTool("send_and_check")).toBe("high");
38
+ });
39
+ });
40
+ //# sourceMappingURL=risk-tiers.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"risk-tiers.test.js","sourceRoot":"","sources":["../src/risk-tiers.test.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { IStorage } from "@consensus-tools/storage";
2
2
  import type { DecisionResult } from "@consensus-tools/wrapper";
3
+ import type { PersonaConfig } from "@consensus-tools/personas";
3
4
  export type ToolExecutor = (toolName: string, args: Record<string, unknown>) => Promise<unknown>;
4
5
  export type Wrappable = ToolExecutor | {
5
6
  execute: ToolExecutor;
@@ -9,27 +10,79 @@ export type Wrappable = ToolExecutor | {
9
10
  call: ToolExecutor;
10
11
  };
11
12
  export type FailPolicy = "closed" | "open";
13
+ export type ExecutionMode = "enforce" | "shadow";
14
+ export interface ModelMessage {
15
+ role: "system" | "user";
16
+ content: string;
17
+ }
18
+ export type ModelAdapter = (messages: ModelMessage[]) => Promise<string>;
19
+ export type RiskTier = "low" | "high";
20
+ export type RiskTierMap = Record<string, RiskTier>;
21
+ export interface FeedbackSignal {
22
+ /** The decision ID this feedback relates to. */
23
+ decisionId: string;
24
+ /** "override_block" = human unblocked a blocked call. "flag_miss" = human flagged a missed risk. */
25
+ type: "override_block" | "flag_miss";
26
+ }
12
27
  export interface LogEvent {
13
28
  event: string;
14
29
  data: Record<string, unknown>;
15
30
  timestamp: number;
16
31
  }
32
+ export interface LlmDecisionResult {
33
+ /** Unique decision ID for feedback reference. */
34
+ decisionId: string;
35
+ /** Final action: allow, block, or escalate. */
36
+ action: "allow" | "block" | "escalate";
37
+ /** Per-persona vote breakdown. */
38
+ votes: Array<{
39
+ personaId: string;
40
+ personaName: string;
41
+ vote: "YES" | "NO" | "REWRITE";
42
+ confidence: number;
43
+ rationale: string;
44
+ source: "llm" | "regex_fallback";
45
+ }>;
46
+ /** Policy used for resolution. */
47
+ policy: string;
48
+ /** Consensus trace from resolveConsensus. */
49
+ consensusTrace: Record<string, unknown>;
50
+ /** Aggregate score (0-1). */
51
+ aggregateScore: number;
52
+ }
17
53
  export interface UniversalConfig {
18
- /** Consensus policy name (maps to wrapper strategy via policyToStrategy). */
54
+ /** Consensus policy name. For regex mode: 'majority', 'supermajority', 'unanimous', 'threshold:X'.
55
+ * For LLM mode: also supports all 9 core policies (WEIGHTED_REPUTATION, MAJORITY_VOTE, etc.). */
19
56
  policy?: string;
20
- /** Guard domain names to use as reviewers. */
57
+ /** Guard domain names to use as reviewers (regex mode). */
21
58
  guards?: string[];
22
- /** Persona pack name (reserved for future use). */
23
- personas?: string;
24
59
  /** Behavior on error: 'closed' blocks, 'open' allows. Default: 'closed'. */
25
60
  failPolicy?: FailPolicy;
26
61
  /** Storage backend: 'memory' for in-memory, or an IStorage instance. Default: 'memory'. */
27
62
  storage?: "memory" | IStorage;
28
63
  /** Logging: true for console.debug, false to disable, or a custom function. Default: true. */
29
64
  logger?: boolean | ((event: LogEvent) => void);
30
- /** Called after every consensus decision. */
31
- onDecision?: (decision: DecisionResult<unknown>) => void | Promise<void>;
65
+ /** Called after every consensus decision (both regex and LLM modes). */
66
+ onDecision?: (decision: DecisionResult<unknown> | LlmDecisionResult) => void | Promise<void>;
32
67
  /** Called when an error occurs during deliberation. */
33
68
  onError?: (err: Error, action: unknown) => void;
69
+ /** LLM model adapter. When provided, enables LLM persona mode. */
70
+ model?: ModelAdapter;
71
+ /** Persona pack name. Default: 'default' (security, compliance, operations). */
72
+ pack?: string;
73
+ /** Custom personas (overrides pack). */
74
+ personas?: PersonaConfig[];
75
+ /** Execution mode: 'enforce' blocks on consensus rejection, 'shadow' logs but never blocks. Default: 'enforce'. */
76
+ mode?: ExecutionMode;
77
+ /** Tool risk tier overrides. Keys are tool names, values are 'low' or 'high'. */
78
+ riskTiers?: RiskTierMap;
79
+ /** Human feedback callback for reputation ground truth. */
80
+ onFeedback?: (signal: FeedbackSignal) => void;
81
+ /** Storage for persisting reputation across restarts. Default: in-memory (lost on restart). */
82
+ reputationStore?: IStorage;
83
+ /** Reputation threshold below which persona respawn triggers. Default: 0.15. */
84
+ respawnThreshold?: number;
85
+ /** Per-persona LLM call timeout in milliseconds. Default: 3000. */
86
+ personaTimeout?: number;
34
87
  }
35
88
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAM/D,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEjG,MAAM,MAAM,SAAS,GACjB,YAAY,GACZ;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,GACzB;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC;AAI3B,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAI3C,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB;AAID,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC9B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC;IAC/C,6CAA6C;IAC7C,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,CAAC,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,uDAAuD;IACvD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACjD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consensus-tools/universal",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Universal facade — wrap any tool executor with consensus governance in one line",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,17 +12,17 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@consensus-tools/core": "0.9.0",
16
- "@consensus-tools/storage": "0.9.0",
17
- "@consensus-tools/policies": "0.9.0",
18
- "@consensus-tools/guards": "0.9.0",
19
- "@consensus-tools/wrapper": "0.9.0",
20
- "@consensus-tools/personas": "0.9.0",
21
- "@consensus-tools/schemas": "0.9.0"
15
+ "@consensus-tools/core": "0.9.1",
16
+ "@consensus-tools/guards": "0.9.1",
17
+ "@consensus-tools/personas": "0.9.1",
18
+ "@consensus-tools/policies": "0.9.1",
19
+ "@consensus-tools/wrapper": "0.9.1",
20
+ "@consensus-tools/schemas": "0.9.1",
21
+ "@consensus-tools/storage": "0.9.1"
22
22
  },
23
23
  "peerDependencies": {
24
- "@consensus-tools/mcp": "0.8.0",
25
24
  "@consensus-tools/langchain": "0.8.0",
25
+ "@consensus-tools/mcp": "0.8.0",
26
26
  "@consensus-tools/ai-sdk": "0.8.0"
27
27
  },
28
28
  "peerDependenciesMeta": {
@@ -154,14 +154,33 @@ describe("consensus.wrap with LLM persona mode", () => {
154
154
  });
155
155
 
156
156
  describe("per-persona timeout", () => {
157
- it("falls back to regex for timed-out personas", async () => {
157
+ it("falls back to regex for timed-out personas and blocks (fail-closed)", async () => {
158
+ const onDecision = vi.fn();
159
+ const executor = createMockExecutor();
160
+ const safe = consensus.wrap(executor, {
161
+ model: createSlowModel(5000),
162
+ personaTimeout: 100,
163
+ onDecision,
164
+ logger: false,
165
+ });
166
+ // LLM times out, regex fallback votes NO (fail-closed), tool is blocked
167
+ await expect(safe("send_email", { to: "user@test.com" })).rejects.toThrow(ConsensusBlockedError);
168
+ expect(executor).not.toHaveBeenCalled();
169
+ // onDecision should still fire with regex_fallback votes
170
+ expect(onDecision).toHaveBeenCalled();
171
+ const decision = onDecision.mock.calls[0]![0] as LlmDecisionResult;
172
+ expect(decision.votes.every((v) => v.source === "regex_fallback")).toBe(true);
173
+ });
174
+
175
+ it("allows with failPolicy open when LLM times out", async () => {
158
176
  const executor = createMockExecutor();
159
177
  const safe = consensus.wrap(executor, {
160
- model: createSlowModel(5000), // 5 second delay, will exceed default 3s timeout
161
- personaTimeout: 100, // 100ms timeout for fast test
178
+ model: createSlowModel(5000),
179
+ personaTimeout: 100,
180
+ failPolicy: "open",
162
181
  logger: false,
163
182
  });
164
- // Should not hang, should fall back to regex
183
+ // failPolicy open: even if regex fallback blocks, tool executes
165
184
  const result = await safe("send_email", { to: "user@test.com" });
166
185
  expect(result).toBeDefined();
167
186
  });
package/src/defaults.ts CHANGED
@@ -69,7 +69,11 @@ export function resolvePolicyType(policy: string): string {
69
69
  // threshold:X -> APPROVAL_VOTE with custom config
70
70
  if (policy.startsWith("threshold:")) return "APPROVAL_VOTE";
71
71
 
72
- // Default to MAJORITY_VOTE
72
+ // Unknown policy — warn and fall back
73
+ console.warn( // eslint-disable-line no-console
74
+ `[consensus] Unknown LLM policy "${policy}", falling back to MAJORITY_VOTE. ` +
75
+ `Valid: ${[...CORE_POLICY_TYPES].join(", ")} or friendly names: ${Object.keys(FRIENDLY_TO_CORE).join(", ")}`,
76
+ );
73
77
  return "MAJORITY_VOTE";
74
78
  }
75
79
 
@@ -107,10 +111,12 @@ export function policyToStrategy(policy: string): StrategyConfig {
107
111
  return { strategy: "threshold", threshold: value };
108
112
  }
109
113
 
110
- // In LLM mode, core policy names are valid. In regex mode, they're not.
111
- // Let the caller decide — we just throw for truly unknown strings.
114
+ // LLM-mode policy used without a model warn loudly, this is a config mistake
112
115
  if (CORE_POLICY_TYPES.has(policy) || FRIENDLY_TO_CORE[policy]) {
113
- // Valid LLM-mode policy used in regex mode — fall back to majority
116
+ console.warn( // eslint-disable-line no-console
117
+ `[consensus] Policy "${policy}" requires LLM mode (provide a model). ` +
118
+ `Falling back to majority in regex mode. This may not match your intended security posture.`,
119
+ );
114
120
  return { strategy: "majority" };
115
121
  }
116
122
 
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import crypto from "node:crypto";
1
2
  import { consensus as wrapWithConsensus } from "@consensus-tools/wrapper";
2
3
  import type { DecisionResult, ReviewerFn, LifecycleHooks } from "@consensus-tools/wrapper";
3
4
  import { createGuardTemplate, GUARD_CONFIGS } from "@consensus-tools/guards";
@@ -53,10 +54,12 @@ function resolveStorage(storage: "memory" | IStorage): IStorage {
53
54
 
54
55
  function createStorageHooks(store: IStorage): LifecycleHooks {
55
56
  return {
57
+ // afterResolve fires for ALL outcomes (allow, block, escalate).
58
+ // No separate onBlock hook needed (was duplicating audit entries).
56
59
  async afterResolve(result: DecisionResult) {
57
60
  await store.update((state) => {
58
61
  state.audit.push({
59
- id: `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
62
+ id: `audit-${crypto.randomUUID()}`,
60
63
  at: new Date().toISOString(),
61
64
  action: result.action,
62
65
  aggregateScore: result.aggregateScore,
@@ -65,18 +68,6 @@ function createStorageHooks(store: IStorage): LifecycleHooks {
65
68
  } as any);
66
69
  });
67
70
  },
68
- async onBlock(result: DecisionResult) {
69
- await store.update((state) => {
70
- state.audit.push({
71
- id: `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
72
- at: new Date().toISOString(),
73
- action: "block",
74
- aggregateScore: result.aggregateScore,
75
- attempt: result.attempt,
76
- scoresCount: result.scores.length,
77
- } as any);
78
- });
79
- },
80
71
  };
81
72
  }
82
73
 
@@ -136,9 +127,11 @@ function createLlmExecutor(
136
127
  }
137
128
  });
138
129
 
139
- // Load persisted reputation if store is configured
130
+ // Load persisted reputation if store is configured.
131
+ // Track readiness so early calls don't race against the load.
132
+ let reputationReady: Promise<void> | undefined;
140
133
  if (config.reputationStore) {
141
- reputationManager.load().catch((err) => {
134
+ reputationReady = reputationManager.load().catch((err) => {
142
135
  if (config.logger !== false) {
143
136
  console.warn("[consensus] Failed to load persisted reputation, starting with defaults:", err); // eslint-disable-line no-console
144
137
  }
@@ -158,15 +151,26 @@ function createLlmExecutor(
158
151
  };
159
152
 
160
153
  return async (toolName: string, args: Record<string, unknown>): Promise<unknown> => {
154
+ // Wait for reputation load before first deliberation
155
+ if (reputationReady) {
156
+ await reputationReady;
157
+ reputationReady = undefined;
158
+ }
159
+
161
160
  try {
162
161
  const decision: LlmDecisionResult = await deliberate(deliberateConfig, toolName, args);
163
162
 
164
- // Fire onDecision callback
163
+ // Fire onDecision callback (wrapped in try/catch so user callback errors
164
+ // don't affect governance decisions)
165
165
  if (config.onDecision) {
166
- await config.onDecision(decision);
166
+ try {
167
+ await config.onDecision(decision);
168
+ } catch (callbackErr) {
169
+ config.onError?.(callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr)), { toolName, args, phase: "onDecision" });
170
+ }
167
171
  }
168
172
 
169
- // Shadow mode: always allow, just log
173
+ // Shadow mode: always allow, just log. Never blocks, even if callback threw.
170
174
  if (mode === "shadow") {
171
175
  return fn(toolName, args);
172
176
  }