@usejarvis/brain 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.
Files changed (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Audit Trail — Logs every tool execution decision for review.
3
+ */
4
+
5
+ import { getDb, generateId } from '../vault/schema.ts';
6
+ import type { ActionCategory } from '../roles/authority.ts';
7
+
8
+ export type AuthorityDecisionType = 'allowed' | 'denied' | 'approval_required';
9
+
10
+ export type AuditEntry = {
11
+ id: string;
12
+ agent_id: string;
13
+ agent_name: string;
14
+ tool_name: string;
15
+ action_category: ActionCategory;
16
+ authority_decision: AuthorityDecisionType;
17
+ approval_id: string | null;
18
+ executed: number; // 0 or 1
19
+ execution_time_ms: number | null;
20
+ created_at: number;
21
+ };
22
+
23
+ export class AuditTrail {
24
+ /**
25
+ * Log a tool execution decision.
26
+ */
27
+ log(entry: {
28
+ agent_id: string;
29
+ agent_name: string;
30
+ tool_name: string;
31
+ action_category: ActionCategory;
32
+ authority_decision: AuthorityDecisionType;
33
+ approval_id?: string | null;
34
+ executed: boolean;
35
+ execution_time_ms?: number | null;
36
+ }): AuditEntry {
37
+ const db = getDb();
38
+ const id = generateId();
39
+ const now = Date.now();
40
+
41
+ db.run(
42
+ `INSERT INTO audit_trail (id, agent_id, agent_name, tool_name, action_category, authority_decision, approval_id, executed, execution_time_ms, created_at)
43
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
44
+ [
45
+ id,
46
+ entry.agent_id,
47
+ entry.agent_name,
48
+ entry.tool_name,
49
+ entry.action_category,
50
+ entry.authority_decision,
51
+ entry.approval_id ?? null,
52
+ entry.executed ? 1 : 0,
53
+ entry.execution_time_ms ?? null,
54
+ now,
55
+ ]
56
+ );
57
+
58
+ return {
59
+ id,
60
+ agent_id: entry.agent_id,
61
+ agent_name: entry.agent_name,
62
+ tool_name: entry.tool_name,
63
+ action_category: entry.action_category as ActionCategory,
64
+ authority_decision: entry.authority_decision,
65
+ approval_id: entry.approval_id ?? null,
66
+ executed: entry.executed ? 1 : 0,
67
+ execution_time_ms: entry.execution_time_ms ?? null,
68
+ created_at: now,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Query audit entries with filters.
74
+ */
75
+ query(filters?: {
76
+ agentId?: string;
77
+ action?: ActionCategory;
78
+ tool?: string;
79
+ decision?: AuthorityDecisionType;
80
+ since?: number;
81
+ limit?: number;
82
+ }): AuditEntry[] {
83
+ const db = getDb();
84
+ const conditions: string[] = [];
85
+ const values: unknown[] = [];
86
+
87
+ if (filters?.agentId) {
88
+ conditions.push('agent_id = ?');
89
+ values.push(filters.agentId);
90
+ }
91
+ if (filters?.action) {
92
+ conditions.push('action_category = ?');
93
+ values.push(filters.action);
94
+ }
95
+ if (filters?.tool) {
96
+ conditions.push('tool_name = ?');
97
+ values.push(filters.tool);
98
+ }
99
+ if (filters?.decision) {
100
+ conditions.push('authority_decision = ?');
101
+ values.push(filters.decision);
102
+ }
103
+ if (filters?.since) {
104
+ conditions.push('created_at >= ?');
105
+ values.push(filters.since);
106
+ }
107
+
108
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
109
+ const limit = filters?.limit ?? 100;
110
+
111
+ return db.query(
112
+ `SELECT * FROM audit_trail ${where} ORDER BY created_at DESC LIMIT ?`
113
+ ).all(...[...values, limit] as any[]) as AuditEntry[];
114
+ }
115
+
116
+ /**
117
+ * Get aggregate statistics.
118
+ */
119
+ getStats(since?: number): {
120
+ total: number;
121
+ allowed: number;
122
+ denied: number;
123
+ approvalRequired: number;
124
+ byCategory: Record<string, number>;
125
+ } {
126
+ const db = getDb();
127
+ const sinceClause = since ? `WHERE created_at >= ${since}` : '';
128
+
129
+ const totals = db.query(
130
+ `SELECT authority_decision, COUNT(*) as count FROM audit_trail ${sinceClause} GROUP BY authority_decision`
131
+ ).all() as { authority_decision: string; count: number }[];
132
+
133
+ const categories = db.query(
134
+ `SELECT action_category, COUNT(*) as count FROM audit_trail ${sinceClause} GROUP BY action_category`
135
+ ).all() as { action_category: string; count: number }[];
136
+
137
+ const stats = {
138
+ total: 0,
139
+ allowed: 0,
140
+ denied: 0,
141
+ approvalRequired: 0,
142
+ byCategory: {} as Record<string, number>,
143
+ };
144
+
145
+ for (const row of totals) {
146
+ stats.total += row.count;
147
+ if (row.authority_decision === 'allowed') stats.allowed = row.count;
148
+ if (row.authority_decision === 'denied') stats.denied = row.count;
149
+ if (row.authority_decision === 'approval_required') stats.approvalRequired = row.count;
150
+ }
151
+
152
+ for (const row of categories) {
153
+ stats.byCategory[row.action_category] = row.count;
154
+ }
155
+
156
+ return stats;
157
+ }
158
+ }
@@ -0,0 +1,519 @@
1
+ import { test, expect, describe, beforeEach } from 'bun:test';
2
+ import { initDatabase, closeDb, getDb } from '../vault/schema.ts';
3
+ import { AuthorityEngine, type AuthorityConfig } from './engine.ts';
4
+ import { ApprovalManager } from './approval.ts';
5
+ import { AuditTrail } from './audit.ts';
6
+ import { AuthorityLearner } from './learning.ts';
7
+ import { EmergencyController } from './emergency.ts';
8
+ import { getActionForTool } from './tool-action-map.ts';
9
+ import type { ActionCategory } from '../roles/authority.ts';
10
+
11
+ function makeConfig(overrides?: Partial<AuthorityConfig>): AuthorityConfig {
12
+ return {
13
+ default_level: 3,
14
+ governed_categories: ['send_email', 'send_message', 'make_payment'],
15
+ overrides: [],
16
+ context_rules: [],
17
+ learning: { enabled: true, suggest_threshold: 5 },
18
+ emergency_state: 'normal',
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ function makeCheckParams(overrides?: Partial<Parameters<AuthorityEngine['checkAuthority']>[0]>) {
24
+ return {
25
+ agentId: 'agent-1',
26
+ agentAuthorityLevel: 5,
27
+ agentRoleId: 'personal-assistant',
28
+ toolName: 'browser_navigate',
29
+ toolCategory: 'browser',
30
+ actionCategory: 'access_browser' as ActionCategory,
31
+ temporaryGrants: new Map<string, ActionCategory[]>(),
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ // --- AuthorityEngine ---
37
+
38
+ describe('AuthorityEngine', () => {
39
+ test('allows action when level meets requirement', () => {
40
+ const engine = new AuthorityEngine(makeConfig());
41
+ const decision = engine.checkAuthority(makeCheckParams({
42
+ agentAuthorityLevel: 5,
43
+ actionCategory: 'access_browser', // requires 5
44
+ }));
45
+ expect(decision.allowed).toBe(true);
46
+ expect(decision.requiresApproval).toBe(false);
47
+ });
48
+
49
+ test('denies action when level is below requirement', () => {
50
+ const engine = new AuthorityEngine(makeConfig());
51
+ const decision = engine.checkAuthority(makeCheckParams({
52
+ agentAuthorityLevel: 4,
53
+ actionCategory: 'execute_command', // requires 5
54
+ }));
55
+ expect(decision.allowed).toBe(false);
56
+ });
57
+
58
+ test('requires approval for governed categories', () => {
59
+ const engine = new AuthorityEngine(makeConfig());
60
+ const decision = engine.checkAuthority(makeCheckParams({
61
+ agentAuthorityLevel: 7,
62
+ actionCategory: 'send_email', // requires 7, governed
63
+ }));
64
+ expect(decision.allowed).toBe(true);
65
+ expect(decision.requiresApproval).toBe(true);
66
+ });
67
+
68
+ test('does not require approval for non-governed categories', () => {
69
+ const engine = new AuthorityEngine(makeConfig());
70
+ const decision = engine.checkAuthority(makeCheckParams({
71
+ agentAuthorityLevel: 5,
72
+ actionCategory: 'access_browser', // not governed
73
+ }));
74
+ expect(decision.requiresApproval).toBe(false);
75
+ });
76
+
77
+ test('per-action override: explicit deny', () => {
78
+ const engine = new AuthorityEngine(makeConfig({
79
+ overrides: [{ action: 'access_browser', allowed: false }],
80
+ }));
81
+ const decision = engine.checkAuthority(makeCheckParams({
82
+ agentAuthorityLevel: 10, // would normally allow
83
+ }));
84
+ expect(decision.allowed).toBe(false);
85
+ });
86
+
87
+ test('per-action override: explicit allow with approval', () => {
88
+ const engine = new AuthorityEngine(makeConfig({
89
+ overrides: [{ action: 'send_email', role_id: 'personal-assistant', allowed: true, requires_approval: true }],
90
+ }));
91
+ const decision = engine.checkAuthority(makeCheckParams({
92
+ agentAuthorityLevel: 7,
93
+ actionCategory: 'send_email',
94
+ }));
95
+ expect(decision.allowed).toBe(true);
96
+ expect(decision.requiresApproval).toBe(true);
97
+ });
98
+
99
+ test('per-action override: explicit allow without approval', () => {
100
+ const engine = new AuthorityEngine(makeConfig({
101
+ overrides: [{ action: 'send_email', allowed: true }],
102
+ }));
103
+ const decision = engine.checkAuthority(makeCheckParams({
104
+ agentAuthorityLevel: 7,
105
+ actionCategory: 'send_email',
106
+ }));
107
+ expect(decision.allowed).toBe(true);
108
+ expect(decision.requiresApproval).toBe(false);
109
+ });
110
+
111
+ test('role-specific override takes priority over global', () => {
112
+ const engine = new AuthorityEngine(makeConfig({
113
+ overrides: [
114
+ { action: 'send_email', allowed: false }, // global: deny
115
+ { action: 'send_email', role_id: 'personal-assistant', allowed: true }, // role: allow
116
+ ],
117
+ }));
118
+ const decision = engine.checkAuthority(makeCheckParams({
119
+ agentAuthorityLevel: 7,
120
+ actionCategory: 'send_email',
121
+ }));
122
+ expect(decision.allowed).toBe(true);
123
+ });
124
+
125
+ test('temporary grants override everything', () => {
126
+ const engine = new AuthorityEngine(makeConfig({
127
+ overrides: [{ action: 'make_payment', allowed: false }], // Explicitly denied
128
+ }));
129
+ const grants = new Map<string, ActionCategory[]>();
130
+ grants.set('agent-1', ['make_payment']);
131
+
132
+ const decision = engine.checkAuthority(makeCheckParams({
133
+ agentAuthorityLevel: 1, // Way below requirement
134
+ actionCategory: 'make_payment',
135
+ temporaryGrants: grants,
136
+ }));
137
+ expect(decision.allowed).toBe(true);
138
+ expect(decision.requiresApproval).toBe(false);
139
+ });
140
+
141
+ test('context rule: time_range', () => {
142
+ const now = new Date();
143
+ const engine = new AuthorityEngine(makeConfig({
144
+ context_rules: [{
145
+ id: 'no-email-at-night',
146
+ action: 'send_email',
147
+ condition: 'time_range',
148
+ params: { start_hour: now.getHours(), end_hour: now.getHours() + 1 },
149
+ effect: 'deny',
150
+ description: 'No emails during current hour',
151
+ }],
152
+ }));
153
+ const decision = engine.checkAuthority(makeCheckParams({
154
+ agentAuthorityLevel: 7,
155
+ actionCategory: 'send_email',
156
+ }));
157
+ expect(decision.allowed).toBe(false);
158
+ expect(decision.contextRule).toBe('no-email-at-night');
159
+ });
160
+
161
+ test('context rule: tool_name', () => {
162
+ const engine = new AuthorityEngine(makeConfig({
163
+ context_rules: [{
164
+ id: 'no-delete-command',
165
+ action: 'execute_command',
166
+ condition: 'tool_name',
167
+ params: { tool_name: 'run_command' },
168
+ effect: 'require_approval',
169
+ description: 'Shell commands need approval',
170
+ }],
171
+ }));
172
+ const decision = engine.checkAuthority(makeCheckParams({
173
+ agentAuthorityLevel: 5,
174
+ toolName: 'run_command',
175
+ actionCategory: 'execute_command',
176
+ }));
177
+ expect(decision.allowed).toBe(true);
178
+ expect(decision.requiresApproval).toBe(true);
179
+ });
180
+
181
+ test('describeRulesForAgent returns readable text', () => {
182
+ const engine = new AuthorityEngine(makeConfig());
183
+ const rules = engine.describeRulesForAgent(5, 'personal-assistant');
184
+ expect(rules).toContain('authority level: 5/10');
185
+ expect(rules).toContain('send_email');
186
+ expect(rules).toContain('make_payment');
187
+ });
188
+
189
+ test('all 13 action categories are checkable', () => {
190
+ const engine = new AuthorityEngine(makeConfig({ governed_categories: [] }));
191
+ const categories: ActionCategory[] = [
192
+ 'read_data', 'write_data', 'delete_data',
193
+ 'send_message', 'send_email',
194
+ 'execute_command', 'install_software',
195
+ 'make_payment', 'modify_settings',
196
+ 'spawn_agent', 'terminate_agent',
197
+ 'access_browser', 'control_app',
198
+ ];
199
+
200
+ for (const cat of categories) {
201
+ const decision = engine.checkAuthority(makeCheckParams({
202
+ agentAuthorityLevel: 10,
203
+ actionCategory: cat,
204
+ }));
205
+ expect(decision.allowed).toBe(true);
206
+ }
207
+ });
208
+ });
209
+
210
+ // --- Tool Action Map ---
211
+
212
+ describe('getActionForTool', () => {
213
+ test('maps known tools by name', () => {
214
+ expect(getActionForTool('run_command', 'terminal')).toBe('execute_command');
215
+ expect(getActionForTool('read_file', 'file-ops')).toBe('read_data');
216
+ expect(getActionForTool('write_file', 'file-ops')).toBe('write_data');
217
+ expect(getActionForTool('browser_navigate', 'browser')).toBe('access_browser');
218
+ expect(getActionForTool('desktop_click', 'desktop')).toBe('control_app');
219
+ expect(getActionForTool('delegate_task', 'delegation')).toBe('spawn_agent');
220
+ });
221
+
222
+ test('falls back to category map for unknown tools', () => {
223
+ expect(getActionForTool('some_browser_tool', 'browser')).toBe('access_browser');
224
+ expect(getActionForTool('some_terminal_tool', 'terminal')).toBe('execute_command');
225
+ });
226
+
227
+ test('defaults to read_data for completely unknown tools', () => {
228
+ expect(getActionForTool('unknown_tool', 'unknown_category')).toBe('read_data');
229
+ });
230
+ });
231
+
232
+ // --- EmergencyController ---
233
+
234
+ describe('EmergencyController', () => {
235
+ test('starts in normal state', () => {
236
+ const ec = new EmergencyController();
237
+ expect(ec.getState()).toBe('normal');
238
+ expect(ec.canExecute()).toBe(true);
239
+ });
240
+
241
+ test('pause blocks execution', () => {
242
+ const ec = new EmergencyController();
243
+ ec.pause();
244
+ expect(ec.getState()).toBe('paused');
245
+ expect(ec.canExecute()).toBe(false);
246
+ });
247
+
248
+ test('resume restores execution', () => {
249
+ const ec = new EmergencyController();
250
+ ec.pause();
251
+ ec.resume();
252
+ expect(ec.getState()).toBe('normal');
253
+ expect(ec.canExecute()).toBe(true);
254
+ });
255
+
256
+ test('kill blocks execution', () => {
257
+ const ec = new EmergencyController();
258
+ ec.kill();
259
+ expect(ec.getState()).toBe('killed');
260
+ expect(ec.canExecute()).toBe(false);
261
+ });
262
+
263
+ test('cannot pause from killed state', () => {
264
+ const ec = new EmergencyController();
265
+ ec.kill();
266
+ ec.pause();
267
+ expect(ec.getState()).toBe('killed');
268
+ });
269
+
270
+ test('reset restores from killed', () => {
271
+ const ec = new EmergencyController();
272
+ ec.kill();
273
+ ec.reset();
274
+ expect(ec.getState()).toBe('normal');
275
+ expect(ec.canExecute()).toBe(true);
276
+ });
277
+
278
+ test('fires state change callback', () => {
279
+ const ec = new EmergencyController();
280
+ const states: string[] = [];
281
+ ec.setStateChangeCallback(s => states.push(s));
282
+ ec.pause();
283
+ ec.resume();
284
+ expect(states).toEqual(['paused', 'normal']);
285
+ });
286
+ });
287
+
288
+ // --- ApprovalManager (requires DB) ---
289
+
290
+ describe('ApprovalManager', () => {
291
+ beforeEach(() => {
292
+ initDatabase(':memory:');
293
+ });
294
+
295
+ test('creates and retrieves approval request', () => {
296
+ const mgr = new ApprovalManager();
297
+ const req = mgr.createRequest({
298
+ agentId: 'agent-1',
299
+ agentName: 'Personal Assistant',
300
+ toolName: 'send_email',
301
+ toolArguments: { to: 'test@example.com', subject: 'hi' },
302
+ actionCategory: 'send_email',
303
+ urgency: 'urgent',
304
+ reason: 'Governed action',
305
+ context: 'User asked to send email',
306
+ });
307
+
308
+ expect(req.id).toBeTruthy();
309
+ expect(req.status).toBe('pending');
310
+ expect(req.urgency).toBe('urgent');
311
+
312
+ const retrieved = mgr.getRequest(req.id);
313
+ expect(retrieved).not.toBeNull();
314
+ expect(retrieved!.tool_name).toBe('send_email');
315
+ });
316
+
317
+ test('approve changes status', () => {
318
+ const mgr = new ApprovalManager();
319
+ const req = mgr.createRequest({
320
+ agentId: 'a1', agentName: 'PA', toolName: 'send_email',
321
+ toolArguments: {}, actionCategory: 'send_email',
322
+ urgency: 'normal', reason: 'test', context: '',
323
+ });
324
+
325
+ const approved = mgr.approve(req.id, 'dashboard');
326
+ expect(approved).not.toBeNull();
327
+ expect(approved!.status).toBe('approved');
328
+ expect(approved!.decided_by).toBe('dashboard');
329
+ });
330
+
331
+ test('deny changes status', () => {
332
+ const mgr = new ApprovalManager();
333
+ const req = mgr.createRequest({
334
+ agentId: 'a1', agentName: 'PA', toolName: 'send_email',
335
+ toolArguments: {}, actionCategory: 'send_email',
336
+ urgency: 'normal', reason: 'test', context: '',
337
+ });
338
+
339
+ const denied = mgr.deny(req.id, 'telegram');
340
+ expect(denied).not.toBeNull();
341
+ expect(denied!.status).toBe('denied');
342
+ });
343
+
344
+ test('cannot approve already decided request', () => {
345
+ const mgr = new ApprovalManager();
346
+ const req = mgr.createRequest({
347
+ agentId: 'a1', agentName: 'PA', toolName: 'send_email',
348
+ toolArguments: {}, actionCategory: 'send_email',
349
+ urgency: 'normal', reason: 'test', context: '',
350
+ });
351
+
352
+ mgr.deny(req.id, 'dashboard');
353
+ const result = mgr.approve(req.id, 'dashboard');
354
+ expect(result).toBeNull();
355
+ });
356
+
357
+ test('getPending returns only pending', () => {
358
+ const mgr = new ApprovalManager();
359
+ mgr.createRequest({
360
+ agentId: 'a1', agentName: 'PA', toolName: 'send_email',
361
+ toolArguments: {}, actionCategory: 'send_email',
362
+ urgency: 'normal', reason: 'test', context: '',
363
+ });
364
+ const req2 = mgr.createRequest({
365
+ agentId: 'a1', agentName: 'PA', toolName: 'make_payment',
366
+ toolArguments: {}, actionCategory: 'make_payment',
367
+ urgency: 'urgent', reason: 'test2', context: '',
368
+ });
369
+ mgr.approve(req2.id, 'dashboard');
370
+
371
+ const pending = mgr.getPending();
372
+ expect(pending.length).toBe(1);
373
+ expect(pending[0]!.tool_name).toBe('send_email');
374
+ });
375
+
376
+ test('findByShortId works', () => {
377
+ const mgr = new ApprovalManager();
378
+ const req = mgr.createRequest({
379
+ agentId: 'a1', agentName: 'PA', toolName: 'send_email',
380
+ toolArguments: {}, actionCategory: 'send_email',
381
+ urgency: 'normal', reason: 'test', context: '',
382
+ });
383
+
384
+ const found = mgr.findByShortId(req.id.slice(0, 8));
385
+ expect(found).not.toBeNull();
386
+ expect(found!.id).toBe(req.id);
387
+ });
388
+
389
+ test('markExecuted updates fields', () => {
390
+ const mgr = new ApprovalManager();
391
+ const req = mgr.createRequest({
392
+ agentId: 'a1', agentName: 'PA', toolName: 'send_email',
393
+ toolArguments: {}, actionCategory: 'send_email',
394
+ urgency: 'normal', reason: 'test', context: '',
395
+ });
396
+ mgr.approve(req.id, 'dashboard');
397
+ mgr.markExecuted(req.id, 'Email sent successfully');
398
+
399
+ const updated = mgr.getRequest(req.id);
400
+ expect(updated!.status).toBe('executed');
401
+ expect(updated!.execution_result).toBe('Email sent successfully');
402
+ });
403
+
404
+ test('expireOld expires old pending requests', () => {
405
+ const mgr = new ApprovalManager();
406
+ mgr.createRequest({
407
+ agentId: 'a1', agentName: 'PA', toolName: 'send_email',
408
+ toolArguments: {}, actionCategory: 'send_email',
409
+ urgency: 'normal', reason: 'test', context: '',
410
+ });
411
+
412
+ // Expire with very large max age — nothing should expire
413
+ const noneExpired = mgr.expireOld(999999999);
414
+ expect(noneExpired).toBe(0);
415
+
416
+ // Manually update created_at to the past so expiry works
417
+ getDb().run('UPDATE approval_requests SET created_at = created_at - 10000');
418
+ const expired = mgr.expireOld(5000);
419
+ expect(expired).toBe(1);
420
+
421
+ const pending = mgr.getPending();
422
+ expect(pending.length).toBe(0);
423
+ });
424
+ });
425
+
426
+ // --- AuditTrail (requires DB) ---
427
+
428
+ describe('AuditTrail', () => {
429
+ beforeEach(() => {
430
+ initDatabase(':memory:');
431
+ });
432
+
433
+ test('logs and queries audit entries', () => {
434
+ const trail = new AuditTrail();
435
+
436
+ trail.log({
437
+ agent_id: 'a1', agent_name: 'PA',
438
+ tool_name: 'browser_navigate', action_category: 'access_browser',
439
+ authority_decision: 'allowed', executed: true, execution_time_ms: 150,
440
+ });
441
+
442
+ trail.log({
443
+ agent_id: 'a1', agent_name: 'PA',
444
+ tool_name: 'send_email', action_category: 'send_email',
445
+ authority_decision: 'approval_required', executed: false,
446
+ });
447
+
448
+ const all = trail.query();
449
+ expect(all.length).toBe(2);
450
+
451
+ const browserOnly = trail.query({ action: 'access_browser' });
452
+ expect(browserOnly.length).toBe(1);
453
+ expect(browserOnly[0]!.tool_name).toBe('browser_navigate');
454
+ });
455
+
456
+ test('getStats aggregates correctly', () => {
457
+ const trail = new AuditTrail();
458
+
459
+ trail.log({ agent_id: 'a1', agent_name: 'PA', tool_name: 't1', action_category: 'read_data', authority_decision: 'allowed', executed: true });
460
+ trail.log({ agent_id: 'a1', agent_name: 'PA', tool_name: 't2', action_category: 'write_data', authority_decision: 'allowed', executed: true });
461
+ trail.log({ agent_id: 'a1', agent_name: 'PA', tool_name: 't3', action_category: 'send_email', authority_decision: 'denied', executed: false });
462
+
463
+ const stats = trail.getStats();
464
+ expect(stats.total).toBe(3);
465
+ expect(stats.allowed).toBe(2);
466
+ expect(stats.denied).toBe(1);
467
+ expect(stats.byCategory['read_data']).toBe(1);
468
+ expect(stats.byCategory['send_email']).toBe(1);
469
+ });
470
+ });
471
+
472
+ // --- AuthorityLearner (requires DB) ---
473
+
474
+ describe('AuthorityLearner', () => {
475
+ beforeEach(() => {
476
+ initDatabase(':memory:');
477
+ });
478
+
479
+ test('no suggestions below threshold', () => {
480
+ const learner = new AuthorityLearner(3);
481
+ learner.recordDecision('send_email', 'send_email', true);
482
+ learner.recordDecision('send_email', 'send_email', true);
483
+
484
+ expect(learner.getSuggestions().length).toBe(0);
485
+ });
486
+
487
+ test('suggests after threshold consecutive approvals', () => {
488
+ const learner = new AuthorityLearner(3);
489
+ learner.recordDecision('send_email', 'send_email', true);
490
+ learner.recordDecision('send_email', 'send_email', true);
491
+ learner.recordDecision('send_email', 'send_email', true);
492
+
493
+ const suggestions = learner.getSuggestions();
494
+ expect(suggestions.length).toBe(1);
495
+ expect(suggestions[0]!.actionCategory).toBe('send_email');
496
+ expect(suggestions[0]!.consecutiveApprovals).toBe(3);
497
+ });
498
+
499
+ test('denial resets consecutive count', () => {
500
+ const learner = new AuthorityLearner(3);
501
+ learner.recordDecision('send_email', 'send_email', true);
502
+ learner.recordDecision('send_email', 'send_email', true);
503
+ learner.recordDecision('send_email', 'send_email', false); // reset
504
+ learner.recordDecision('send_email', 'send_email', true);
505
+
506
+ expect(learner.getSuggestions().length).toBe(0);
507
+ });
508
+
509
+ test('markSuggestionSent prevents re-suggestion', () => {
510
+ const learner = new AuthorityLearner(2);
511
+ learner.recordDecision('send_email', 'send_email', true);
512
+ learner.recordDecision('send_email', 'send_email', true);
513
+
514
+ expect(learner.getSuggestions().length).toBe(1);
515
+
516
+ learner.markSuggestionSent('send_email', 'send_email');
517
+ expect(learner.getSuggestions().length).toBe(0);
518
+ });
519
+ });