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.
- package/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
- package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
- package/config/skills/agent/web-search/SKILL.md +70 -0
- package/config/skills/agent/web-search/execute.sh +170 -0
- package/config/skills/agent/web-search/skill.json +23 -0
- package/dist/backend/backend/src/constants.d.ts +34 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +34 -1
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +36 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +90 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.js +309 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.js +134 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts +78 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.js +358 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup.types.d.ts +163 -0
- package/dist/backend/backend/src/services/backup/backup.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup.types.js +13 -0
- package/dist/backend/backend/src/services/backup/backup.types.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +29 -2
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +97 -13
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +67 -2
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
- package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.js +8 -0
- package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.js +1 -0
- package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +34 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +34 -1
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts +70 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts.map +1 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js +427 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +90 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.js +309 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.js +134 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts +78 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.js +358 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup.types.d.ts +163 -0
- package/dist/cli/backend/src/services/backup/backup.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup.types.js +13 -0
- package/dist/cli/backend/src/services/backup/backup.types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +410 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js +863 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +292 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +1093 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +328 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +171 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts +89 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.js +148 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.js.map +1 -0
- package/dist/cli/backend/src/services/user/user-identity.service.d.ts +86 -0
- package/dist/cli/backend/src/services/user/user-identity.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/user/user-identity.service.js +190 -0
- package/dist/cli/backend/src/services/user/user-identity.service.js.map +1 -0
- package/dist/cli/cli/src/commands/backup.d.ts +31 -0
- package/dist/cli/cli/src/commands/backup.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/backup.js +280 -0
- package/dist/cli/cli/src/commands/backup.js.map +1 -0
- package/dist/cli/cli/src/index.js +10 -0
- package/dist/cli/cli/src/index.js.map +1 -1
- package/package.json +9 -3
- package/packages/crewly-agent/README.md +27 -0
- package/packages/crewly-agent/bin/crewly-agent +33 -0
- package/packages/crewly-agent/package.json +39 -0
- package/packages/crewly-agent/src/cli.ts +168 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
- package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
- package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
- package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
- package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
- package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
- package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
- package/packages/crewly-agent/src/runtime/index.ts +38 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
- package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
- package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
- package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
- package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
- package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
- package/packages/crewly-agent/src/runtime/types.ts +637 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
- 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
|
+
}
|