@bahulam/code 2.6.14 → 2.6.15
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/package.json +4 -4
- package/src/core/approval-log.mjs +23 -0
- package/src/core/approval.mjs +93 -17
- package/src/core/file-diff.mjs +1 -1
- package/src/core/risk-tier.mjs +53 -2
- package/src/core/safety.mjs +61 -4
- package/src/core/tool-executor.mjs +25 -13
- package/src/core/trust.mjs +5 -3
- package/src/index.mjs +1 -1
- package/src/terminal/repl-render.mjs +104 -7
- package/src/terminal/repl-state.mjs +1 -0
- package/src/terminal/repl.mjs +158 -67
- package/src/terminal/tool-display.mjs +20 -1
- package/src/ui/approval.mjs +11 -0
- package/src/ui/input-dock.mjs +127 -13
- package/src/ui/tool-card.mjs +156 -13
- package/src/ui/tool-details.mjs +96 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bahulam/code",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.15",
|
|
4
4
|
"description": "Bahulam Code \u2014 abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"web-tree-sitter": "^0.26.9"
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"@bahulam/runtime-darwin-arm64": "2.6.
|
|
53
|
-
"@bahulam/runtime-linux-x64": "2.6.
|
|
54
|
-
"@bahulam/runtime-win32-x64": "2.6.
|
|
52
|
+
"@bahulam/runtime-darwin-arm64": "2.6.15",
|
|
53
|
+
"@bahulam/runtime-linux-x64": "2.6.15",
|
|
54
|
+
"@bahulam/runtime-win32-x64": "2.6.15"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { isSensitiveConfigPath } from './safety.mjs';
|
|
3
4
|
|
|
4
5
|
const REDACTED = 'REDACTED';
|
|
5
6
|
const MAX_ARG_LOG_CHARS = 4000;
|
|
@@ -53,10 +54,32 @@ function sanitizeForLog(value, depth = 0) {
|
|
|
53
54
|
function safeArgs(args) {
|
|
54
55
|
if (args?.command) return redactSensitive(String(args.command)).slice(0, MAX_ARG_LOG_CHARS);
|
|
55
56
|
if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0, MAX_ARG_LOG_CHARS);
|
|
57
|
+
if (Array.isArray(args?.files)) {
|
|
58
|
+
try {
|
|
59
|
+
const { files, ...rest } = args;
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
...sanitizeForLog(rest),
|
|
62
|
+
files: files.slice(0, 100).map(file => sanitizeProjectFileArg(file)),
|
|
63
|
+
}).slice(0, MAX_ARG_LOG_CHARS);
|
|
64
|
+
} catch {
|
|
65
|
+
return '[files omitted]';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
56
68
|
try { return JSON.stringify(sanitizeForLog(args || {})).slice(0, MAX_ARG_LOG_CHARS); }
|
|
57
69
|
catch { return redactSensitive(String(args || '')).slice(0, MAX_ARG_LOG_CHARS); }
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
function sanitizeProjectFileArg(file) {
|
|
73
|
+
if (typeof file === 'string') return redactSensitive(file);
|
|
74
|
+
if (!file || typeof file !== 'object') return sanitizeForLog(file);
|
|
75
|
+
const out = sanitizeForLog(file);
|
|
76
|
+
const filePath = file.path || file.file_path || '';
|
|
77
|
+
if (isSensitiveConfigPath(filePath) && typeof out.content === 'string') {
|
|
78
|
+
out.content = '[redacted sensitive config]';
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
function safeText(value, max = 500) {
|
|
61
84
|
if (!value) return undefined;
|
|
62
85
|
return redactSensitive(String(value)).slice(0, max);
|
package/src/core/approval.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
defaultOptions as approvalOptions,
|
|
26
26
|
} from '../ui/approval.mjs';
|
|
27
27
|
import { clearInputPrompt, isInputDockMounted, moveToContent, renderDockOverlay } from '../ui/input-dock.mjs';
|
|
28
|
-
import { validateShellCommand } from './safety.mjs';
|
|
28
|
+
import { isApprovalRequiredWritePath, isSensitiveConfigPath, validateShellCommand } from './safety.mjs';
|
|
29
29
|
import { classifyCommand } from '../permissions/command-classifier.mjs';
|
|
30
30
|
import { ApprovalLog } from './approval-log.mjs';
|
|
31
31
|
import { TrustStore } from './trust.mjs';
|
|
@@ -46,9 +46,19 @@ const WRITE_TOOLS = new Set([
|
|
|
46
46
|
|
|
47
47
|
function defaultWhy(tier, tool, args) {
|
|
48
48
|
const subject = approvalSummary(tool, args);
|
|
49
|
+
const protectedTarget = protectedWriteTarget(tool, args);
|
|
50
|
+
if (protectedTarget) {
|
|
51
|
+
const target = protectedTarget.path;
|
|
52
|
+
if (protectedTarget.sensitive) {
|
|
53
|
+
return `Edits sensitive environment config (${target}). Confirm before writing; values stay redacted in summaries and diffs.`;
|
|
54
|
+
}
|
|
55
|
+
return `Edits protected project file (${subject || target}). Confirm before writing.`;
|
|
56
|
+
}
|
|
49
57
|
switch (tier) {
|
|
50
58
|
case TIERS.SENSITIVE_READ:
|
|
51
59
|
return `Reads a sensitive path (${subject || 'secret-like file'}). Confirm before exposing its contents to the agent.`;
|
|
60
|
+
case TIERS.PROTECTED_EDIT:
|
|
61
|
+
return `Edits a protected project file. Confirm before writing.`;
|
|
52
62
|
case TIERS.SHELL_DANGEROUS:
|
|
53
63
|
return `Shell command matches a high-risk pattern (rm -rf, sudo, force push, etc.). Confirm before running.`;
|
|
54
64
|
case TIERS.DESTRUCTIVE:
|
|
@@ -62,6 +72,26 @@ function defaultWhy(tier, tool, args) {
|
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
function protectedWriteTarget(tool, args = {}) {
|
|
76
|
+
const paths = writeTargetPaths(tool, args);
|
|
77
|
+
const path = paths.find(isApprovalRequiredWritePath);
|
|
78
|
+
if (!path) return null;
|
|
79
|
+
return { path, sensitive: isSensitiveConfigPath(path) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeTargetPaths(tool, args = {}) {
|
|
83
|
+
const key = String(tool || '').toLowerCase();
|
|
84
|
+
if (key === 'write_file' || key === 'edit_file') {
|
|
85
|
+
return [args.file_path || args.path].filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
if (key === 'write_project') {
|
|
88
|
+
return (args.files || [])
|
|
89
|
+
.map(file => typeof file === 'string' ? file : file?.path || file?.file_path)
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
65
95
|
function approvalSummary(tool, args = {}) {
|
|
66
96
|
const summary = toolDisplaySummary(tool, args);
|
|
67
97
|
if (tool !== 'shell') return summary;
|
|
@@ -118,6 +148,8 @@ export class ApprovalManager {
|
|
|
118
148
|
this.history = [];
|
|
119
149
|
this.rejectionHints = [];
|
|
120
150
|
this._rl = null;
|
|
151
|
+
this._approvalPromptActive = false;
|
|
152
|
+
this._approvalPromptWasRaw = false;
|
|
121
153
|
}
|
|
122
154
|
|
|
123
155
|
setReadline(rl) { this._rl = rl; }
|
|
@@ -128,6 +160,29 @@ export class ApprovalManager {
|
|
|
128
160
|
this._approvalPromptEnd = onApprovalPromptEnd || null;
|
|
129
161
|
}
|
|
130
162
|
|
|
163
|
+
_beginApprovalInput() {
|
|
164
|
+
if (this._approvalPromptActive || !process.stdin.isTTY) return false;
|
|
165
|
+
this._approvalPromptActive = true;
|
|
166
|
+
this._approvalPromptWasRaw = process.stdin.isRaw;
|
|
167
|
+
if (this._execPause) this._execPause();
|
|
168
|
+
if (this._rl) this._rl.pause();
|
|
169
|
+
if (typeof process.stdin.setRawMode === 'function') {
|
|
170
|
+
process.stdin.setRawMode(true);
|
|
171
|
+
}
|
|
172
|
+
process.stdin.resume();
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_endApprovalInput() {
|
|
177
|
+
if (!this._approvalPromptActive) return;
|
|
178
|
+
if (typeof process.stdin.setRawMode === 'function') {
|
|
179
|
+
process.stdin.setRawMode(this._approvalPromptWasRaw || false);
|
|
180
|
+
}
|
|
181
|
+
if (this._rl) this._rl.resume();
|
|
182
|
+
if (this._execResume) this._execResume();
|
|
183
|
+
this._approvalPromptActive = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
131
186
|
revoke() {
|
|
132
187
|
const wasActive = this.approveAll || this.approvedToolTypes.size > 0;
|
|
133
188
|
this.approveAll = false;
|
|
@@ -275,6 +330,9 @@ export class ApprovalManager {
|
|
|
275
330
|
printedHeight = block.split('\n').length;
|
|
276
331
|
};
|
|
277
332
|
|
|
333
|
+
const ownsApprovalInput = isInteractive ? this._beginApprovalInput() : false;
|
|
334
|
+
|
|
335
|
+
try {
|
|
278
336
|
if (isInteractive) drawPrompt();
|
|
279
337
|
|
|
280
338
|
// ── Input loop ─────────────────────────────────────────────────
|
|
@@ -334,7 +392,7 @@ export class ApprovalManager {
|
|
|
334
392
|
return { approved: true, tier };
|
|
335
393
|
|
|
336
394
|
case 'allow-session': {
|
|
337
|
-
if (!this.policy.hitl?.allowSessionTrust) return this._prompt(toolName, args, context);
|
|
395
|
+
if (!this.policy.hitl?.allowSessionTrust) return await this._prompt(toolName, args, context);
|
|
338
396
|
const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'SESSION' });
|
|
339
397
|
write(` ${GREEN}✓${RST} ${DIM}trusted for this session: ${rule.pattern}${RST}\n\n`);
|
|
340
398
|
this.history.push({ tool: toolName, decision: 'session-trust', tier, time: Date.now(), rule_id: rule.id });
|
|
@@ -343,7 +401,7 @@ export class ApprovalManager {
|
|
|
343
401
|
}
|
|
344
402
|
|
|
345
403
|
case 'allow-project': {
|
|
346
|
-
if (!this.policy.hitl?.allowProjectTrust) return this._prompt(toolName, args, context);
|
|
404
|
+
if (!this.policy.hitl?.allowProjectTrust) return await this._prompt(toolName, args, context);
|
|
347
405
|
const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'PROJECT' });
|
|
348
406
|
write(` ${GREEN}✓${RST} ${DIM}trusted for this project: ${rule.pattern}${RST}\n\n`);
|
|
349
407
|
this.history.push({ tool: toolName, decision: 'project-trust', tier, time: Date.now(), rule_id: rule.id });
|
|
@@ -362,7 +420,7 @@ export class ApprovalManager {
|
|
|
362
420
|
}
|
|
363
421
|
|
|
364
422
|
case 'allow-all':
|
|
365
|
-
if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
|
|
423
|
+
if (requiresExplicitApproval(tier)) return await this._prompt(toolName, args, context);
|
|
366
424
|
this.approveAll = true;
|
|
367
425
|
write(` ${GREEN}✓✓${RST} ${DIM}allow-all activated${RST}\n\n`);
|
|
368
426
|
this.history.push({ tool: toolName, decision: 'approve-all', tier, time: Date.now() });
|
|
@@ -370,7 +428,7 @@ export class ApprovalManager {
|
|
|
370
428
|
return { approved: true, tier };
|
|
371
429
|
|
|
372
430
|
case 'allow-type':
|
|
373
|
-
if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
|
|
431
|
+
if (requiresExplicitApproval(tier)) return await this._prompt(toolName, args, context);
|
|
374
432
|
this.approvedToolTypes.add(toolName);
|
|
375
433
|
this.history.push({ tool: toolName, decision: 'type-approve', tier, time: Date.now() });
|
|
376
434
|
this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session', prompt });
|
|
@@ -380,7 +438,7 @@ export class ApprovalManager {
|
|
|
380
438
|
case 'why':
|
|
381
439
|
write(`\n ${DIM}${(context.reason || why).slice(0, 400)}${RST}\n\n`);
|
|
382
440
|
printedHeight = 0;
|
|
383
|
-
return this._prompt(toolName, args, context);
|
|
441
|
+
return await this._prompt(toolName, args, context);
|
|
384
442
|
|
|
385
443
|
case 'edit':
|
|
386
444
|
case 'replan':
|
|
@@ -395,7 +453,10 @@ export class ApprovalManager {
|
|
|
395
453
|
}
|
|
396
454
|
|
|
397
455
|
default:
|
|
398
|
-
return this._prompt(toolName, args, context);
|
|
456
|
+
return await this._prompt(toolName, args, context);
|
|
457
|
+
}
|
|
458
|
+
} finally {
|
|
459
|
+
if (ownsApprovalInput) this._endApprovalInput();
|
|
399
460
|
}
|
|
400
461
|
}
|
|
401
462
|
|
|
@@ -406,16 +467,25 @@ export class ApprovalManager {
|
|
|
406
467
|
return;
|
|
407
468
|
}
|
|
408
469
|
|
|
409
|
-
|
|
410
|
-
if (
|
|
470
|
+
const managesInput = !this._approvalPromptActive;
|
|
471
|
+
if (managesInput) {
|
|
472
|
+
if (this._execPause) this._execPause();
|
|
473
|
+
if (this._rl) this._rl.pause();
|
|
474
|
+
}
|
|
411
475
|
|
|
412
476
|
const wasRaw = process.stdin.isRaw;
|
|
413
|
-
process.stdin.setRawMode
|
|
477
|
+
if (managesInput && typeof process.stdin.setRawMode === 'function') {
|
|
478
|
+
process.stdin.setRawMode(true);
|
|
479
|
+
}
|
|
414
480
|
process.stdin.resume();
|
|
415
481
|
process.stdin.once('data', (data) => {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
482
|
+
if (managesInput) {
|
|
483
|
+
if (typeof process.stdin.setRawMode === 'function') {
|
|
484
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
485
|
+
}
|
|
486
|
+
if (this._rl) this._rl.resume();
|
|
487
|
+
if (this._execResume) this._execResume();
|
|
488
|
+
}
|
|
419
489
|
|
|
420
490
|
const bytes = [...data];
|
|
421
491
|
const str = data.toString();
|
|
@@ -444,8 +514,11 @@ export class ApprovalManager {
|
|
|
444
514
|
return;
|
|
445
515
|
}
|
|
446
516
|
|
|
447
|
-
|
|
448
|
-
if (
|
|
517
|
+
const managesInput = !this._approvalPromptActive;
|
|
518
|
+
if (managesInput) {
|
|
519
|
+
if (this._execPause) this._execPause();
|
|
520
|
+
if (this._rl) this._rl.pause();
|
|
521
|
+
}
|
|
449
522
|
|
|
450
523
|
const wasRaw = process.stdin.isRaw;
|
|
451
524
|
if (typeof process.stdin.setRawMode === 'function') {
|
|
@@ -460,8 +533,10 @@ export class ApprovalManager {
|
|
|
460
533
|
if (typeof process.stdin.setRawMode === 'function') {
|
|
461
534
|
process.stdin.setRawMode(wasRaw || false);
|
|
462
535
|
}
|
|
463
|
-
if (
|
|
464
|
-
|
|
536
|
+
if (managesInput) {
|
|
537
|
+
if (this._rl) this._rl.resume();
|
|
538
|
+
if (this._execResume) this._execResume();
|
|
539
|
+
}
|
|
465
540
|
};
|
|
466
541
|
const finish = () => {
|
|
467
542
|
cleanup();
|
|
@@ -535,6 +610,7 @@ function promptForLog(tool, args, tier, why) {
|
|
|
535
610
|
function approvalTitleForLog(tier) {
|
|
536
611
|
switch (tier) {
|
|
537
612
|
case TIERS.SENSITIVE_READ:
|
|
613
|
+
case TIERS.PROTECTED_EDIT:
|
|
538
614
|
case TIERS.SHELL_DANGEROUS:
|
|
539
615
|
case TIERS.DESTRUCTIVE:
|
|
540
616
|
return 'EXPLICIT APPROVAL';
|
package/src/core/file-diff.mjs
CHANGED
package/src/core/risk-tier.mjs
CHANGED
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
* intent can't be hidden behind a friendly description.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { isApprovalRequiredWritePath, isSensitiveConfigPath } from './safety.mjs';
|
|
18
|
+
|
|
17
19
|
// ── Tier enum ────────────────────────────────────────────────────────────
|
|
18
20
|
|
|
19
21
|
export const TIERS = Object.freeze({
|
|
20
22
|
READ: 'read',
|
|
21
23
|
SENSITIVE_READ: 'sensitive-read',
|
|
22
24
|
LOCAL_EDIT: 'local-edit',
|
|
25
|
+
PROTECTED_EDIT: 'protected-edit',
|
|
23
26
|
SHELL_SAFE: 'shell-safe',
|
|
24
27
|
SHELL_MEDIUM: 'shell-medium',
|
|
25
28
|
SHELL_DANGEROUS: 'shell-dangerous',
|
|
@@ -38,6 +41,7 @@ export const BEHAVIOR = Object.freeze({
|
|
|
38
41
|
[TIERS.READ]: 'auto',
|
|
39
42
|
[TIERS.SENSITIVE_READ]: 'prompt-explicit',
|
|
40
43
|
[TIERS.LOCAL_EDIT]: 'auto-with-undo',
|
|
44
|
+
[TIERS.PROTECTED_EDIT]: 'prompt-explicit',
|
|
41
45
|
[TIERS.SHELL_SAFE]: 'auto',
|
|
42
46
|
[TIERS.SHELL_MEDIUM]: 'prompt-safe',
|
|
43
47
|
[TIERS.SHELL_DANGEROUS]: 'prompt-explicit',
|
|
@@ -77,6 +81,14 @@ const LOCAL_EDIT_TOOLS = new Set([
|
|
|
77
81
|
'workflow_create_multi', 'workflow_sync_multi',
|
|
78
82
|
]);
|
|
79
83
|
|
|
84
|
+
const WRITE_PATH_KEYS = [
|
|
85
|
+
'path', 'file_path', 'file', 'target',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const WRITE_PATH_ARRAY_KEYS = [
|
|
89
|
+
'paths', 'file_paths', 'targets',
|
|
90
|
+
];
|
|
91
|
+
|
|
80
92
|
const DESTRUCTIVE_TOOLS = new Set([
|
|
81
93
|
'delete_file', 'skill_remove',
|
|
82
94
|
]);
|
|
@@ -219,6 +231,7 @@ const TIER_ORDER = [
|
|
|
219
231
|
TIERS.SENSITIVE_READ,
|
|
220
232
|
TIERS.SHELL_SAFE,
|
|
221
233
|
TIERS.LOCAL_EDIT,
|
|
234
|
+
TIERS.PROTECTED_EDIT,
|
|
222
235
|
TIERS.NETWORK,
|
|
223
236
|
TIERS.SHELL_MEDIUM,
|
|
224
237
|
TIERS.DESTRUCTIVE,
|
|
@@ -243,7 +256,9 @@ export function classify(tool, args = {}) {
|
|
|
243
256
|
if (READ_TOOLS.has(tool)) {
|
|
244
257
|
return isSensitiveRead(args) ? TIERS.SENSITIVE_READ : TIERS.READ;
|
|
245
258
|
}
|
|
246
|
-
if (LOCAL_EDIT_TOOLS.has(tool))
|
|
259
|
+
if (LOCAL_EDIT_TOOLS.has(tool)) {
|
|
260
|
+
return isApprovalRequiredWrite(args) ? TIERS.PROTECTED_EDIT : TIERS.LOCAL_EDIT;
|
|
261
|
+
}
|
|
247
262
|
if (DESTRUCTIVE_TOOLS.has(tool)) return TIERS.DESTRUCTIVE;
|
|
248
263
|
if (NETWORK_TOOLS.has(tool)) return TIERS.NETWORK;
|
|
249
264
|
|
|
@@ -273,6 +288,7 @@ export function label(tier) {
|
|
|
273
288
|
case TIERS.READ: return 'READ';
|
|
274
289
|
case TIERS.SENSITIVE_READ: return 'SENSITIVE-READ';
|
|
275
290
|
case TIERS.LOCAL_EDIT: return 'LOCAL-EDIT';
|
|
291
|
+
case TIERS.PROTECTED_EDIT: return 'PROTECTED-EDIT';
|
|
276
292
|
case TIERS.SHELL_SAFE: return 'SHELL-SAFE';
|
|
277
293
|
case TIERS.SHELL_MEDIUM: return 'SHELL-MEDIUM';
|
|
278
294
|
case TIERS.SHELL_DANGEROUS: return 'SHELL-DANGEROUS';
|
|
@@ -310,11 +326,15 @@ export function isSensitiveReadPath(filePath = '') {
|
|
|
310
326
|
|
|
311
327
|
const parts = normalized.split('/').filter(Boolean);
|
|
312
328
|
const base = parts[parts.length - 1] || normalized;
|
|
313
|
-
if (base
|
|
329
|
+
if (isSensitiveConfigPath(base)) return true;
|
|
314
330
|
if (/\.pem$/i.test(base)) return true;
|
|
315
331
|
return parts.includes('secrets');
|
|
316
332
|
}
|
|
317
333
|
|
|
334
|
+
export function isApprovalRequiredWrite(args = {}) {
|
|
335
|
+
return extractWritePaths(args).some(isApprovalRequiredWritePath);
|
|
336
|
+
}
|
|
337
|
+
|
|
318
338
|
function extractReadPaths(args = {}) {
|
|
319
339
|
const paths = [];
|
|
320
340
|
for (const key of READ_PATH_KEYS) {
|
|
@@ -335,3 +355,34 @@ function extractReadPaths(args = {}) {
|
|
|
335
355
|
}
|
|
336
356
|
return paths;
|
|
337
357
|
}
|
|
358
|
+
|
|
359
|
+
function extractWritePaths(args = {}) {
|
|
360
|
+
const paths = [];
|
|
361
|
+
for (const key of WRITE_PATH_KEYS) {
|
|
362
|
+
if (typeof args[key] === 'string') paths.push(args[key]);
|
|
363
|
+
}
|
|
364
|
+
for (const key of WRITE_PATH_ARRAY_KEYS) {
|
|
365
|
+
const value = args[key];
|
|
366
|
+
if (!Array.isArray(value)) continue;
|
|
367
|
+
for (const item of value) {
|
|
368
|
+
if (typeof item === 'string') {
|
|
369
|
+
paths.push(item);
|
|
370
|
+
} else if (item && typeof item === 'object') {
|
|
371
|
+
for (const nestedKey of WRITE_PATH_KEYS) {
|
|
372
|
+
if (typeof item[nestedKey] === 'string') paths.push(item[nestedKey]);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (Array.isArray(args.files)) {
|
|
378
|
+
for (const item of args.files) {
|
|
379
|
+
if (typeof item === 'string') paths.push(item);
|
|
380
|
+
else if (item && typeof item === 'object') {
|
|
381
|
+
for (const nestedKey of WRITE_PATH_KEYS) {
|
|
382
|
+
if (typeof item[nestedKey] === 'string') paths.push(item[nestedKey]);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return paths;
|
|
388
|
+
}
|
package/src/core/safety.mjs
CHANGED
|
@@ -28,6 +28,18 @@ const PROTECTED_NAMES = new Set([
|
|
|
28
28
|
'go.sum',
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
|
+
const APPROVAL_REQUIRED_WRITE_NAMES = new Set([
|
|
32
|
+
'.env',
|
|
33
|
+
'.env.local',
|
|
34
|
+
'.env.production',
|
|
35
|
+
'package.json',
|
|
36
|
+
'package-lock.json',
|
|
37
|
+
'yarn.lock',
|
|
38
|
+
'pnpm-lock.yaml',
|
|
39
|
+
'Cargo.lock',
|
|
40
|
+
'go.sum',
|
|
41
|
+
]);
|
|
42
|
+
|
|
31
43
|
/** Directory names that indicate a source root — never delete the dir itself. */
|
|
32
44
|
const SOURCE_DIRS = new Set([
|
|
33
45
|
'src',
|
|
@@ -80,13 +92,35 @@ const HIGH_RISK_COMMANDS = [
|
|
|
80
92
|
|
|
81
93
|
// ── Validation Functions ─────────────────────────────────────
|
|
82
94
|
|
|
95
|
+
function basenameFor(filePath = '') {
|
|
96
|
+
return path.basename(String(filePath || '').replace(/\\/g, '/'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isEnvLikeName(name = '') {
|
|
100
|
+
return /^\.env(?:\.|$)/.test(String(name || ''));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isProtectedName(name = '') {
|
|
104
|
+
return PROTECTED_NAMES.has(name) || isEnvLikeName(name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isSensitiveConfigPath(filePath = '') {
|
|
108
|
+
return isEnvLikeName(basenameFor(filePath));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isApprovalRequiredWritePath(filePath = '') {
|
|
112
|
+
const basename = basenameFor(filePath);
|
|
113
|
+
return isSensitiveConfigPath(filePath) || APPROVAL_REQUIRED_WRITE_NAMES.has(basename);
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
/**
|
|
84
117
|
* Check if a path is protected (should not be deleted/overwritten entirely).
|
|
85
118
|
* @param {string} filePath - Absolute path
|
|
86
119
|
* @param {string} cwd - Current working directory
|
|
87
|
-
* @
|
|
120
|
+
* @param {{ allowApprovalRequiredWrite?: boolean }} options
|
|
121
|
+
* @returns {{ safe: boolean, reason?: string, requiresApproval?: boolean, sensitive?: boolean, protected?: boolean }}
|
|
88
122
|
*/
|
|
89
|
-
export function validatePath(filePath, cwd = process.cwd()) {
|
|
123
|
+
export function validatePath(filePath, cwd = process.cwd(), options = {}) {
|
|
90
124
|
if (!filePath) return { safe: false, reason: 'Empty path' };
|
|
91
125
|
|
|
92
126
|
const resolved = path.resolve(cwd, filePath);
|
|
@@ -100,7 +134,16 @@ export function validatePath(filePath, cwd = process.cwd()) {
|
|
|
100
134
|
}
|
|
101
135
|
|
|
102
136
|
// Block protected file names at any level
|
|
103
|
-
if (
|
|
137
|
+
if (isProtectedName(basename) && !resolved.includes('node_modules/')) {
|
|
138
|
+
if (options.allowApprovalRequiredWrite && isApprovalRequiredWritePath(resolved)) {
|
|
139
|
+
return {
|
|
140
|
+
safe: true,
|
|
141
|
+
reason: `Approval required for protected path: ${basename}`,
|
|
142
|
+
requiresApproval: true,
|
|
143
|
+
protected: true,
|
|
144
|
+
sensitive: isSensitiveConfigPath(resolved),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
104
147
|
return { safe: false, reason: `Protected path: ${basename}` };
|
|
105
148
|
}
|
|
106
149
|
|
|
@@ -172,16 +215,29 @@ export function validateShellCommand(command) {
|
|
|
172
215
|
* @returns {{ safe: boolean, reason?: string }}
|
|
173
216
|
*/
|
|
174
217
|
export function validateWrite(filePath, content, cwd = process.cwd()) {
|
|
175
|
-
const pathCheck = validatePath(filePath, cwd);
|
|
218
|
+
const pathCheck = validatePath(filePath, cwd, { allowApprovalRequiredWrite: true });
|
|
176
219
|
if (!pathCheck.safe) return pathCheck;
|
|
177
220
|
|
|
178
221
|
const resolved = path.resolve(cwd, filePath);
|
|
222
|
+
const basename = path.basename(resolved);
|
|
179
223
|
|
|
180
224
|
// Don't allow overwriting .git internals
|
|
181
225
|
if (resolved.includes('/.git/')) {
|
|
182
226
|
return { safe: false, reason: 'Cannot write to .git directory' };
|
|
183
227
|
}
|
|
184
228
|
|
|
229
|
+
if (pathCheck.requiresApproval) {
|
|
230
|
+
return {
|
|
231
|
+
safe: true,
|
|
232
|
+
reason: pathCheck.sensitive
|
|
233
|
+
? `Sensitive config write requires approval: ${basename}`
|
|
234
|
+
: `Protected file write requires approval: ${basename}`,
|
|
235
|
+
requiresApproval: true,
|
|
236
|
+
protected: true,
|
|
237
|
+
sensitive: pathCheck.sensitive,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
185
241
|
// Warn if writing a very large file (> 1MB)
|
|
186
242
|
if (content && content.length > 1_000_000) {
|
|
187
243
|
return { safe: true, reason: `Large file write: ${(content.length / 1024).toFixed(0)}KB` };
|
|
@@ -196,6 +252,7 @@ export function validateWrite(filePath, content, cwd = process.cwd()) {
|
|
|
196
252
|
export function getSafetyRules() {
|
|
197
253
|
return {
|
|
198
254
|
protectedNames: [...PROTECTED_NAMES],
|
|
255
|
+
approvalRequiredWriteNames: [...APPROVAL_REQUIRED_WRITE_NAMES],
|
|
199
256
|
sourceDirs: [...SOURCE_DIRS],
|
|
200
257
|
blockedPatterns: DANGEROUS_SHELL_PATTERNS.length,
|
|
201
258
|
highRiskPatterns: HIGH_RISK_COMMANDS.length,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { createToolRegistry } from '../tools/registry.mjs';
|
|
12
12
|
import { detectCommandType, filterOutput } from './output-filter.mjs';
|
|
13
|
-
import { validatePath, validateDelete, validateShellCommand, validateWrite } from './safety.mjs';
|
|
13
|
+
import { isSensitiveConfigPath, validatePath, validateDelete, validateShellCommand, validateWrite } from './safety.mjs';
|
|
14
14
|
import { classifyCommand, isExitCodeError } from '../permissions/command-classifier.mjs';
|
|
15
15
|
import { analyzeCode } from '../context/ast-parser.mjs';
|
|
16
16
|
import { ProjectRegistry } from '../tools/project-overview.mjs';
|
|
@@ -366,18 +366,35 @@ export function createToolExecutor({
|
|
|
366
366
|
return result;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
function buildResultFileDiff(filePath, before, after) {
|
|
370
|
+
const diff = buildFileDiff({
|
|
371
|
+
filePath,
|
|
372
|
+
before,
|
|
373
|
+
after,
|
|
374
|
+
cwd: projectRootFor(filePath),
|
|
375
|
+
});
|
|
376
|
+
if (!isSensitiveConfigPath(filePath)) return diff;
|
|
377
|
+
return {
|
|
378
|
+
...diff,
|
|
379
|
+
hunks: [],
|
|
380
|
+
unified: '',
|
|
381
|
+
redacted: true,
|
|
382
|
+
sensitive: true,
|
|
383
|
+
redaction_reason: 'Sensitive config diff redacted',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
369
387
|
function attachFileDiff(result, filePath, before, after) {
|
|
370
388
|
try {
|
|
371
|
-
const diff =
|
|
372
|
-
filePath,
|
|
373
|
-
before,
|
|
374
|
-
after,
|
|
375
|
-
cwd: projectRootFor(filePath),
|
|
376
|
-
});
|
|
389
|
+
const diff = buildResultFileDiff(filePath, before, after);
|
|
377
390
|
result.file_diff = diff;
|
|
378
391
|
result.diff = diff.unified;
|
|
379
392
|
result.lines_added = diff.lines_added;
|
|
380
393
|
result.lines_removed = diff.lines_removed;
|
|
394
|
+
if (diff.redacted) {
|
|
395
|
+
result.output = `File updated: ${diff.relative_path || filePath}\nDiff redacted for sensitive config file.`;
|
|
396
|
+
result.redacted = true;
|
|
397
|
+
}
|
|
381
398
|
} catch { /* best effort */ }
|
|
382
399
|
return result;
|
|
383
400
|
}
|
|
@@ -821,12 +838,7 @@ export function createToolExecutor({
|
|
|
821
838
|
|
|
822
839
|
await occRegistry.call('Write', { file_path: filePath, content });
|
|
823
840
|
const after = readTextIfExists(filePath);
|
|
824
|
-
diffs.push(
|
|
825
|
-
filePath,
|
|
826
|
-
before,
|
|
827
|
-
after,
|
|
828
|
-
cwd: projectRootFor(filePath),
|
|
829
|
-
}));
|
|
841
|
+
diffs.push(buildResultFileDiff(filePath, before, after));
|
|
830
842
|
updateProjectIndex(filePath);
|
|
831
843
|
results.push(rawPath);
|
|
832
844
|
} catch (err) {
|
package/src/core/trust.mjs
CHANGED
|
@@ -96,11 +96,13 @@ export class TrustStore {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
if ((reask.on_risk_increase ?? this.policy.hitl?.reaskOnRiskIncrease) && rule.tier && tier) {
|
|
99
|
-
const order = [TIERS.READ, TIERS.SHELL_SAFE, TIERS.LOCAL_EDIT, TIERS.SHELL_MEDIUM, TIERS.NETWORK, TIERS.SHELL_DANGEROUS, TIERS.DESTRUCTIVE];
|
|
99
|
+
const order = [TIERS.READ, TIERS.SHELL_SAFE, TIERS.LOCAL_EDIT, TIERS.PROTECTED_EDIT, TIERS.SHELL_MEDIUM, TIERS.NETWORK, TIERS.SHELL_DANGEROUS, TIERS.DESTRUCTIVE];
|
|
100
100
|
if (order.indexOf(tier) > order.indexOf(rule.tier)) return 'risk tier increased';
|
|
101
101
|
}
|
|
102
|
-
if ((this.policy.hitl?.alwaysAskForDangerous ?? true) && (tier === TIERS.SHELL_DANGEROUS || tier === TIERS.DESTRUCTIVE)) {
|
|
103
|
-
return
|
|
102
|
+
if ((this.policy.hitl?.alwaysAskForDangerous ?? true) && (tier === TIERS.SHELL_DANGEROUS || tier === TIERS.DESTRUCTIVE || tier === TIERS.PROTECTED_EDIT)) {
|
|
103
|
+
return tier === TIERS.PROTECTED_EDIT
|
|
104
|
+
? 'protected edit requires fresh approval'
|
|
105
|
+
: 'dangerous tier requires fresh approval';
|
|
104
106
|
}
|
|
105
107
|
if ((reask.on_command_shape_change ?? this.policy.hitl?.reaskOnCommandShapeChange) && rule.command_shape && args.command) {
|
|
106
108
|
if (rule.command_shape !== commandShape(args.command)) return 'command shape changed';
|
package/src/index.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { printBanner, printProjectInfo, printHints, printAuthStatus, printStyled
|
|
|
35
35
|
import { ContextRetriever } from './context/retriever.mjs';
|
|
36
36
|
import { loadSettings } from './config/settings.mjs';
|
|
37
37
|
|
|
38
|
-
const VERSION = '2.6.
|
|
38
|
+
const VERSION = '2.6.15';
|
|
39
39
|
|
|
40
40
|
// ── Arg Parsing (consolidated from index.mjs + cli-args.mjs) ──
|
|
41
41
|
|