crewly 1.11.5 → 1.12.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 (208) hide show
  1. package/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
  2. package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
  3. package/config/skills/agent/web-search/SKILL.md +70 -0
  4. package/config/skills/agent/web-search/execute.sh +170 -0
  5. package/config/skills/agent/web-search/skill.json +23 -0
  6. package/dist/backend/backend/src/constants.d.ts +34 -1
  7. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  8. package/dist/backend/backend/src/constants.js +34 -1
  9. package/dist/backend/backend/src/constants.js.map +1 -1
  10. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
  11. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  12. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
  13. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  14. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
  15. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
  16. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
  18. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
  20. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
  22. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
  24. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
  25. package/dist/backend/backend/src/index.d.ts.map +1 -1
  26. package/dist/backend/backend/src/index.js +36 -2
  27. package/dist/backend/backend/src/index.js.map +1 -1
  28. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
  29. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
  31. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +90 -0
  33. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
  34. package/dist/backend/backend/src/services/backup/backup-archive.service.js +309 -0
  35. package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -0
  36. package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
  37. package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
  38. package/dist/backend/backend/src/services/backup/backup-cloud.client.js +134 -0
  39. package/dist/backend/backend/src/services/backup/backup-cloud.client.js.map +1 -0
  40. package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts +78 -0
  41. package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
  42. package/dist/backend/backend/src/services/backup/backup-restore.service.js +358 -0
  43. package/dist/backend/backend/src/services/backup/backup-restore.service.js.map +1 -0
  44. package/dist/backend/backend/src/services/backup/backup.types.d.ts +163 -0
  45. package/dist/backend/backend/src/services/backup/backup.types.d.ts.map +1 -0
  46. package/dist/backend/backend/src/services/backup/backup.types.js +13 -0
  47. package/dist/backend/backend/src/services/backup/backup.types.js.map +1 -0
  48. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +29 -2
  49. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
  50. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +97 -13
  51. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
  52. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
  53. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
  54. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
  55. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
  56. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
  57. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
  59. package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
  60. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
  61. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
  63. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
  64. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
  65. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
  66. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
  67. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
  68. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
  69. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
  70. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
  71. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
  72. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
  73. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
  74. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
  75. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
  76. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
  77. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
  78. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
  80. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
  81. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
  82. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
  84. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  85. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
  86. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
  88. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
  90. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
  92. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  93. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
  94. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
  95. package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
  96. package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
  97. package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/template/template.service.js +67 -2
  99. package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
  100. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
  101. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
  103. package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
  104. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
  105. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
  106. package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
  107. package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
  108. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
  109. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
  110. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
  111. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
  112. package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
  113. package/dist/backend/backend/src/types/intent-task.types.js +8 -0
  114. package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
  115. package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
  116. package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
  117. package/dist/backend/backend/src/types/v2/request.types.js +1 -0
  118. package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
  119. package/dist/cli/backend/src/constants.d.ts +34 -1
  120. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  121. package/dist/cli/backend/src/constants.js +34 -1
  122. package/dist/cli/backend/src/constants.js.map +1 -1
  123. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts +70 -0
  124. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts.map +1 -0
  125. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js +427 -0
  126. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js.map +1 -0
  127. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +90 -0
  128. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
  129. package/dist/cli/backend/src/services/backup/backup-archive.service.js +309 -0
  130. package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -0
  131. package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
  132. package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
  133. package/dist/cli/backend/src/services/backup/backup-cloud.client.js +134 -0
  134. package/dist/cli/backend/src/services/backup/backup-cloud.client.js.map +1 -0
  135. package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts +78 -0
  136. package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
  137. package/dist/cli/backend/src/services/backup/backup-restore.service.js +358 -0
  138. package/dist/cli/backend/src/services/backup/backup-restore.service.js.map +1 -0
  139. package/dist/cli/backend/src/services/backup/backup.types.d.ts +163 -0
  140. package/dist/cli/backend/src/services/backup/backup.types.d.ts.map +1 -0
  141. package/dist/cli/backend/src/services/backup/backup.types.js +13 -0
  142. package/dist/cli/backend/src/services/backup/backup.types.js.map +1 -0
  143. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +410 -0
  144. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -0
  145. package/dist/cli/backend/src/services/cloud/cloud-client.service.js +863 -0
  146. package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -0
  147. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +292 -0
  148. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -0
  149. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +1093 -0
  150. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -0
  151. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +328 -0
  152. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -0
  153. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +171 -0
  154. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -0
  155. package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts +89 -0
  156. package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts.map +1 -0
  157. package/dist/cli/backend/src/services/cloud/device-identity.service.js +148 -0
  158. package/dist/cli/backend/src/services/cloud/device-identity.service.js.map +1 -0
  159. package/dist/cli/backend/src/services/user/user-identity.service.d.ts +86 -0
  160. package/dist/cli/backend/src/services/user/user-identity.service.d.ts.map +1 -0
  161. package/dist/cli/backend/src/services/user/user-identity.service.js +190 -0
  162. package/dist/cli/backend/src/services/user/user-identity.service.js.map +1 -0
  163. package/dist/cli/cli/src/commands/backup.d.ts +31 -0
  164. package/dist/cli/cli/src/commands/backup.d.ts.map +1 -0
  165. package/dist/cli/cli/src/commands/backup.js +280 -0
  166. package/dist/cli/cli/src/commands/backup.js.map +1 -0
  167. package/dist/cli/cli/src/index.js +10 -0
  168. package/dist/cli/cli/src/index.js.map +1 -1
  169. package/package.json +9 -3
  170. package/packages/crewly-agent/README.md +27 -0
  171. package/packages/crewly-agent/bin/crewly-agent +33 -0
  172. package/packages/crewly-agent/package.json +39 -0
  173. package/packages/crewly-agent/src/cli.ts +168 -0
  174. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
  175. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
  176. package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
  177. package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
  178. package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
  179. package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
  180. package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
  181. package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
  182. package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
  183. package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
  184. package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
  185. package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
  186. package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
  187. package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
  188. package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
  189. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
  190. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
  191. package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
  192. package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
  193. package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
  194. package/packages/crewly-agent/src/runtime/index.ts +38 -0
  195. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
  196. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
  197. package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
  198. package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
  199. package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
  200. package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
  201. package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
  202. package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
  203. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
  204. package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
  205. package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
  206. package/packages/crewly-agent/src/runtime/types.ts +637 -0
  207. package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
  208. package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * AuditLogService Tests
