codebot-ai 1.7.0 → 1.8.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 CHANGED
@@ -15,6 +15,7 @@ export declare class Agent {
15
15
  private auditLogger;
16
16
  private policyEnforcer;
17
17
  private tokenTracker;
18
+ private branchCreated;
18
19
  private askPermission;
19
20
  private onMessage?;
20
21
  constructor(opts: {
@@ -54,6 +55,15 @@ export declare class Agent {
54
55
  * or session resume corruption.
55
56
  */
56
57
  private repairToolCallMessages;
58
+ /**
59
+ * Auto-create a feature branch when always_branch is enabled and on main/master.
60
+ * Called before the first write/edit operation. Fail-open: if branching fails, continue.
61
+ */
62
+ private ensureBranch;
63
+ /** Sanitize user message into a branch-safe slug. */
64
+ private sanitizeSlug;
65
+ /** Check capability-based restrictions before tool execution. Returns reason string or null. */
66
+ private checkToolCapabilities;
57
67
  private buildSystemPrompt;
58
68
  }
59
69
  //# sourceMappingURL=agent.d.ts.map
package/dist/agent.js CHANGED
@@ -104,15 +104,16 @@ class Agent {
104
104
  auditLogger;
105
105
  policyEnforcer;
106
106
  tokenTracker;
107
+ branchCreated = false;
107
108
  askPermission;
108
109
  onMessage;
109
110
  constructor(opts) {
110
111
  this.provider = opts.provider;
111
112
  this.model = opts.model;
112
- this.tools = new tools_1.ToolRegistry(process.cwd());
113
- this.context = new manager_1.ContextManager(opts.model, opts.provider);
114
- // Load policy
113
+ // Load policy FIRST — tools need it for filesystem/git enforcement
115
114
  this.policyEnforcer = new policy_1.PolicyEnforcer((0, policy_1.loadPolicy)(process.cwd()), process.cwd());
115
+ this.tools = new tools_1.ToolRegistry(process.cwd(), this.policyEnforcer);
116
+ this.context = new manager_1.ContextManager(opts.model, opts.provider);
116
117
  // Use policy-defined max iterations as default, CLI overrides
117
118
  this.maxIterations = opts.maxIterations || this.policyEnforcer.getMaxIterations();
118
119
  this.autoApprove = opts.autoApprove || false;
@@ -326,6 +327,19 @@ class Agent {
326
327
  // Helper to execute a single tool with cache + rate limiting
327
328
  const executeTool = async (prep) => {
328
329
  const toolName = prep.tc.function.name;
330
+ // Auto-branch on first write/edit when always_branch is enabled (v1.8.0)
331
+ if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') {
332
+ const branchName = await this.ensureBranch();
333
+ if (branchName) {
334
+ this.auditLogger.log({ tool: 'git', action: 'execute', args: { branch: branchName }, result: 'auto-branch' });
335
+ }
336
+ }
337
+ // Capability check: fine-grained resource restrictions (v1.8.0)
338
+ const capBlock = this.checkToolCapabilities(toolName, prep.args);
339
+ if (capBlock) {
340
+ this.auditLogger.log({ tool: toolName, action: 'capability_block', args: prep.args, reason: capBlock });
341
+ return { content: `Error: ${capBlock}`, is_error: true };
342
+ }
329
343
  // Check cache first
330
344
  if (prep.tool.cacheable) {
331
345
  const cacheKey = cache_1.ToolCache.key(toolName, prep.args);
@@ -501,6 +515,78 @@ class Agent {
501
515
  }
502
516
  }
503
517
  }
518
+ /**
519
+ * Auto-create a feature branch when always_branch is enabled and on main/master.
520
+ * Called before the first write/edit operation. Fail-open: if branching fails, continue.
521
+ */
522
+ async ensureBranch() {
523
+ if (this.branchCreated)
524
+ return null;
525
+ if (!this.policyEnforcer.shouldAlwaysBranch())
526
+ return null;
527
+ try {
528
+ const { execSync } = require('child_process');
529
+ const cwd = process.cwd();
530
+ const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
531
+ cwd, encoding: 'utf-8', timeout: 5000,
532
+ }).trim();
533
+ if (currentBranch !== 'main' && currentBranch !== 'master') {
534
+ this.branchCreated = true;
535
+ return null; // Already on a feature branch
536
+ }
537
+ // Generate branch name from first user message
538
+ const firstUserMsg = this.messages.find(m => m.role === 'user');
539
+ const prefix = this.policyEnforcer.getBranchPrefix();
540
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
541
+ const slug = this.sanitizeSlug(firstUserMsg?.content || 'task');
542
+ const branchName = `${prefix}${timestamp}-${slug}`;
543
+ execSync(`git checkout -b "${branchName}"`, {
544
+ cwd, encoding: 'utf-8', timeout: 10000,
545
+ });
546
+ this.branchCreated = true;
547
+ return branchName;
548
+ }
549
+ catch {
550
+ // Don't block the operation if branching fails (not in a git repo, etc.)
551
+ this.branchCreated = true; // Don't retry
552
+ return null;
553
+ }
554
+ }
555
+ /** Sanitize user message into a branch-safe slug. */
556
+ sanitizeSlug(message) {
557
+ return message
558
+ .toLowerCase()
559
+ .replace(/[^a-z0-9\s-]/g, '')
560
+ .replace(/\s+/g, '-')
561
+ .substring(0, 30)
562
+ .replace(/-+$/, '') || 'task';
563
+ }
564
+ /** Check capability-based restrictions before tool execution. Returns reason string or null. */
565
+ checkToolCapabilities(toolName, args) {
566
+ // fs_write check for write/edit tools
567
+ if ((toolName === 'write_file' || toolName === 'edit_file' || toolName === 'batch_edit') && args.path) {
568
+ const check = this.policyEnforcer.checkCapability(toolName, 'fs_write', args.path);
569
+ if (!check.allowed)
570
+ return check.reason || 'Capability blocked';
571
+ }
572
+ // shell_commands check for execute tool
573
+ if (toolName === 'execute' && args.command) {
574
+ const check = this.policyEnforcer.checkCapability(toolName, 'shell_commands', args.command);
575
+ if (!check.allowed)
576
+ return check.reason || 'Capability blocked';
577
+ }
578
+ // net_access check for web tools
579
+ if ((toolName === 'web_fetch' || toolName === 'http_client') && args.url) {
580
+ try {
581
+ const domain = new URL(args.url).hostname;
582
+ const check = this.policyEnforcer.checkCapability(toolName, 'net_access', domain);
583
+ if (!check.allowed)
584
+ return check.reason || 'Capability blocked';
585
+ }
586
+ catch { /* invalid URL handled by the tool itself */ }
587
+ }
588
+ return null;
589
+ }
504
590
  buildSystemPrompt(supportsTools) {
505
591
  let repoMap = '';
506
592
  try {
package/dist/audit.d.ts CHANGED
@@ -15,7 +15,7 @@ export interface AuditEntry {
15
15
  sessionId: string;
16
16
  sequence: number;
17
17
  tool: string;
18
- action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block';
18
+ action: 'execute' | 'deny' | 'error' | 'security_block' | 'policy_block' | 'capability_block';
19
19
  args: Record<string, unknown>;
20
20
  result?: string;
21
21
  reason?: string;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Capability-Based Tool Permissions for CodeBot v1.8.0
3
+ *
4
+ * Fine-grained, per-tool resource restrictions.
5
+ * Configured via .codebot/policy.json → tools.capabilities.
6
+ *
7
+ * Example:
8
+ * {
9
+ * "execute": {
10
+ * "shell_commands": ["npm", "node", "git", "tsc"],
11
+ * "max_output_kb": 500
12
+ * },
13
+ * "write_file": {
14
+ * "fs_write": ["./src/**", "./tests/**"]
15
+ * }
16
+ * }
17
+ */
18
+ export interface ToolCapabilities {
19
+ fs_read?: string[];
20
+ fs_write?: string[];
21
+ net_access?: string[];
22
+ shell_commands?: string[];
23
+ max_output_kb?: number;
24
+ }
25
+ export type CapabilityConfig = Record<string, ToolCapabilities>;
26
+ export declare class CapabilityChecker {
27
+ private capabilities;
28
+ private projectRoot;
29
+ constructor(capabilities: CapabilityConfig, projectRoot: string);
30
+ /** Get capabilities for a tool. undefined = no restrictions. */
31
+ getToolCapabilities(toolName: string): ToolCapabilities | undefined;
32
+ /** Check if a specific capability is allowed. */
33
+ checkCapability(toolName: string, capabilityType: keyof ToolCapabilities, value: string | number): {
34
+ allowed: boolean;
35
+ reason?: string;
36
+ };
37
+ /** Check if a path matches any of the allowed glob patterns. */
38
+ private checkGlobs;
39
+ /** Check if a domain is in the allowed list. */
40
+ private checkDomain;
41
+ /** Check if a command starts with an allowed prefix. */
42
+ private checkCommandPrefix;
43
+ /** Check if output size is within the cap. */
44
+ private checkOutputSize;
45
+ /** Simple glob matching (** = any depth, * = one segment). */
46
+ private matchGlob;
47
+ }
48
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * Capability-Based Tool Permissions for CodeBot v1.8.0
4
+ *
5
+ * Fine-grained, per-tool resource restrictions.
6
+ * Configured via .codebot/policy.json → tools.capabilities.
7
+ *
8
+ * Example:
9
+ * {
10
+ * "execute": {
11
+ * "shell_commands": ["npm", "node", "git", "tsc"],
12
+ * "max_output_kb": 500
13
+ * },
14
+ * "write_file": {
15
+ * "fs_write": ["./src/**", "./tests/**"]
16
+ * }
17
+ * }
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.CapabilityChecker = void 0;
54
+ const path = __importStar(require("path"));
55
+ // ── Capability Checker ──
56
+ class CapabilityChecker {
57
+ capabilities;
58
+ projectRoot;
59
+ constructor(capabilities, projectRoot) {
60
+ this.capabilities = capabilities;
61
+ this.projectRoot = projectRoot;
62
+ }
63
+ /** Get capabilities for a tool. undefined = no restrictions. */
64
+ getToolCapabilities(toolName) {
65
+ return this.capabilities[toolName];
66
+ }
67
+ /** Check if a specific capability is allowed. */
68
+ checkCapability(toolName, capabilityType, value) {
69
+ const caps = this.capabilities[toolName];
70
+ if (!caps)
71
+ return { allowed: true }; // No caps defined = unrestricted
72
+ switch (capabilityType) {
73
+ case 'fs_read':
74
+ return this.checkGlobs(caps.fs_read, value, 'fs_read', toolName);
75
+ case 'fs_write':
76
+ return this.checkGlobs(caps.fs_write, value, 'fs_write', toolName);
77
+ case 'net_access':
78
+ return this.checkDomain(caps.net_access, value, toolName);
79
+ case 'shell_commands':
80
+ return this.checkCommandPrefix(caps.shell_commands, value, toolName);
81
+ case 'max_output_kb':
82
+ return this.checkOutputSize(caps.max_output_kb, value, toolName);
83
+ default:
84
+ return { allowed: true };
85
+ }
86
+ }
87
+ /** Check if a path matches any of the allowed glob patterns. */
88
+ checkGlobs(patterns, filePath, capName, toolName) {
89
+ if (!patterns || patterns.length === 0)
90
+ return { allowed: true };
91
+ const resolved = path.resolve(filePath);
92
+ const relative = path.relative(this.projectRoot, resolved);
93
+ // Don't restrict paths outside the project (those are handled by security.ts)
94
+ if (relative.startsWith('..'))
95
+ return { allowed: true };
96
+ for (const pattern of patterns) {
97
+ if (this.matchGlob(relative, pattern)) {
98
+ return { allowed: true };
99
+ }
100
+ }
101
+ return {
102
+ allowed: false,
103
+ reason: `Tool "${toolName}" ${capName} capability blocks "${relative}" (allowed: ${patterns.join(', ')})`,
104
+ };
105
+ }
106
+ /** Check if a domain is in the allowed list. */
107
+ checkDomain(allowed, domain, toolName) {
108
+ if (allowed === undefined)
109
+ return { allowed: true }; // undefined = unrestricted
110
+ if (allowed.length === 0) {
111
+ // Empty array = no network access allowed
112
+ return {
113
+ allowed: false,
114
+ reason: `Tool "${toolName}" has no allowed network domains`,
115
+ };
116
+ }
117
+ if (allowed.includes('*'))
118
+ return { allowed: true };
119
+ const normalizedDomain = domain.toLowerCase();
120
+ for (const d of allowed) {
121
+ const nd = d.toLowerCase();
122
+ if (normalizedDomain === nd)
123
+ return { allowed: true };
124
+ if (normalizedDomain.endsWith('.' + nd))
125
+ return { allowed: true };
126
+ }
127
+ return {
128
+ allowed: false,
129
+ reason: `Tool "${toolName}" cannot access domain "${domain}" (allowed: ${allowed.join(', ')})`,
130
+ };
131
+ }
132
+ /** Check if a command starts with an allowed prefix. */
133
+ checkCommandPrefix(allowed, command, toolName) {
134
+ if (!allowed || allowed.length === 0)
135
+ return { allowed: true };
136
+ const cmd = command.trim();
137
+ for (const prefix of allowed) {
138
+ if (cmd === prefix || cmd.startsWith(prefix + ' ')) {
139
+ return { allowed: true };
140
+ }
141
+ }
142
+ // Extract first word for the error message
143
+ const firstWord = cmd.split(/\s+/)[0] || cmd.substring(0, 30);
144
+ return {
145
+ allowed: false,
146
+ reason: `Tool "${toolName}" cannot run "${firstWord}" (allowed commands: ${allowed.join(', ')})`,
147
+ };
148
+ }
149
+ /** Check if output size is within the cap. */
150
+ checkOutputSize(maxKb, actualKb, toolName) {
151
+ if (maxKb === undefined || maxKb <= 0)
152
+ return { allowed: true };
153
+ if (actualKb <= maxKb)
154
+ return { allowed: true };
155
+ return {
156
+ allowed: false,
157
+ reason: `Tool "${toolName}" output ${actualKb}KB exceeds cap of ${maxKb}KB`,
158
+ };
159
+ }
160
+ /** Simple glob matching (** = any depth, * = one segment). */
161
+ matchGlob(relativePath, pattern) {
162
+ const cleanPattern = pattern.replace(/^\.\//, '');
163
+ const cleanPath = relativePath.replace(/^\.\//, '');
164
+ // Exact match
165
+ if (cleanPath === cleanPattern)
166
+ return true;
167
+ // Prefix match (directory)
168
+ if (cleanPath.startsWith(cleanPattern + '/'))
169
+ return true;
170
+ if (cleanPath.startsWith(cleanPattern + path.sep))
171
+ return true;
172
+ // Glob expansion
173
+ if (pattern.includes('*')) {
174
+ const regex = new RegExp('^' +
175
+ cleanPattern
176
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
177
+ .replace(/\*\*/g, '<<GLOBSTAR>>')
178
+ .replace(/\*/g, '[^/]*')
179
+ .replace(/<<GLOBSTAR>>/g, '.*') +
180
+ '$');
181
+ return regex.test(cleanPath);
182
+ }
183
+ return false;
184
+ }
185
+ }
186
+ exports.CapabilityChecker = CapabilityChecker;
187
+ //# sourceMappingURL=capabilities.js.map
package/dist/cli.js CHANGED
@@ -49,7 +49,8 @@ const scheduler_1 = require("./scheduler");
49
49
  const audit_1 = require("./audit");
50
50
  const policy_1 = require("./policy");
51
51
  const sandbox_1 = require("./sandbox");
52
- const VERSION = '1.7.0';
52
+ const replay_1 = require("./replay");
53
+ const VERSION = '1.8.0';
53
54
  const C = {
54
55
  reset: '\x1b[0m',
55
56
  bold: '\x1b[1m',
@@ -168,6 +169,65 @@ async function main() {
168
169
  console.log(` Network: ${info.defaults.network ? 'enabled' : 'disabled'} by default`);
169
170
  return;
170
171
  }
172
+ // --replay: Replay a saved session
173
+ if (args.replay) {
174
+ const replayId = typeof args.replay === 'string'
175
+ ? args.replay
176
+ : history_1.SessionManager.latest();
177
+ if (!replayId) {
178
+ console.log(c('No session to replay. Specify an ID or ensure a previous session exists.', 'yellow'));
179
+ return;
180
+ }
181
+ const data = (0, replay_1.loadSessionForReplay)(replayId);
182
+ if (!data) {
183
+ console.log(c(`Session ${replayId} not found or empty.`, 'red'));
184
+ return;
185
+ }
186
+ console.log(c(`\nReplaying session ${replayId.substring(0, 12)}...`, 'cyan'));
187
+ console.log(c(` ${data.messages.length} messages (${data.userMessages.length} user, ${data.assistantMessages.length} assistant)`, 'dim'));
188
+ const replayProvider = new replay_1.ReplayProvider(data.assistantMessages);
189
+ const config = await resolveConfig(args);
190
+ const agent = new agent_1.Agent({
191
+ provider: replayProvider,
192
+ model: config.model,
193
+ providerName: 'replay',
194
+ autoApprove: true,
195
+ });
196
+ // Collect recorded tool results in order for sequential comparison
197
+ const recordedResults = Array.from(data.toolResults.values());
198
+ let resultIndex = 0;
199
+ let divergences = 0;
200
+ for (const userMsg of data.userMessages) {
201
+ console.log(c(`\n> ${truncate(userMsg.content, 100)}`, 'cyan'));
202
+ for await (const event of agent.run(userMsg.content)) {
203
+ if (event.type === 'tool_result' && event.toolResult && !event.toolResult.is_error) {
204
+ const recorded = recordedResults[resultIndex++];
205
+ if (recorded !== undefined) {
206
+ const diff = (0, replay_1.compareOutputs)(recorded, event.toolResult.result);
207
+ if (diff) {
208
+ divergences++;
209
+ console.log(c(` ⚠ Divergence in ${event.toolResult.name || 'tool'}:`, 'yellow'));
210
+ console.log(c(` ${diff.split('\n').join('\n ')}`, 'dim'));
211
+ }
212
+ else {
213
+ console.log(c(` ✓ ${event.toolResult.name || 'tool'} — output matches`, 'green'));
214
+ }
215
+ }
216
+ }
217
+ else if (event.type === 'text') {
218
+ process.stdout.write(c('.', 'dim'));
219
+ }
220
+ }
221
+ }
222
+ console.log(c(`\n\nReplay complete.`, 'bold'));
223
+ if (divergences === 0) {
224
+ console.log(c(' All tool outputs match — session is reproducible.', 'green'));
225
+ }
226
+ else {
227
+ console.log(c(` ${divergences} divergence(s) detected — environment may have changed.`, 'yellow'));
228
+ }
229
+ return;
230
+ }
171
231
  // First run: auto-launch setup if nothing is configured
172
232
  if ((0, setup_1.isFirstRun)() && process.stdin.isTTY && !args.message) {
173
233
  console.log(c('Welcome! No configuration found — launching setup...', 'cyan'));
@@ -177,6 +237,11 @@ async function main() {
177
237
  }
178
238
  const config = await resolveConfig(args);
179
239
  const provider = createProvider(config);
240
+ // Deterministic mode: set temperature=0
241
+ if (args.deterministic) {
242
+ provider.temperature = 0;
243
+ console.log(c(' Deterministic mode: temperature=0', 'dim'));
244
+ }
180
245
  // Session management
181
246
  let resumeId;
182
247
  if (args.continue) {
@@ -637,6 +702,21 @@ function parseArgs(argv) {
637
702
  }
638
703
  continue;
639
704
  }
705
+ if (arg === '--replay') {
706
+ const next = argv[i + 1];
707
+ if (next && !next.startsWith('--')) {
708
+ result['replay'] = next;
709
+ i++;
710
+ }
711
+ else {
712
+ result['replay'] = true; // replay latest
713
+ }
714
+ continue;
715
+ }
716
+ if (arg === '--deterministic') {
717
+ result['deterministic'] = true;
718
+ continue;
719
+ }
640
720
  if (arg.startsWith('--')) {
641
721
  const key = arg.slice(2);
642
722
  const next = argv[i + 1];
@@ -692,6 +772,10 @@ ${c('Security & Policy:', 'bold')}
692
772
  --verify-audit [id] Verify audit log hash chain integrity
693
773
  --sandbox-info Show Docker sandbox status
694
774
 
775
+ ${c('Debugging & Replay:', 'bold')}
776
+ --replay [id] Replay a session, re-execute tools, compare outputs
777
+ --deterministic Set temperature=0 for reproducible outputs
778
+
695
779
  ${c('Supported Providers:', 'bold')}
696
780
  Local: Ollama, LM Studio, vLLM (auto-detected)
697
781
  Anthropic: Claude Opus/Sonnet/Haiku (ANTHROPIC_API_KEY)
package/dist/history.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Message } from './types';
2
+ import { IntegrityResult } from './integrity';
2
3
  export interface SessionMeta {
3
4
  id: string;
4
5
  model: string;
@@ -11,14 +12,17 @@ export declare class SessionManager {
11
12
  private sessionId;
12
13
  private filePath;
13
14
  private model;
15
+ private integrityKey;
14
16
  constructor(model: string, sessionId?: string);
15
17
  getId(): string;
16
- /** Append a message to the session file */
18
+ /** Append a message to the session file (HMAC signed) */
17
19
  save(message: Message): void;
18
- /** Save all messages (atomic overwrite via temp file + rename) */
20
+ /** Save all messages (atomic overwrite, HMAC signed) */
19
21
  saveAll(messages: Message[]): void;
20
- /** Load messages from a session file (skips malformed lines) */
22
+ /** Load messages from a session file (verifies HMAC, drops tampered) */
21
23
  load(): Message[];
24
+ /** Verify integrity of all messages in this session. */
25
+ verifyIntegrity(): IntegrityResult;
22
26
  /** List recent sessions */
23
27
  static list(limit?: number): SessionMeta[];
24
28
  /** Get the most recent session ID */
package/dist/history.js CHANGED
@@ -38,38 +38,50 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const os = __importStar(require("os"));
40
40
  const crypto = __importStar(require("crypto"));
41
+ const integrity_1 = require("./integrity");
41
42
  const SESSIONS_DIR = path.join(os.homedir(), '.codebot', 'sessions');
42
43
  class SessionManager {
43
44
  sessionId;
44
45
  filePath;
45
46
  model;
47
+ integrityKey;
46
48
  constructor(model, sessionId) {
47
49
  this.model = model;
48
50
  this.sessionId = sessionId || crypto.randomUUID();
51
+ this.integrityKey = (0, integrity_1.deriveSessionKey)(this.sessionId);
49
52
  fs.mkdirSync(SESSIONS_DIR, { recursive: true });
50
53
  this.filePath = path.join(SESSIONS_DIR, `${this.sessionId}.jsonl`);
51
54
  }
52
55
  getId() {
53
56
  return this.sessionId;
54
57
  }
55
- /** Append a message to the session file */
58
+ /** Append a message to the session file (HMAC signed) */
56
59
  save(message) {
57
60
  try {
58
- const line = JSON.stringify({
61
+ const record = {
59
62
  ...message,
60
63
  _ts: new Date().toISOString(),
61
64
  _model: this.model,
62
- });
63
- fs.appendFileSync(this.filePath, line + '\n');
65
+ };
66
+ record._sig = (0, integrity_1.signMessage)(record, this.integrityKey);
67
+ fs.appendFileSync(this.filePath, JSON.stringify(record) + '\n');
64
68
  }
65
69
  catch {
66
70
  // Don't crash on write failure — session persistence is best-effort
67
71
  }
68
72
  }
69
- /** Save all messages (atomic overwrite via temp file + rename) */
73
+ /** Save all messages (atomic overwrite, HMAC signed) */
70
74
  saveAll(messages) {
71
75
  try {
72
- const lines = messages.map(m => JSON.stringify({ ...m, _ts: new Date().toISOString(), _model: this.model }));
76
+ const lines = messages.map(m => {
77
+ const record = {
78
+ ...m,
79
+ _ts: new Date().toISOString(),
80
+ _model: this.model,
81
+ };
82
+ record._sig = (0, integrity_1.signMessage)(record, this.integrityKey);
83
+ return JSON.stringify(record);
84
+ });
73
85
  const tmpPath = this.filePath + '.tmp';
74
86
  fs.writeFileSync(tmpPath, lines.join('\n') + '\n');
75
87
  fs.renameSync(tmpPath, this.filePath);
@@ -78,7 +90,7 @@ class SessionManager {
78
90
  // Don't crash — session persistence is best-effort
79
91
  }
80
92
  }
81
- /** Load messages from a session file (skips malformed lines) */
93
+ /** Load messages from a session file (verifies HMAC, drops tampered) */
82
94
  load() {
83
95
  if (!fs.existsSync(this.filePath))
84
96
  return [];
@@ -92,20 +104,55 @@ class SessionManager {
92
104
  if (!content)
93
105
  return [];
94
106
  const messages = [];
107
+ let dropped = 0;
95
108
  for (const line of content.split('\n')) {
96
109
  try {
97
110
  const obj = JSON.parse(line);
111
+ // Verify integrity if signature present (backward compat: unsigned messages pass)
112
+ if (obj._sig) {
113
+ if (!(0, integrity_1.verifyMessage)(obj, this.integrityKey)) {
114
+ dropped++;
115
+ continue; // Drop tampered message
116
+ }
117
+ }
98
118
  delete obj._ts;
99
119
  delete obj._model;
120
+ delete obj._sig;
100
121
  messages.push(obj);
101
122
  }
102
123
  catch {
103
- // Skip malformed line — don't crash the whole load
104
124
  continue;
105
125
  }
106
126
  }
127
+ if (dropped > 0) {
128
+ console.warn(`Warning: Dropped ${dropped} tampered message(s) from session ${this.sessionId.substring(0, 8)}.`);
129
+ }
107
130
  return messages;
108
131
  }
132
+ /** Verify integrity of all messages in this session. */
133
+ verifyIntegrity() {
134
+ if (!fs.existsSync(this.filePath)) {
135
+ return { valid: 0, tampered: 0, unsigned: 0, tamperedIndices: [] };
136
+ }
137
+ try {
138
+ const content = fs.readFileSync(this.filePath, 'utf-8').trim();
139
+ if (!content)
140
+ return { valid: 0, tampered: 0, unsigned: 0, tamperedIndices: [] };
141
+ const records = [];
142
+ for (const line of content.split('\n')) {
143
+ try {
144
+ records.push(JSON.parse(line));
145
+ }
146
+ catch {
147
+ continue;
148
+ }
149
+ }
150
+ return (0, integrity_1.verifyMessages)(records, this.integrityKey);
151
+ }
152
+ catch {
153
+ return { valid: 0, tampered: 0, unsigned: 0, tamperedIndices: [] };
154
+ }
155
+ }
109
156
  /** List recent sessions */
110
157
  static list(limit = 10) {
111
158
  if (!fs.existsSync(SESSIONS_DIR))
package/dist/index.d.ts CHANGED
@@ -11,5 +11,11 @@ export { loadPlugins } from './plugins';
11
11
  export { loadMCPTools } from './mcp';
12
12
  export { MODEL_REGISTRY, PROVIDER_DEFAULTS, getModelInfo, detectProvider } from './providers/registry';
13
13
  export type { ModelInfo } from './providers/registry';
14
+ export { CapabilityChecker } from './capabilities';
15
+ export type { ToolCapabilities, CapabilityConfig } from './capabilities';
16
+ export { deriveSessionKey, signMessage, verifyMessage, verifyMessages } from './integrity';
17
+ export type { IntegrityResult } from './integrity';
18
+ export { ReplayProvider, loadSessionForReplay, compareOutputs, listReplayableSessions } from './replay';
19
+ export type { SessionReplayData, ReplayDivergence } from './replay';
14
20
  export * from './types';
15
21
  //# sourceMappingURL=index.d.ts.map