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 +10 -0
- package/dist/agent.js +89 -3
- package/dist/audit.d.ts +1 -1
- package/dist/capabilities.d.ts +48 -0
- package/dist/capabilities.js +187 -0
- package/dist/cli.js +85 -1
- package/dist/history.d.ts +7 -3
- package/dist/history.js +55 -8
- package/dist/index.d.ts +6 -0
- package/dist/index.js +13 -1
- package/dist/integrity.d.ts +35 -0
- package/dist/integrity.js +135 -0
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +32 -6
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +4 -0
- package/dist/providers/openai.d.ts +1 -0
- package/dist/providers/openai.js +4 -0
- package/dist/replay.d.ts +55 -0
- package/dist/replay.js +196 -0
- package/dist/tools/batch-edit.d.ts +3 -0
- package/dist/tools/batch-edit.js +12 -0
- package/dist/tools/edit.d.ts +3 -0
- package/dist/tools/edit.js +11 -0
- package/dist/tools/git.d.ts +5 -0
- package/dist/tools/git.js +31 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +6 -6
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.js +11 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
20
|
+
/** Save all messages (atomic overwrite, HMAC signed) */
|
|
19
21
|
saveAll(messages: Message[]): void;
|
|
20
|
-
/** Load messages from a session file (
|
|
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
|
|
61
|
+
const record = {
|
|
59
62
|
...message,
|
|
60
63
|
_ts: new Date().toISOString(),
|
|
61
64
|
_model: this.model,
|
|
62
|
-
}
|
|
63
|
-
|
|
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
|
|
73
|
+
/** Save all messages (atomic overwrite, HMAC signed) */
|
|
70
74
|
saveAll(messages) {
|
|
71
75
|
try {
|
|
72
|
-
const lines = messages.map(m =>
|
|
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 (
|
|
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
|