3
+ *
4
+ * Tests for the SQLite-backed persistent audit log service.
5
+ *
6
+ * @module services/agent/crewly-agent/audit-log.service.test
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
+
11
+ import { mkdtempSync, rmSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { tmpdir } from 'os';
14
+ import { AuditLogService } from './audit-log.service.js';
15
+ import type { AuditEntry } from './types.js';
16
+
17
+ describe('AuditLogService', () => {
18
+ let service: AuditLogService;
19
+ let tempDir: string;
20
+
21
+ beforeEach(() => {
22
+ tempDir = mkdtempSync(join(tmpdir(), 'audit-log-test-'));
23
+ service = new AuditLogService(tempDir);
24
+ service.initialize();
25
+ });
26
+
27
+ afterEach(() => {
28
+ service.close();
29
+ rmSync(tempDir, { recursive: true, force: true });
30
+ });
31
+
32
+ /** Helper to create a valid AuditEntry */
33
+ function makeEntry(overrides: Partial<AuditEntry> = {}): AuditEntry {
34
+ return {
35
+ timestamp: new Date().toISOString(),
36
+ sessionName: 'test-session',
37
+ toolName: 'edit_file',
38
+ sensitivity: 'destructive',
39
+ args: { path: '/tmp/test.txt' },
40
+ success: true,
41
+ durationMs: 42,
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ describe('initialize()', () => {
47
+ it('should create the database and tables', () => {
48
+ expect(service.isInitialized()).toBe(true);
49
+ });
50
+
51
+ it('should be idempotent', () => {
52
+ service.initialize(); // second call should not throw
53
+ expect(service.isInitialized()).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe('append()', () => {
58
+ it('should insert a tool-call audit entry', () => {
59
+ service.append(makeEntry());
60
+ const rows = service.query({ limit: 10 });
61
+ expect(rows).toHaveLength(1);
62
+ expect(rows[0].toolName).toBe('edit_file');
63
+ expect(rows[0].action).toBe('tool_call');
64
+ expect(rows[0].success).toBe(true);
65
+ expect(rows[0].args.path).toBe('/tmp/test.txt');
66
+ });
67
+
68
+ it('should store failed entries', () => {
69
+ service.append(makeEntry({ success: false, error: 'Permission denied' }));
70
+ const rows = service.query({ limit: 10 });
71
+ expect(rows[0].success).toBe(false);
72
+ expect(rows[0].error).toBe('Permission denied');
73
+ });
74
+
75
+ it('should throw if not initialized', () => {
76
+ const uninit = new AuditLogService(tempDir, 'other.sqlite');
77
+ expect(() => uninit.append(makeEntry())).toThrow('not initialized');
78
+ });
79
+ });
80
+
81
+ describe('logApprovalEvent()', () => {
82
+ it('should log an approval_granted event', () => {
83
+ service.logApprovalEvent(
84
+ 'approval-1', 'sess-1', 'rm_file', 'destructive', 'approval_granted', 'api',
85
+ );
86
+ const rows = service.query({ limit: 10 });
87
+ expect(rows).toHaveLength(1);
88
+ expect(rows[0].action).toBe('approval_granted');
89
+ expect(rows[0].resolvedBy).toBe('api');
90
+ expect(rows[0].approvalId).toBe('approval-1');
91
+ });
92
+
93
+ it('should log an approval_rejected event', () => {
94
+ service.logApprovalEvent(
95
+ 'approval-2', 'sess-1', 'git_push', 'destructive', 'approval_rejected', 'auditor',
96
+ );
97
+ const rows = service.query({ limit: 10 });
98
+ expect(rows[0].action).toBe('approval_rejected');
99
+ expect(rows[0].resolvedBy).toBe('auditor');
100
+ });
101
+
102
+ it('should log an approval_expired event', () => {
103
+ service.logApprovalEvent(
104
+ 'approval-3', 'sess-1', 'curl', 'destructive', 'approval_expired', 'auto-expire',
105
+ );
106
+ const rows = service.query({ limit: 10 });
107
+ expect(rows[0].action).toBe('approval_expired');
108
+ });
109
+ });
110
+
111
+ describe('query()', () => {
112
+ beforeEach(() => {
113
+ // Insert diverse entries
114
+ service.append(makeEntry({ sessionName: 'sess-1', toolName: 'read_file', sensitivity: 'safe', timestamp: '2026-01-01T00:00:00Z' }));
115
+ service.append(makeEntry({ sessionName: 'sess-1', toolName: 'edit_file', sensitivity: 'destructive', timestamp: '2026-01-02T00:00:00Z' }));
116
+ service.append(makeEntry({ sessionName: 'sess-2', toolName: 'git_push', sensitivity: 'destructive', timestamp: '2026-01-03T00:00:00Z' }));
117
+ service.logApprovalEvent('a-1', 'sess-1', 'rm_file', 'destructive', 'approval_granted', 'api');
118
+ });
119
+
120
+ it('should return entries in reverse chronological order', () => {
121
+ const rows = service.query({ limit: 100 });
122
+ expect(rows).toHaveLength(4);
123
+ // Most recent (approval event) should be first
124
+ expect(rows[0].action).toBe('approval_granted');
125
+ });
126
+
127
+ it('should filter by sessionName', () => {
128
+ const rows = service.query({ limit: 100, sessionName: 'sess-2' });
129
+ expect(rows).toHaveLength(1);
130
+ expect(rows[0].toolName).toBe('git_push');
131
+ });
132
+
133
+ it('should filter by sensitivity', () => {
134
+ const rows = service.query({ limit: 100, sensitivity: 'safe' });
135
+ expect(rows).toHaveLength(1);
136
+ expect(rows[0].toolName).toBe('read_file');
137
+ });
138
+
139
+ it('should filter by toolName', () => {
140
+ const rows = service.query({ limit: 100, toolName: 'edit_file' });
141
+ expect(rows).toHaveLength(1);
142
+ });
143
+
144
+ it('should filter by action', () => {
145
+ const rows = service.query({ limit: 100, action: 'approval_granted' });
146
+ expect(rows).toHaveLength(1);
147
+ });
148
+
149
+ it('should filter by since', () => {
150
+ const rows = service.query({ limit: 100, since: '2026-01-02T00:00:00Z' });
151
+ // Should include entries from Jan 2, Jan 3, and the approval event
152
+ expect(rows.length).toBeGreaterThanOrEqual(2);
153
+ });
154
+
155
+ it('should filter by until', () => {
156
+ const rows = service.query({ limit: 100, until: '2026-01-01T23:59:59Z' });
157
+ expect(rows).toHaveLength(1);
158
+ expect(rows[0].toolName).toBe('read_file');
159
+ });
160
+
161
+ it('should respect limit', () => {
162
+ const rows = service.query({ limit: 2 });
163
+ expect(rows).toHaveLength(2);
164
+ });
165
+
166
+ it('should support offset for pagination', () => {
167
+ const page1 = service.query({ limit: 2, offset: 0 });
168
+ const page2 = service.query({ limit: 2, offset: 2 });
169
+ expect(page1).toHaveLength(2);
170
+ expect(page2).toHaveLength(2);
171
+ expect(page1[0].id).not.toBe(page2[0].id);
172
+ });
173
+ });
174
+
175
+ describe('count()', () => {
176
+ it('should return total entry count', () => {
177
+ service.append(makeEntry());
178
+ service.append(makeEntry());
179
+ service.append(makeEntry());
180
+ expect(service.count()).toBe(3);
181
+ });
182
+
183
+ it('should filter count by sessionName', () => {
184
+ service.append(makeEntry({ sessionName: 'a' }));
185
+ service.append(makeEntry({ sessionName: 'b' }));
186
+ service.append(makeEntry({ sessionName: 'a' }));
187
+ expect(service.count({ sessionName: 'a' })).toBe(2);
188
+ });
189
+ });
190
+
191
+ describe('close()', () => {
192
+ it('should close the database and reset state', () => {
193
+ service.close();
194
+ expect(service.isInitialized()).toBe(false);
195
+ });
196
+
197
+ it('should be safe to call multiple times', () => {
198
+ service.close();
199
+ service.close(); // no throw
200
+ });
201
+ });
202
+
203
+ describe('getDbPath()', () => {
204
+ it('should return the database file path', () => {
205
+ expect(service.getDbPath()).toContain('audit-log.sqlite');
206
+ });
207
+ });
208
+ });
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Persistent Audit Log Service (SQLite)
3
+ *
4
+ * Provides durable, queryable audit logging using better-sqlite3.
5
+ * Replaces the append-only JSONL approach with a proper relational store
6
+ * that supports efficient filtering, pagination, and aggregation.
7
+ *
8
+ * Single WAL-mode database shared across all sessions. Thread-safe for
9
+ * the single-threaded Node.js event loop (better-sqlite3 is synchronous).
10
+ *
11
+ * @module services/agent/crewly-agent/audit-log.service
12
+ */
13
+
14
+ import Database from 'better-sqlite3';
15
+ import { join } from 'path';
16
+ import { mkdirSync } from 'fs';
17
+ import type { AuditEntry, AuditLogFilters, ToolSensitivity } from './types.js';
18
+
19
+ /** Default database filename */
20
+ const DB_FILENAME = 'audit-log.sqlite';
21
+
22
+ /** Default directory under .crewly */
23
+ const AUDIT_DIR = 'audit-logs';
24
+
25
+ /** Maximum rows returned by a single query */
26
+ const MAX_QUERY_LIMIT = 10000;
27
+
28
+ /**
29
+ * Extended audit entry stored in the database.
30
+ * Adds an auto-incremented row ID and an action field for approval events.
31
+ */
32
+ export interface AuditLogRow extends AuditEntry {
33
+ /** Auto-incremented row ID */
34
+ id: number;
35
+ /** Action type: 'tool_call' | 'approval_granted' | 'approval_rejected' | 'approval_expired' */
36
+ action: string;
37
+ /** Who resolved an approval (e.g. 'api', 'auditor', 'auto-expire') */
38
+ resolvedBy?: string;
39
+ /** Approval ID if this entry relates to an approval event */
40
+ approvalId?: string;
41
+ }
42
+
43
+ /**
44
+ * Query filters for the audit log, extending the base AuditLogFilters.
45
+ */
46
+ export interface AuditLogQueryFilters extends AuditLogFilters {
47
+ /** Filter by session name */
48
+ sessionName?: string;
49
+ /** Filter by action type */
50
+ action?: string;
51
+ /** Filter entries after this ISO timestamp */
52
+ since?: string;
53
+ /** Filter entries before this ISO timestamp */
54
+ until?: string;
55
+ /** Offset for pagination */
56
+ offset?: number;
57
+ }
58
+
59
+ /**
60
+ * SQLite-backed persistent audit log service.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const log = new AuditLogService('/path/to/.crewly');
65
+ * log.initialize();
66
+ * log.append(auditEntry);
67
+ * log.logApprovalEvent('approval-123', 'session-1', 'edit_file', 'destructive', 'approved', 'api');
68
+ * const entries = log.query({ limit: 50, sessionName: 'session-1' });
69
+ * log.close();
70
+ * ```
71
+ */
72
+ export class AuditLogService {
73
+ private db: Database.Database | null = null;
74
+ private dbPath: string;
75
+ private initialized = false;
76
+
77
+ /** Prepared statements for performance */
78
+ private stmtInsert: Database.Statement | null = null;
79
+ private stmtInsertApproval: Database.Statement | null = null;
80
+
81
+ /**
82
+ * Create a new AuditLogService.
83
+ *
84
+ * @param crewlyHome - Path to the .crewly directory
85
+ * @param dbFilename - Database filename (default: audit-log.sqlite)
86
+ */
87
+ constructor(crewlyHome: string, dbFilename: string = DB_FILENAME) {
88
+ const dir = join(crewlyHome, AUDIT_DIR);
89
+ this.dbPath = join(dir, dbFilename);
90
+ }
91
+
92
+ /**
93
+ * Initialize the database, creating tables if needed.
94
+ * Enables WAL mode for better concurrent read performance.
95
+ */
96
+ initialize(): void {
97
+ if (this.initialized) return;
98
+
99
+ // Ensure directory exists
100
+ const dir = join(this.dbPath, '..');
101
+ mkdirSync(dir, { recursive: true });
102
+
103
+ this.db = new Database(this.dbPath);
104
+ this.db.pragma('journal_mode = WAL');
105
+ this.db.pragma('foreign_keys = ON');
106
+
107
+ this.db.exec(`
108
+ CREATE TABLE IF NOT EXISTS audit_log (
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ timestamp TEXT NOT NULL,
111
+ session_name TEXT,
112
+ tool_name TEXT NOT NULL,
113
+ sensitivity TEXT NOT NULL,
114
+ args TEXT NOT NULL DEFAULT '{}',
115
+ success INTEGER NOT NULL DEFAULT 1,
116
+ error TEXT,
117
+ duration_ms REAL NOT NULL DEFAULT 0,
118
+ action TEXT NOT NULL DEFAULT 'tool_call',
119
+ resolved_by TEXT,
120
+ approval_id TEXT
121
+ );
122
+
123
+ CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
124
+ CREATE INDEX IF NOT EXISTS idx_audit_session ON audit_log(session_name);
125
+ CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action);
126
+ CREATE INDEX IF NOT EXISTS idx_audit_tool ON audit_log(tool_name);
127
+ `);
128
+
129
+ this.stmtInsert = this.db.prepare(`
130
+ INSERT INTO audit_log (timestamp, session_name, tool_name, sensitivity, args, success, error, duration_ms, action)
131
+ VALUES (@timestamp, @sessionName, @toolName, @sensitivity, @args, @success, @error, @durationMs, 'tool_call')
132
+ `);
133
+
134
+ this.stmtInsertApproval = this.db.prepare(`
135
+ INSERT INTO audit_log (timestamp, session_name, tool_name, sensitivity, args, success, duration_ms, action, resolved_by, approval_id)
136
+ VALUES (@timestamp, @sessionName, @toolName, @sensitivity, @args, 1, 0, @action, @resolvedBy, @approvalId)
137
+ `);
138
+
139
+ this.initialized = true;
140
+ }
141
+
142
+ /**
143
+ * Append a tool-call audit entry.
144
+ *
145
+ * @param entry - The AuditEntry from the agent runtime
146
+ */
147
+ append(entry: AuditEntry): void {
148
+ this.ensureInitialized();
149
+ this.stmtInsert!.run({
150
+ timestamp: entry.timestamp,
151
+ sessionName: entry.sessionName ?? null,
152
+ toolName: entry.toolName,
153
+ sensitivity: entry.sensitivity,
154
+ args: JSON.stringify(entry.args),
155
+ success: entry.success ? 1 : 0,
156
+ error: entry.error ?? null,
157
+ durationMs: entry.durationMs,
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Log an approval lifecycle event (granted, rejected, expired).
163
+ *
164
+ * @param approvalId - The approval request ID
165
+ * @param sessionName - Agent session name
166
+ * @param toolName - Tool that was approved/rejected
167
+ * @param sensitivity - Sensitivity classification
168
+ * @param action - 'approval_granted' | 'approval_rejected' | 'approval_expired'
169
+ * @param resolvedBy - Who resolved it
170
+ */
171
+ logApprovalEvent(
172
+ approvalId: string,
173
+ sessionName: string,
174
+ toolName: string,
175
+ sensitivity: ToolSensitivity,
176
+ action: 'approval_granted' | 'approval_rejected' | 'approval_expired',
177
+ resolvedBy: string,
178
+ ): void {
179
+ this.ensureInitialized();
180
+ this.stmtInsertApproval!.run({
181
+ timestamp: new Date().toISOString(),
182
+ sessionName,
183
+ toolName,
184
+ sensitivity,
185
+ args: '{}',
186
+ action,
187
+ resolvedBy,
188
+ approvalId,
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Query audit log entries with filters and pagination.
194
+ *
195
+ * @param filters - Query filters
196
+ * @returns Array of audit log rows, most recent first
197
+ */
198
+ query(filters: AuditLogQueryFilters): AuditLogRow[] {
199
+ this.ensureInitialized();
200
+
201
+ const conditions: string[] = [];
202
+ const params: Record<string, unknown> = {};
203
+
204
+ if (filters.sessionName) {
205
+ conditions.push('session_name = @sessionName');
206
+ params.sessionName = filters.sessionName;
207
+ }
208
+ if (filters.sensitivity) {
209
+ conditions.push('sensitivity = @sensitivity');
210
+ params.sensitivity = filters.sensitivity;
211
+ }
212
+ if (filters.toolName) {
213
+ conditions.push('tool_name = @toolName');
214
+ params.toolName = filters.toolName;
215
+ }
216
+ if (filters.action) {
217
+ conditions.push('action = @action');
218
+ params.action = filters.action;
219
+ }
220
+ if (filters.since) {
221
+ conditions.push('timestamp >= @since');
222
+ params.since = filters.since;
223
+ }
224
+ if (filters.until) {
225
+ conditions.push('timestamp <= @until');
226
+ params.until = filters.until;
227
+ }
228
+
229
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
230
+ const limit = Math.min(filters.limit || 100, MAX_QUERY_LIMIT);
231
+ const offset = filters.offset || 0;
232
+
233
+ const sql = `
234
+ SELECT id, timestamp, session_name, tool_name, sensitivity, args,
235
+ success, error, duration_ms, action, resolved_by, approval_id
236
+ FROM audit_log
237
+ ${where}
238
+ ORDER BY id DESC
239
+ LIMIT @limit OFFSET @offset
240
+ `;
241
+
242
+ params.limit = limit;
243
+ params.offset = offset;
244
+
245
+ const rows = this.db!.prepare(sql).all(params) as Array<Record<string, unknown>>;
246
+
247
+ return rows.map(row => ({
248
+ id: row.id as number,
249
+ timestamp: row.timestamp as string,
250
+ sessionName: row.session_name as string | undefined,
251
+ toolName: row.tool_name as string,
252
+ sensitivity: row.sensitivity as ToolSensitivity,
253
+ args: JSON.parse((row.args as string) || '{}'),
254
+ success: (row.success as number) === 1,
255
+ error: row.error as string | undefined,
256
+ durationMs: row.duration_ms as number,
257
+ action: row.action as string,
258
+ resolvedBy: row.resolved_by as string | undefined,
259
+ approvalId: row.approval_id as string | undefined,
260
+ }));
261
+ }
262
+
263
+ /**
264
+ * Get total count of entries matching filters (for pagination).
265
+ *
266
+ * @param filters - Same filters as query (limit/offset ignored)
267
+ * @returns Total matching row count
268
+ */
269
+ count(filters: Partial<AuditLogQueryFilters> = {}): number {
270
+ this.ensureInitialized();
271
+
272
+ const conditions: string[] = [];
273
+ const params: Record<string, unknown> = {};
274
+
275
+ if (filters.sessionName) {
276
+ conditions.push('session_name = @sessionName');
277
+ params.sessionName = filters.sessionName;
278
+ }
279
+ if (filters.action) {
280
+ conditions.push('action = @action');
281
+ params.action = filters.action;
282
+ }
283
+
284
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
285
+ const sql = `SELECT COUNT(*) as cnt FROM audit_log ${where}`;
286
+ const row = this.db!.prepare(sql).get(params) as { cnt: number };
287
+ return row.cnt;
288
+ }
289
+
290
+ /**
291
+ * Get the database file path.
292
+ *
293
+ * @returns Absolute path to the SQLite database
294
+ */
295
+ getDbPath(): string {
296
+ return this.dbPath;
297
+ }
298
+
299
+ /**
300
+ * Check if the service is initialized.
301
+ *
302
+ * @returns True if initialize() has been called
303
+ */
304
+ isInitialized(): boolean {
305
+ return this.initialized;
306
+ }
307
+
308
+ /**
309
+ * Close the database connection.
310
+ * Safe to call multiple times.
311
+ */
312
+ close(): void {
313
+ if (this.db) {
314
+ this.db.close();
315
+ this.db = null;
316
+ this.stmtInsert = null;
317
+ this.stmtInsertApproval = null;
318
+ this.initialized = false;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Ensure the service is initialized before operations.
324
+ *
325
+ * @throws Error if not initialized
326
+ */
327
+ private ensureInitialized(): void {
328
+ if (!this.initialized || !this.db) {
329
+ throw new Error('AuditLogService not initialized. Call initialize() first.');
330
+ }
331
+ }
332
+ }