codebot-ai 1.5.0 → 1.7.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/dist/agent.d.ts +13 -0
- package/dist/agent.js +63 -4
- package/dist/audit.d.ts +61 -0
- package/dist/audit.js +231 -0
- package/dist/cli.js +181 -26
- package/dist/mcp.js +54 -3
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +71 -7
- package/dist/plugins.d.ts +0 -14
- package/dist/plugins.js +27 -14
- package/dist/policy.d.ts +123 -0
- package/dist/policy.js +418 -0
- package/dist/sandbox.d.ts +65 -0
- package/dist/sandbox.js +214 -0
- package/dist/secrets.d.ts +26 -0
- package/dist/secrets.js +86 -0
- package/dist/security.d.ts +18 -0
- package/dist/security.js +167 -0
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.js +286 -0
- package/dist/tools/batch-edit.js +20 -1
- package/dist/tools/edit.js +29 -5
- package/dist/tools/execute.js +81 -4
- package/dist/tools/package-manager.js +36 -0
- package/dist/tools/web-fetch.js +42 -2
- package/dist/tools/write.js +17 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Message, AgentEvent, LLMProvider } from './types';
|
|
2
|
+
import { AuditLogger } from './audit';
|
|
3
|
+
import { PolicyEnforcer } from './policy';
|
|
4
|
+
import { TokenTracker } from './telemetry';
|
|
2
5
|
export declare class Agent {
|
|
3
6
|
private provider;
|
|
4
7
|
private tools;
|
|
@@ -9,11 +12,15 @@ export declare class Agent {
|
|
|
9
12
|
private model;
|
|
10
13
|
private cache;
|
|
11
14
|
private rateLimiter;
|
|
15
|
+
private auditLogger;
|
|
16
|
+
private policyEnforcer;
|
|
17
|
+
private tokenTracker;
|
|
12
18
|
private askPermission;
|
|
13
19
|
private onMessage?;
|
|
14
20
|
constructor(opts: {
|
|
15
21
|
provider: LLMProvider;
|
|
16
22
|
model: string;
|
|
23
|
+
providerName?: string;
|
|
17
24
|
maxIterations?: number;
|
|
18
25
|
autoApprove?: boolean;
|
|
19
26
|
askPermission?: (tool: string, args: Record<string, unknown>) => Promise<boolean>;
|
|
@@ -30,6 +37,12 @@ export declare class Agent {
|
|
|
30
37
|
after: number;
|
|
31
38
|
};
|
|
32
39
|
getMessages(): Message[];
|
|
40
|
+
/** Get the token tracker for session summary / CLI display */
|
|
41
|
+
getTokenTracker(): TokenTracker;
|
|
42
|
+
/** Get the policy enforcer for inspection */
|
|
43
|
+
getPolicyEnforcer(): PolicyEnforcer;
|
|
44
|
+
/** Get the audit logger for verification */
|
|
45
|
+
getAuditLogger(): AuditLogger;
|
|
33
46
|
/**
|
|
34
47
|
* Validate and repair message history to prevent OpenAI 400 errors.
|
|
35
48
|
* Handles three types of corruption:
|
package/dist/agent.js
CHANGED
|
@@ -45,6 +45,9 @@ const registry_1 = require("./providers/registry");
|
|
|
45
45
|
const plugins_1 = require("./plugins");
|
|
46
46
|
const cache_1 = require("./cache");
|
|
47
47
|
const rate_limiter_1 = require("./rate-limiter");
|
|
48
|
+
const audit_1 = require("./audit");
|
|
49
|
+
const policy_1 = require("./policy");
|
|
50
|
+
const telemetry_1 = require("./telemetry");
|
|
48
51
|
/** Lightweight schema validation — returns error string or null if valid */
|
|
49
52
|
function validateToolArgs(args, schema) {
|
|
50
53
|
const props = schema.properties;
|
|
@@ -98,6 +101,9 @@ class Agent {
|
|
|
98
101
|
model;
|
|
99
102
|
cache;
|
|
100
103
|
rateLimiter;
|
|
104
|
+
auditLogger;
|
|
105
|
+
policyEnforcer;
|
|
106
|
+
tokenTracker;
|
|
101
107
|
askPermission;
|
|
102
108
|
onMessage;
|
|
103
109
|
constructor(opts) {
|
|
@@ -105,12 +111,21 @@ class Agent {
|
|
|
105
111
|
this.model = opts.model;
|
|
106
112
|
this.tools = new tools_1.ToolRegistry(process.cwd());
|
|
107
113
|
this.context = new manager_1.ContextManager(opts.model, opts.provider);
|
|
108
|
-
|
|
114
|
+
// Load policy
|
|
115
|
+
this.policyEnforcer = new policy_1.PolicyEnforcer((0, policy_1.loadPolicy)(process.cwd()), process.cwd());
|
|
116
|
+
// Use policy-defined max iterations as default, CLI overrides
|
|
117
|
+
this.maxIterations = opts.maxIterations || this.policyEnforcer.getMaxIterations();
|
|
109
118
|
this.autoApprove = opts.autoApprove || false;
|
|
110
119
|
this.askPermission = opts.askPermission || defaultAskPermission;
|
|
111
120
|
this.onMessage = opts.onMessage;
|
|
112
121
|
this.cache = new cache_1.ToolCache();
|
|
113
122
|
this.rateLimiter = new rate_limiter_1.RateLimiter();
|
|
123
|
+
this.auditLogger = new audit_1.AuditLogger();
|
|
124
|
+
// Token & cost tracking
|
|
125
|
+
this.tokenTracker = new telemetry_1.TokenTracker(opts.model, opts.providerName || 'unknown');
|
|
126
|
+
const costLimit = this.policyEnforcer.getCostLimitUsd();
|
|
127
|
+
if (costLimit > 0)
|
|
128
|
+
this.tokenTracker.setCostLimit(costLimit);
|
|
114
129
|
// Load plugins
|
|
115
130
|
try {
|
|
116
131
|
const plugins = (0, plugins_1.loadPlugins)(process.cwd());
|
|
@@ -177,6 +192,10 @@ class Agent {
|
|
|
177
192
|
}
|
|
178
193
|
break;
|
|
179
194
|
case 'usage':
|
|
195
|
+
// Track tokens and cost
|
|
196
|
+
if (event.usage) {
|
|
197
|
+
this.tokenTracker.recordUsage(event.usage.inputTokens || 0, event.usage.outputTokens || 0);
|
|
198
|
+
}
|
|
180
199
|
yield { type: 'usage', usage: event.usage };
|
|
181
200
|
break;
|
|
182
201
|
case 'error':
|
|
@@ -228,6 +247,11 @@ class Agent {
|
|
|
228
247
|
yield { type: 'done' };
|
|
229
248
|
return;
|
|
230
249
|
}
|
|
250
|
+
// Cost budget check: stop if over limit
|
|
251
|
+
if (this.tokenTracker.isOverBudget()) {
|
|
252
|
+
yield { type: 'error', error: `Cost limit exceeded ($${this.tokenTracker.getTotalCost().toFixed(4)} / $${this.policyEnforcer.getCostLimitUsd().toFixed(2)}). Stopping.` };
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
231
255
|
const prepared = [];
|
|
232
256
|
for (const tc of toolCalls) {
|
|
233
257
|
const toolName = tc.function.name;
|
|
@@ -236,6 +260,13 @@ class Agent {
|
|
|
236
260
|
prepared.push({ tc, tool: null, args: {}, denied: false, error: `Error: Unknown tool "${toolName}"` });
|
|
237
261
|
continue;
|
|
238
262
|
}
|
|
263
|
+
// Policy check: is this tool allowed?
|
|
264
|
+
const policyCheck = this.policyEnforcer.isToolAllowed(toolName);
|
|
265
|
+
if (!policyCheck.allowed) {
|
|
266
|
+
this.auditLogger.log({ tool: toolName, action: 'policy_block', args: {}, reason: policyCheck.reason });
|
|
267
|
+
prepared.push({ tc, tool, args: {}, denied: false, error: `Error: ${policyCheck.reason}` });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
239
270
|
let args;
|
|
240
271
|
try {
|
|
241
272
|
args = JSON.parse(tc.function.arguments);
|
|
@@ -251,14 +282,17 @@ class Agent {
|
|
|
251
282
|
continue;
|
|
252
283
|
}
|
|
253
284
|
yield { type: 'tool_call', toolCall: { name: toolName, args } };
|
|
254
|
-
// Permission check
|
|
255
|
-
const
|
|
256
|
-
|
|
285
|
+
// Permission check: policy override > tool default
|
|
286
|
+
const policyPermission = this.policyEnforcer.getToolPermission(toolName);
|
|
287
|
+
const effectivePermission = policyPermission || tool.permission;
|
|
288
|
+
const needsPermission = effectivePermission === 'always-ask' ||
|
|
289
|
+
(effectivePermission === 'prompt' && !this.autoApprove);
|
|
257
290
|
let denied = false;
|
|
258
291
|
if (needsPermission) {
|
|
259
292
|
const approved = await this.askPermission(toolName, args);
|
|
260
293
|
if (!approved) {
|
|
261
294
|
denied = true;
|
|
295
|
+
this.auditLogger.log({ tool: toolName, action: 'deny', args, reason: 'User denied permission' });
|
|
262
296
|
}
|
|
263
297
|
}
|
|
264
298
|
prepared.push({ tc, tool, args, denied });
|
|
@@ -304,6 +338,13 @@ class Agent {
|
|
|
304
338
|
await this.rateLimiter.throttle(toolName);
|
|
305
339
|
try {
|
|
306
340
|
const output = await prep.tool.execute(prep.args);
|
|
341
|
+
// Audit log: successful execution
|
|
342
|
+
this.auditLogger.log({ tool: toolName, action: 'execute', args: prep.args, result: 'success' });
|
|
343
|
+
// Telemetry: track tool calls and file modifications
|
|
344
|
+
this.tokenTracker.recordToolCall();
|
|
345
|
+
if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && prep.args.path) {
|
|
346
|
+
this.tokenTracker.recordFileModified(prep.args.path);
|
|
347
|
+
}
|
|
307
348
|
// Store in cache for cacheable tools
|
|
308
349
|
if (prep.tool.cacheable) {
|
|
309
350
|
const ttl = cache_1.ToolCache.TTL[toolName] || 30_000;
|
|
@@ -315,10 +356,16 @@ class Agent {
|
|
|
315
356
|
if (filePath)
|
|
316
357
|
this.cache.invalidate(filePath);
|
|
317
358
|
}
|
|
359
|
+
// Audit log: check if tool returned a security block
|
|
360
|
+
if (output.startsWith('Error: Blocked:') || output.startsWith('Error: CWD')) {
|
|
361
|
+
this.auditLogger.log({ tool: toolName, action: 'security_block', args: prep.args, reason: output });
|
|
362
|
+
}
|
|
318
363
|
return { content: output };
|
|
319
364
|
}
|
|
320
365
|
catch (err) {
|
|
321
366
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
367
|
+
// Audit log: error
|
|
368
|
+
this.auditLogger.log({ tool: toolName, action: 'error', args: prep.args, result: 'error', reason: errMsg });
|
|
322
369
|
return { content: `Error: ${errMsg}`, is_error: true };
|
|
323
370
|
}
|
|
324
371
|
};
|
|
@@ -375,6 +422,18 @@ class Agent {
|
|
|
375
422
|
getMessages() {
|
|
376
423
|
return [...this.messages];
|
|
377
424
|
}
|
|
425
|
+
/** Get the token tracker for session summary / CLI display */
|
|
426
|
+
getTokenTracker() {
|
|
427
|
+
return this.tokenTracker;
|
|
428
|
+
}
|
|
429
|
+
/** Get the policy enforcer for inspection */
|
|
430
|
+
getPolicyEnforcer() {
|
|
431
|
+
return this.policyEnforcer;
|
|
432
|
+
}
|
|
433
|
+
/** Get the audit logger for verification */
|
|
434
|
+
getAuditLogger() {
|
|
435
|
+
return this.auditLogger;
|
|
436
|
+
}
|
|
378
437
|
/**
|
|
379
438
|
* Validate and repair message history to prevent OpenAI 400 errors.
|
|
380
439
|
* Handles three types of corruption:
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit logger for CodeBot v1.7.0
|
|
3
|
+
*
|
|
4
|
+
* Provides append-only JSONL logging of all security-relevant actions.
|
|
5
|
+
* Logs are stored at ~/.codebot/audit/audit-YYYY-MM-DD.jsonl
|
|
6
|
+
*
|
|
7
|
+
* v1.7.0: Hash-chained entries for tamper detection.
|
|
8
|
+
* Each entry includes a SHA-256 hash of (prevHash + entry content).
|
|
9
|
+
* Verification walks the chain and detects any modifications.
|
|
10
|
+
*
|
|
11
|
+
* NEVER throws — audit failures must not crash the agent.
|
|
12
|
+
*/
|
|
13
|
+
export interface AuditEntry {
|
|
14
|
+
timestamp: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
sequence: number;
|
|
17
|
+
tool: string;
|
|
18
|
+
action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block';
|
|
19
|
+
args: Record<string, unknown>;
|
|
20
|
+
result?: string;
|
|
21
|
+
reason?: string;
|
|
22
|
+
prevHash: string;
|
|
23
|
+
hash: string;
|
|
24
|
+
}
|
|
25
|
+
/** Result of verifying an audit chain */
|
|
26
|
+
export interface VerifyResult {
|
|
27
|
+
valid: boolean;
|
|
28
|
+
entriesChecked: number;
|
|
29
|
+
firstInvalidAt?: number;
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare class AuditLogger {
|
|
33
|
+
private logDir;
|
|
34
|
+
private sessionId;
|
|
35
|
+
private sequence;
|
|
36
|
+
private prevHash;
|
|
37
|
+
constructor(logDir?: string);
|
|
38
|
+
getSessionId(): string;
|
|
39
|
+
/** Append a hash-chained audit entry to the log file */
|
|
40
|
+
log(entry: Omit<AuditEntry, 'timestamp' | 'sessionId' | 'sequence' | 'prevHash' | 'hash'>): void;
|
|
41
|
+
/** Read log entries, optionally filtered */
|
|
42
|
+
query(filter?: {
|
|
43
|
+
tool?: string;
|
|
44
|
+
action?: string;
|
|
45
|
+
since?: string;
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
}): AuditEntry[];
|
|
48
|
+
/**
|
|
49
|
+
* Verify the hash chain integrity of audit entries.
|
|
50
|
+
* Walks through entries for a given session and checks each hash.
|
|
51
|
+
*/
|
|
52
|
+
static verify(entries: AuditEntry[]): VerifyResult;
|
|
53
|
+
/**
|
|
54
|
+
* Verify all entries for a given session.
|
|
55
|
+
*/
|
|
56
|
+
verifySession(sessionId?: string): VerifyResult;
|
|
57
|
+
private getLogFilePath;
|
|
58
|
+
private rotateIfNeeded;
|
|
59
|
+
private sanitizeArgs;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=audit.d.ts.map
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AuditLogger = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const secrets_1 = require("./secrets");
|
|
42
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB before rotation
|
|
43
|
+
const MAX_ARG_LENGTH = 500;
|
|
44
|
+
const GENESIS_HASH = 'genesis';
|
|
45
|
+
class AuditLogger {
|
|
46
|
+
logDir;
|
|
47
|
+
sessionId;
|
|
48
|
+
sequence = 0;
|
|
49
|
+
prevHash = GENESIS_HASH;
|
|
50
|
+
constructor(logDir) {
|
|
51
|
+
this.logDir = logDir || path.join(os.homedir(), '.codebot', 'audit');
|
|
52
|
+
this.sessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Can't create dir — logging will be disabled
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
getSessionId() {
|
|
61
|
+
return this.sessionId;
|
|
62
|
+
}
|
|
63
|
+
/** Append a hash-chained audit entry to the log file */
|
|
64
|
+
log(entry) {
|
|
65
|
+
try {
|
|
66
|
+
this.sequence++;
|
|
67
|
+
// Build entry without hash first (hash is computed over the other fields)
|
|
68
|
+
const partial = {
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
sessionId: this.sessionId,
|
|
71
|
+
sequence: this.sequence,
|
|
72
|
+
tool: entry.tool,
|
|
73
|
+
action: entry.action,
|
|
74
|
+
args: this.sanitizeArgs(entry.args),
|
|
75
|
+
result: entry.result,
|
|
76
|
+
reason: entry.reason,
|
|
77
|
+
prevHash: this.prevHash,
|
|
78
|
+
};
|
|
79
|
+
// Compute hash: SHA-256 of (prevHash + JSON of partial entry)
|
|
80
|
+
const hashInput = this.prevHash + JSON.stringify(partial);
|
|
81
|
+
const hash = crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
82
|
+
const fullEntry = { ...partial, hash };
|
|
83
|
+
this.prevHash = hash;
|
|
84
|
+
const logFile = this.getLogFilePath();
|
|
85
|
+
const line = JSON.stringify(fullEntry) + '\n';
|
|
86
|
+
this.rotateIfNeeded(logFile);
|
|
87
|
+
fs.appendFileSync(logFile, line, 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Audit failures must NEVER crash the agent
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Read log entries, optionally filtered */
|
|
94
|
+
query(filter) {
|
|
95
|
+
const entries = [];
|
|
96
|
+
try {
|
|
97
|
+
const files = fs.readdirSync(this.logDir)
|
|
98
|
+
.filter(f => f.startsWith('audit-') && f.endsWith('.jsonl'))
|
|
99
|
+
.sort();
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
const content = fs.readFileSync(path.join(this.logDir, file), 'utf-8');
|
|
102
|
+
for (const line of content.split('\n')) {
|
|
103
|
+
if (!line.trim())
|
|
104
|
+
continue;
|
|
105
|
+
try {
|
|
106
|
+
const entry = JSON.parse(line);
|
|
107
|
+
if (filter?.tool && entry.tool !== filter.tool)
|
|
108
|
+
continue;
|
|
109
|
+
if (filter?.action && entry.action !== filter.action)
|
|
110
|
+
continue;
|
|
111
|
+
if (filter?.since && entry.timestamp < filter.since)
|
|
112
|
+
continue;
|
|
113
|
+
if (filter?.sessionId && entry.sessionId !== filter.sessionId)
|
|
114
|
+
continue;
|
|
115
|
+
entries.push(entry);
|
|
116
|
+
}
|
|
117
|
+
catch { /* skip malformed */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Can't read logs
|
|
123
|
+
}
|
|
124
|
+
return entries;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Verify the hash chain integrity of audit entries.
|
|
128
|
+
* Walks through entries for a given session and checks each hash.
|
|
129
|
+
*/
|
|
130
|
+
static verify(entries) {
|
|
131
|
+
if (entries.length === 0) {
|
|
132
|
+
return { valid: true, entriesChecked: 0 };
|
|
133
|
+
}
|
|
134
|
+
// Sort by sequence
|
|
135
|
+
const sorted = [...entries].sort((a, b) => a.sequence - b.sequence);
|
|
136
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
137
|
+
const entry = sorted[i];
|
|
138
|
+
// Check sequence continuity
|
|
139
|
+
if (i === 0 && entry.prevHash !== GENESIS_HASH) {
|
|
140
|
+
// First entry should reference genesis (unless it's a continuation)
|
|
141
|
+
// Allow non-genesis for continuation of previous sessions
|
|
142
|
+
}
|
|
143
|
+
// Recompute hash
|
|
144
|
+
const partial = {
|
|
145
|
+
timestamp: entry.timestamp,
|
|
146
|
+
sessionId: entry.sessionId,
|
|
147
|
+
sequence: entry.sequence,
|
|
148
|
+
tool: entry.tool,
|
|
149
|
+
action: entry.action,
|
|
150
|
+
args: entry.args,
|
|
151
|
+
result: entry.result,
|
|
152
|
+
reason: entry.reason,
|
|
153
|
+
prevHash: entry.prevHash,
|
|
154
|
+
};
|
|
155
|
+
const hashInput = entry.prevHash + JSON.stringify(partial);
|
|
156
|
+
const expectedHash = crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
157
|
+
if (entry.hash !== expectedHash) {
|
|
158
|
+
return {
|
|
159
|
+
valid: false,
|
|
160
|
+
entriesChecked: i + 1,
|
|
161
|
+
firstInvalidAt: entry.sequence,
|
|
162
|
+
reason: `Hash mismatch at sequence ${entry.sequence}: expected ${expectedHash.substring(0, 16)}..., got ${entry.hash.substring(0, 16)}...`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Check chain continuity (sequence i+1 should reference sequence i's hash)
|
|
166
|
+
if (i < sorted.length - 1) {
|
|
167
|
+
const next = sorted[i + 1];
|
|
168
|
+
if (next.prevHash !== entry.hash) {
|
|
169
|
+
return {
|
|
170
|
+
valid: false,
|
|
171
|
+
entriesChecked: i + 2,
|
|
172
|
+
firstInvalidAt: next.sequence,
|
|
173
|
+
reason: `Chain break at sequence ${next.sequence}: prevHash doesn't match previous entry's hash`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { valid: true, entriesChecked: sorted.length };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Verify all entries for a given session.
|
|
182
|
+
*/
|
|
183
|
+
verifySession(sessionId) {
|
|
184
|
+
const sid = sessionId || this.sessionId;
|
|
185
|
+
const entries = this.query({ sessionId: sid });
|
|
186
|
+
return AuditLogger.verify(entries);
|
|
187
|
+
}
|
|
188
|
+
getLogFilePath() {
|
|
189
|
+
const date = new Date().toISOString().split('T')[0];
|
|
190
|
+
return path.join(this.logDir, `audit-${date}.jsonl`);
|
|
191
|
+
}
|
|
192
|
+
rotateIfNeeded(logFile) {
|
|
193
|
+
try {
|
|
194
|
+
if (!fs.existsSync(logFile))
|
|
195
|
+
return;
|
|
196
|
+
const stat = fs.statSync(logFile);
|
|
197
|
+
if (stat.size >= MAX_LOG_SIZE) {
|
|
198
|
+
const rotated = logFile.replace('.jsonl', `-${Date.now()}.jsonl`);
|
|
199
|
+
fs.renameSync(logFile, rotated);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Rotation failure is non-fatal
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
sanitizeArgs(args) {
|
|
207
|
+
const sanitized = {};
|
|
208
|
+
for (const [key, value] of Object.entries(args)) {
|
|
209
|
+
if (typeof value === 'string') {
|
|
210
|
+
let masked = (0, secrets_1.maskSecretsInString)(value);
|
|
211
|
+
if (masked.length > MAX_ARG_LENGTH) {
|
|
212
|
+
masked = masked.substring(0, MAX_ARG_LENGTH) + `... (${value.length} chars)`;
|
|
213
|
+
}
|
|
214
|
+
sanitized[key] = masked;
|
|
215
|
+
}
|
|
216
|
+
else if (typeof value === 'object' && value !== null) {
|
|
217
|
+
const str = JSON.stringify(value);
|
|
218
|
+
const masked = (0, secrets_1.maskSecretsInString)(str);
|
|
219
|
+
sanitized[key] = masked.length > MAX_ARG_LENGTH
|
|
220
|
+
? masked.substring(0, MAX_ARG_LENGTH) + '...'
|
|
221
|
+
: masked;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
sanitized[key] = value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return sanitized;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
exports.AuditLogger = AuditLogger;
|
|
231
|
+
//# sourceMappingURL=audit.js.map
|