@bahulam/code 2.6.13 → 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/agents/scaffold.mjs +1 -0
- package/src/commands/agent.mjs +3 -2
- package/src/core/approval-log.mjs +45 -4
- package/src/core/approval.mjs +265 -41
- package/src/core/file-diff.mjs +1 -1
- package/src/core/headless.mjs +14 -3
- package/src/core/local-agent.mjs +3 -2
- package/src/core/risk-tier.mjs +53 -2
- package/src/core/safety.mjs +61 -4
- package/src/core/tool-executor.mjs +38 -16
- package/src/core/trust.mjs +5 -3
- package/src/index.mjs +1 -1
- package/src/terminal/agents.mjs +194 -18
- package/src/terminal/repl-render.mjs +126 -11
- package/src/terminal/repl-state.mjs +2 -0
- package/src/terminal/repl.mjs +553 -88
- package/src/terminal/tool-display.mjs +154 -2
- package/src/ui/approval.mjs +211 -14
- package/src/ui/icons.mjs +11 -5
- package/src/ui/input-dock.mjs +214 -29
- package/src/ui/slash-commands.mjs +10 -0
- package/src/ui/tool-card.mjs +261 -30
- package/src/ui/tool-details.mjs +206 -14
- package/src/ui/transcript-block.mjs +2 -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
|
}
|
package/src/agents/scaffold.mjs
CHANGED
package/src/commands/agent.mjs
CHANGED
|
@@ -59,7 +59,7 @@ function getAuth() {
|
|
|
59
59
|
const auth = new TarangAuth();
|
|
60
60
|
const creds = auth.loadCredentials();
|
|
61
61
|
if (!creds.token) {
|
|
62
|
-
process.stderr.write(`${RED}✗ Not authenticated. Run
|
|
62
|
+
process.stderr.write(`${RED}✗ Not authenticated. Run bahulam-code login first.${RESET}\n`);
|
|
63
63
|
process.exit(1);
|
|
64
64
|
}
|
|
65
65
|
return creds;
|
|
@@ -76,13 +76,14 @@ async function apiFetch(path, options = {}) {
|
|
|
76
76
|
const headers = {
|
|
77
77
|
'Authorization': `Bearer ${creds.token}`,
|
|
78
78
|
'Content-Type': 'application/json',
|
|
79
|
+
'X-Product': 'bahulam',
|
|
79
80
|
...options.headers,
|
|
80
81
|
};
|
|
81
82
|
const url = `${baseUrl}${path}`;
|
|
82
83
|
const response = await fetch(url, { ...options, headers });
|
|
83
84
|
|
|
84
85
|
if (response.status === 401) {
|
|
85
|
-
process.stderr.write(`${RED}✗ Authentication failed. Run
|
|
86
|
+
process.stderr.write(`${RED}✗ Authentication failed. Run bahulam-code login to re-authenticate.${RESET}\n`);
|
|
86
87
|
process.exit(1);
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -1,7 +1,11 @@
|
|
|
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';
|
|
6
|
+
const MAX_ARG_LOG_CHARS = 4000;
|
|
7
|
+
const MAX_PROMPT_SUBJECT_CHARS = 4000;
|
|
8
|
+
const MAX_PROMPT_REASON_CHARS = 1200;
|
|
5
9
|
const SENSITIVE_KEY_RE = /^(?:authorization|api[-_]?key|apikey|key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key)$|(?:^|[-_])(?:api[-_]?key|apikey|token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key)(?:$|[-_])/i;
|
|
6
10
|
const SENSITIVE_ASSIGNMENT_KEY = String.raw`[A-Z0-9_-]*(?:API[-_]?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|ACCESS[-_]?KEY|SECRET[-_]?ACCESS[-_]?KEY)[A-Z0-9_-]*`;
|
|
7
11
|
const SENSITIVE_JSON_KEY = String.raw`(?:authorization|api[-_]?key|apikey|key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key|[A-Z0-9_-]*(?:API[-_]?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|ACCESS[-_]?KEY|SECRET[-_]?ACCESS[-_]?KEY)[A-Z0-9_-]*)`;
|
|
@@ -48,10 +52,32 @@ function sanitizeForLog(value, depth = 0) {
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
function safeArgs(args) {
|
|
51
|
-
if (args?.command) return redactSensitive(String(args.command)).slice(0,
|
|
52
|
-
if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
if (args?.command) return redactSensitive(String(args.command)).slice(0, MAX_ARG_LOG_CHARS);
|
|
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
|
+
}
|
|
68
|
+
try { return JSON.stringify(sanitizeForLog(args || {})).slice(0, MAX_ARG_LOG_CHARS); }
|
|
69
|
+
catch { return redactSensitive(String(args || '')).slice(0, MAX_ARG_LOG_CHARS); }
|
|
70
|
+
}
|
|
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;
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
function safeText(value, max = 500) {
|
|
@@ -59,6 +85,19 @@ function safeText(value, max = 500) {
|
|
|
59
85
|
return redactSensitive(String(value)).slice(0, max);
|
|
60
86
|
}
|
|
61
87
|
|
|
88
|
+
function safePrompt(prompt) {
|
|
89
|
+
if (!prompt || typeof prompt !== 'object') return undefined;
|
|
90
|
+
const out = {
|
|
91
|
+
title: safeText(prompt.title, 200),
|
|
92
|
+
subject: safeText(prompt.subject, MAX_PROMPT_SUBJECT_CHARS),
|
|
93
|
+
reason: safeText(prompt.reason, MAX_PROMPT_REASON_CHARS),
|
|
94
|
+
};
|
|
95
|
+
for (const key of Object.keys(out)) {
|
|
96
|
+
if (out[key] === undefined) delete out[key];
|
|
97
|
+
}
|
|
98
|
+
return Object.keys(out).length ? out : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
62
101
|
export class ApprovalLog {
|
|
63
102
|
constructor({ cwd = process.cwd() } = {}) {
|
|
64
103
|
this.cwd = cwd;
|
|
@@ -68,6 +107,7 @@ export class ApprovalLog {
|
|
|
68
107
|
append(entry) {
|
|
69
108
|
try {
|
|
70
109
|
fs.mkdirSync(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
110
|
+
const prompt = safePrompt(entry.prompt);
|
|
71
111
|
const line = JSON.stringify({
|
|
72
112
|
ts: new Date().toISOString(),
|
|
73
113
|
tier: entry.tier,
|
|
@@ -77,6 +117,7 @@ export class ApprovalLog {
|
|
|
77
117
|
scope: entry.scope || 'once',
|
|
78
118
|
rule_id: entry.rule_id || null,
|
|
79
119
|
reason: safeText(entry.reason),
|
|
120
|
+
...(prompt ? { prompt } : {}),
|
|
80
121
|
});
|
|
81
122
|
fs.appendFileSync(this.filePath, line + '\n', { mode: 0o600 });
|
|
82
123
|
try { fs.chmodSync(this.filePath, 0o600); } catch {}
|
package/src/core/approval.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Shell commands are risk-assessed (safe/medium/high).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { shellCommandDisplay, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
13
|
+
import { shellCommandDisplay, shellCommandProfile, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
14
14
|
import {
|
|
15
15
|
classify as classifyTier,
|
|
16
16
|
TIERS,
|
|
@@ -20,11 +20,12 @@ import {
|
|
|
20
20
|
} from './risk-tier.mjs';
|
|
21
21
|
import {
|
|
22
22
|
renderApprovalPrompt,
|
|
23
|
+
renderApprovalDockPrompt,
|
|
23
24
|
renderTrustedApproval,
|
|
24
25
|
defaultOptions as approvalOptions,
|
|
25
26
|
} from '../ui/approval.mjs';
|
|
26
|
-
import { isInputDockMounted, moveToContent } from '../ui/input-dock.mjs';
|
|
27
|
-
import { validateShellCommand } from './safety.mjs';
|
|
27
|
+
import { clearInputPrompt, isInputDockMounted, moveToContent, renderDockOverlay } from '../ui/input-dock.mjs';
|
|
28
|
+
import { isApprovalRequiredWritePath, isSensitiveConfigPath, validateShellCommand } from './safety.mjs';
|
|
28
29
|
import { classifyCommand } from '../permissions/command-classifier.mjs';
|
|
29
30
|
import { ApprovalLog } from './approval-log.mjs';
|
|
30
31
|
import { TrustStore } from './trust.mjs';
|
|
@@ -45,9 +46,19 @@ const WRITE_TOOLS = new Set([
|
|
|
45
46
|
|
|
46
47
|
function defaultWhy(tier, tool, args) {
|
|
47
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
|
+
}
|
|
48
57
|
switch (tier) {
|
|
49
58
|
case TIERS.SENSITIVE_READ:
|
|
50
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.`;
|
|
51
62
|
case TIERS.SHELL_DANGEROUS:
|
|
52
63
|
return `Shell command matches a high-risk pattern (rm -rf, sudo, force push, etc.). Confirm before running.`;
|
|
53
64
|
case TIERS.DESTRUCTIVE:
|
|
@@ -61,6 +72,26 @@ function defaultWhy(tier, tool, args) {
|
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
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
|
+
|
|
64
95
|
function approvalSummary(tool, args = {}) {
|
|
65
96
|
const summary = toolDisplaySummary(tool, args);
|
|
66
97
|
if (tool !== 'shell') return summary;
|
|
@@ -72,12 +103,22 @@ function shellHardBlockReason(tool, args = {}) {
|
|
|
72
103
|
if (tool !== 'shell') return '';
|
|
73
104
|
const command = args.command || args.cmd || '';
|
|
74
105
|
const safety = validateShellCommand(command);
|
|
75
|
-
if (!safety.safe) return safety.reason || 'Blocked by shell safety policy';
|
|
106
|
+
if (!safety.safe) return withShellRetryHint(safety.reason || 'Blocked by shell safety policy');
|
|
76
107
|
const classification = classifyCommand(command);
|
|
77
|
-
if (classification.classification === 'blocked')
|
|
108
|
+
if (classification.classification === 'blocked') {
|
|
109
|
+
return withShellRetryHint(classification.reason || 'Blocked by shell safety policy');
|
|
110
|
+
}
|
|
78
111
|
return '';
|
|
79
112
|
}
|
|
80
113
|
|
|
114
|
+
function withShellRetryHint(reason) {
|
|
115
|
+
const text = String(reason || '').trim();
|
|
116
|
+
if (/command substitution|backticks|\$\(\)/i.test(text)) {
|
|
117
|
+
return `${text}. Retry with separate simple shell commands instead of backticks or $().`;
|
|
118
|
+
}
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
|
|
81
122
|
// ── ANSI helpers ──
|
|
82
123
|
|
|
83
124
|
const RST = '\x1b[0m';
|
|
@@ -107,13 +148,39 @@ export class ApprovalManager {
|
|
|
107
148
|
this.history = [];
|
|
108
149
|
this.rejectionHints = [];
|
|
109
150
|
this._rl = null;
|
|
151
|
+
this._approvalPromptActive = false;
|
|
152
|
+
this._approvalPromptWasRaw = false;
|
|
110
153
|
}
|
|
111
154
|
|
|
112
155
|
setReadline(rl) { this._rl = rl; }
|
|
113
156
|
|
|
114
|
-
setExecutionHooks({ onPause, onResume } = {}) {
|
|
157
|
+
setExecutionHooks({ onPause, onResume, onApprovalPromptEnd } = {}) {
|
|
115
158
|
this._execPause = onPause || null;
|
|
116
159
|
this._execResume = onResume || null;
|
|
160
|
+
this._approvalPromptEnd = onApprovalPromptEnd || null;
|
|
161
|
+
}
|
|
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;
|
|
117
184
|
}
|
|
118
185
|
|
|
119
186
|
revoke() {
|
|
@@ -143,7 +210,8 @@ export class ApprovalManager {
|
|
|
143
210
|
const reason = `Blocked by safety policy: ${hardBlock}`;
|
|
144
211
|
this.history.push({ tool: toolName, decision: 'safety-block', tier, time: Date.now(), reason });
|
|
145
212
|
this.approvalLog.append({ tool: toolName, args, tier, decision: 'safety_block', scope: 'none', reason });
|
|
146
|
-
|
|
213
|
+
if (isInputDockMounted()) moveToContent();
|
|
214
|
+
writeSafetyBlock(reason);
|
|
147
215
|
return { approved: false, tier, reason, blocked: true, code: 'safety_block' };
|
|
148
216
|
}
|
|
149
217
|
|
|
@@ -203,25 +271,48 @@ export class ApprovalManager {
|
|
|
203
271
|
async _prompt(toolName, args, context = {}) {
|
|
204
272
|
const tier = context.tier || classifyTier(toolName, args);
|
|
205
273
|
const why = context.reason || context.why || defaultWhy(tier, toolName, args);
|
|
206
|
-
const options = this._optionsFor(tier);
|
|
274
|
+
const options = this._optionsFor(tier, toolName, args);
|
|
207
275
|
|
|
208
276
|
let selected = 0; // arrow-driven cursor
|
|
209
277
|
let printedHeight = 0;
|
|
278
|
+
let showDetails = false;
|
|
279
|
+
|
|
280
|
+
const isInteractive = process.stdin.isTTY;
|
|
210
281
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
if (isInputDockMounted()) moveToContent();
|
|
282
|
+
// In rich TTY mode approval lives in the fixed input dock, replacing
|
|
283
|
+
// "+ add instruction" until the user decides. Fallback/plain mode
|
|
284
|
+
// still renders in the transcript.
|
|
285
|
+
const useDockPrompt = isInteractive && isInputDockMounted();
|
|
286
|
+
if (!useDockPrompt && isInputDockMounted()) moveToContent();
|
|
216
287
|
|
|
217
288
|
// For TTYs we redraw in place on every arrow key so the prompt feels
|
|
218
289
|
// live. For non-TTYs / pipes we just print once and read a line.
|
|
219
|
-
const isInteractive = process.stdin.isTTY;
|
|
220
290
|
if (!isInteractive) {
|
|
221
|
-
write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options }) + '\n');
|
|
291
|
+
write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options, showDetails }) + '\n');
|
|
222
292
|
}
|
|
223
293
|
|
|
224
294
|
const drawPrompt = () => {
|
|
295
|
+
if (useDockPrompt) {
|
|
296
|
+
const dock = renderApprovalDockPrompt({
|
|
297
|
+
tool: toolName,
|
|
298
|
+
args,
|
|
299
|
+
tier,
|
|
300
|
+
why,
|
|
301
|
+
selected,
|
|
302
|
+
options,
|
|
303
|
+
showDetails,
|
|
304
|
+
width: process.stderr.columns || process.stdout.columns || 96,
|
|
305
|
+
});
|
|
306
|
+
renderDockOverlay({
|
|
307
|
+
context: dock.context,
|
|
308
|
+
lines: dock.lines || [`${dock.prefix || ''}${dock.value || ''}`],
|
|
309
|
+
meta: dock.meta,
|
|
310
|
+
tips: dock.tips,
|
|
311
|
+
maxRows: dock.maxRows,
|
|
312
|
+
});
|
|
313
|
+
printedHeight = 0;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
225
316
|
// Move up over the previous render before re-printing. Use a
|
|
226
317
|
// bounded per-line clear instead of \x1b[J — a full clear-to-end
|
|
227
318
|
// would wipe the input dock's bottom rule and tips row that live
|
|
@@ -234,11 +325,14 @@ export class ApprovalManager {
|
|
|
234
325
|
}
|
|
235
326
|
write(`\x1b[${printedHeight}F`); // back to the anchor row
|
|
236
327
|
}
|
|
237
|
-
const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options });
|
|
328
|
+
const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options, showDetails });
|
|
238
329
|
write(block + '\n');
|
|
239
330
|
printedHeight = block.split('\n').length;
|
|
240
331
|
};
|
|
241
332
|
|
|
333
|
+
const ownsApprovalInput = isInteractive ? this._beginApprovalInput() : false;
|
|
334
|
+
|
|
335
|
+
try {
|
|
242
336
|
if (isInteractive) drawPrompt();
|
|
243
337
|
|
|
244
338
|
// ── Input loop ─────────────────────────────────────────────────
|
|
@@ -266,6 +360,11 @@ export class ApprovalManager {
|
|
|
266
360
|
// Letter shortcut: match against option.key
|
|
267
361
|
if (typeof k === 'string' && k.length === 1) {
|
|
268
362
|
const lower = k.toLowerCase();
|
|
363
|
+
if (lower === 'd' && toolName === 'shell') {
|
|
364
|
+
showDetails = !showDetails;
|
|
365
|
+
if (isInteractive) drawPrompt();
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
269
368
|
const idx = options.findIndex(o => o.key === lower);
|
|
270
369
|
if (idx >= 0) {
|
|
271
370
|
selected = idx;
|
|
@@ -278,28 +377,35 @@ export class ApprovalManager {
|
|
|
278
377
|
};
|
|
279
378
|
|
|
280
379
|
const value = await choose();
|
|
380
|
+
if (useDockPrompt) {
|
|
381
|
+
clearInputPrompt();
|
|
382
|
+
if (this._approvalPromptEnd) this._approvalPromptEnd();
|
|
383
|
+
moveToContent();
|
|
384
|
+
}
|
|
385
|
+
const prompt = promptForLog(toolName, args, tier, why);
|
|
281
386
|
|
|
282
387
|
switch (value) {
|
|
283
388
|
case 'approve':
|
|
284
389
|
this.history.push({ tool: toolName, decision: 'yes', tier, time: Date.now() });
|
|
285
|
-
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once' });
|
|
390
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once', prompt });
|
|
391
|
+
writeApprovalConfirmation(toolName, args, 'approved once');
|
|
286
392
|
return { approved: true, tier };
|
|
287
393
|
|
|
288
394
|
case 'allow-session': {
|
|
289
|
-
if (!this.policy.hitl?.allowSessionTrust) return this._prompt(toolName, args, context);
|
|
395
|
+
if (!this.policy.hitl?.allowSessionTrust) return await this._prompt(toolName, args, context);
|
|
290
396
|
const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'SESSION' });
|
|
291
397
|
write(` ${GREEN}✓${RST} ${DIM}trusted for this session: ${rule.pattern}${RST}\n\n`);
|
|
292
398
|
this.history.push({ tool: toolName, decision: 'session-trust', tier, time: Date.now(), rule_id: rule.id });
|
|
293
|
-
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id });
|
|
399
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id, prompt });
|
|
294
400
|
return { approved: true, tier, scope: 'SESSION', rule_id: rule.id };
|
|
295
401
|
}
|
|
296
402
|
|
|
297
403
|
case 'allow-project': {
|
|
298
|
-
if (!this.policy.hitl?.allowProjectTrust) return this._prompt(toolName, args, context);
|
|
404
|
+
if (!this.policy.hitl?.allowProjectTrust) return await this._prompt(toolName, args, context);
|
|
299
405
|
const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'PROJECT' });
|
|
300
406
|
write(` ${GREEN}✓${RST} ${DIM}trusted for this project: ${rule.pattern}${RST}\n\n`);
|
|
301
407
|
this.history.push({ tool: toolName, decision: 'project-trust', tier, time: Date.now(), rule_id: rule.id });
|
|
302
|
-
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id });
|
|
408
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id, prompt });
|
|
303
409
|
return { approved: true, tier, scope: 'PROJECT', rule_id: rule.id };
|
|
304
410
|
}
|
|
305
411
|
|
|
@@ -308,30 +414,31 @@ export class ApprovalManager {
|
|
|
308
414
|
const reason = 'User stopped the command';
|
|
309
415
|
write(` ${RED}✗${RST} ${DIM}stopped${RST}\n\n`);
|
|
310
416
|
this.history.push({ tool: toolName, decision: 'no', tier, time: Date.now(), reason });
|
|
311
|
-
this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason });
|
|
417
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason, prompt });
|
|
312
418
|
this._rememberRejection({ tool: toolName, args, tier, decision: 'reject', reason, note: '' });
|
|
313
419
|
return { approved: false, tier, reason };
|
|
314
420
|
}
|
|
315
421
|
|
|
316
422
|
case 'allow-all':
|
|
317
|
-
if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
|
|
423
|
+
if (requiresExplicitApproval(tier)) return await this._prompt(toolName, args, context);
|
|
318
424
|
this.approveAll = true;
|
|
319
425
|
write(` ${GREEN}✓✓${RST} ${DIM}allow-all activated${RST}\n\n`);
|
|
320
426
|
this.history.push({ tool: toolName, decision: 'approve-all', tier, time: Date.now() });
|
|
321
|
-
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session' });
|
|
427
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session', prompt });
|
|
322
428
|
return { approved: true, tier };
|
|
323
429
|
|
|
324
430
|
case 'allow-type':
|
|
325
|
-
if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
|
|
431
|
+
if (requiresExplicitApproval(tier)) return await this._prompt(toolName, args, context);
|
|
326
432
|
this.approvedToolTypes.add(toolName);
|
|
327
433
|
this.history.push({ tool: toolName, decision: 'type-approve', tier, time: Date.now() });
|
|
328
|
-
this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session' });
|
|
434
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session', prompt });
|
|
435
|
+
writeApprovalConfirmation(toolName, args, 'always allow');
|
|
329
436
|
return { approved: true, tier };
|
|
330
437
|
|
|
331
438
|
case 'why':
|
|
332
439
|
write(`\n ${DIM}${(context.reason || why).slice(0, 400)}${RST}\n\n`);
|
|
333
440
|
printedHeight = 0;
|
|
334
|
-
return this._prompt(toolName, args, context);
|
|
441
|
+
return await this._prompt(toolName, args, context);
|
|
335
442
|
|
|
336
443
|
case 'edit':
|
|
337
444
|
case 'replan':
|
|
@@ -340,13 +447,16 @@ export class ApprovalManager {
|
|
|
340
447
|
const reason = note ? `User asked to re-plan: ${note}` : 'User asked to re-plan';
|
|
341
448
|
write(` ${YELLOW}↩${RST} ${DIM}${note ? `re-plan — ${truncateNote(note)}` : 'reject with hint — rework the plan'}${RST}\n\n`);
|
|
342
449
|
this.history.push({ tool: toolName, decision: 'replan', tier, time: Date.now(), reason });
|
|
343
|
-
this.approvalLog.append({ tool: toolName, args, tier, decision: 'replan', scope: 'once', reason });
|
|
450
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'replan', scope: 'once', reason, prompt });
|
|
344
451
|
this._rememberRejection({ tool: toolName, args, tier, decision: 'replan', reason, note });
|
|
345
452
|
return { approved: false, tier, reason };
|
|
346
453
|
}
|
|
347
454
|
|
|
348
455
|
default:
|
|
349
|
-
return this._prompt(toolName, args, context);
|
|
456
|
+
return await this._prompt(toolName, args, context);
|
|
457
|
+
}
|
|
458
|
+
} finally {
|
|
459
|
+
if (ownsApprovalInput) this._endApprovalInput();
|
|
350
460
|
}
|
|
351
461
|
}
|
|
352
462
|
|
|
@@ -357,16 +467,25 @@ export class ApprovalManager {
|
|
|
357
467
|
return;
|
|
358
468
|
}
|
|
359
469
|
|
|
360
|
-
|
|
361
|
-
if (
|
|
470
|
+
const managesInput = !this._approvalPromptActive;
|
|
471
|
+
if (managesInput) {
|
|
472
|
+
if (this._execPause) this._execPause();
|
|
473
|
+
if (this._rl) this._rl.pause();
|
|
474
|
+
}
|
|
362
475
|
|
|
363
476
|
const wasRaw = process.stdin.isRaw;
|
|
364
|
-
process.stdin.setRawMode
|
|
477
|
+
if (managesInput && typeof process.stdin.setRawMode === 'function') {
|
|
478
|
+
process.stdin.setRawMode(true);
|
|
479
|
+
}
|
|
365
480
|
process.stdin.resume();
|
|
366
481
|
process.stdin.once('data', (data) => {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
489
|
|
|
371
490
|
const bytes = [...data];
|
|
372
491
|
const str = data.toString();
|
|
@@ -395,8 +514,11 @@ export class ApprovalManager {
|
|
|
395
514
|
return;
|
|
396
515
|
}
|
|
397
516
|
|
|
398
|
-
|
|
399
|
-
if (
|
|
517
|
+
const managesInput = !this._approvalPromptActive;
|
|
518
|
+
if (managesInput) {
|
|
519
|
+
if (this._execPause) this._execPause();
|
|
520
|
+
if (this._rl) this._rl.pause();
|
|
521
|
+
}
|
|
400
522
|
|
|
401
523
|
const wasRaw = process.stdin.isRaw;
|
|
402
524
|
if (typeof process.stdin.setRawMode === 'function') {
|
|
@@ -411,8 +533,10 @@ export class ApprovalManager {
|
|
|
411
533
|
if (typeof process.stdin.setRawMode === 'function') {
|
|
412
534
|
process.stdin.setRawMode(wasRaw || false);
|
|
413
535
|
}
|
|
414
|
-
if (
|
|
415
|
-
|
|
536
|
+
if (managesInput) {
|
|
537
|
+
if (this._rl) this._rl.resume();
|
|
538
|
+
if (this._execResume) this._execResume();
|
|
539
|
+
}
|
|
416
540
|
};
|
|
417
541
|
const finish = () => {
|
|
418
542
|
cleanup();
|
|
@@ -439,8 +563,8 @@ export class ApprovalManager {
|
|
|
439
563
|
});
|
|
440
564
|
}
|
|
441
565
|
|
|
442
|
-
_optionsFor(tier) {
|
|
443
|
-
return approvalOptions(tier);
|
|
566
|
+
_optionsFor(tier, toolName, args) {
|
|
567
|
+
return approvalOptions(tier, { tool: toolName, args });
|
|
444
568
|
}
|
|
445
569
|
|
|
446
570
|
getSummary() {
|
|
@@ -474,3 +598,103 @@ function truncateNote(note) {
|
|
|
474
598
|
const text = String(note || '').trim();
|
|
475
599
|
return text.length <= 120 ? text : text.slice(0, 119) + '…';
|
|
476
600
|
}
|
|
601
|
+
|
|
602
|
+
function promptForLog(tool, args, tier, why) {
|
|
603
|
+
return {
|
|
604
|
+
title: `${approvalTitleForLog(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
|
|
605
|
+
subject: approvalSummary(tool, args),
|
|
606
|
+
reason: why || '',
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function approvalTitleForLog(tier) {
|
|
611
|
+
switch (tier) {
|
|
612
|
+
case TIERS.SENSITIVE_READ:
|
|
613
|
+
case TIERS.PROTECTED_EDIT:
|
|
614
|
+
case TIERS.SHELL_DANGEROUS:
|
|
615
|
+
case TIERS.DESTRUCTIVE:
|
|
616
|
+
return 'EXPLICIT APPROVAL';
|
|
617
|
+
default:
|
|
618
|
+
return 'APPROVAL';
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function writeApprovalConfirmation(tool, args, label) {
|
|
623
|
+
if (tool === 'shell') {
|
|
624
|
+
write(` ${GREEN}✓${RST} ${DIM}${label} · shell ${shellApprovalSubject(args)}${RST}\n`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const subject = approvalSummary(tool, args);
|
|
628
|
+
const suffix = subject ? ` · ${truncateNote(subject)}` : '';
|
|
629
|
+
write(` ${GREEN}✓${RST} ${DIM}${label}${suffix}${RST}\n\n`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function shellApprovalSubject(args = {}) {
|
|
633
|
+
const profile = shellCommandProfile(args.command || args.cmd || '');
|
|
634
|
+
const subject = profile.compact ? profile.summary : profile.command;
|
|
635
|
+
return `$ ${truncateNote(subject)}`;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function writeSafetyBlock(reason) {
|
|
639
|
+
const cols = Math.max(40, Math.min(process.stderr.columns || process.stdout.columns || 80, 140));
|
|
640
|
+
const firstPrefix = ` ${RED}✗${RST} ${DIM}`;
|
|
641
|
+
const nextPrefix = ` ${DIM}`;
|
|
642
|
+
const firstWidth = Math.max(24, cols - 5);
|
|
643
|
+
const nextWidth = Math.max(24, cols - 5);
|
|
644
|
+
const lines = wrapSafetyText(reason, firstWidth, nextWidth);
|
|
645
|
+
|
|
646
|
+
write(`${firstPrefix}${lines[0] || ''}${RST}\n`);
|
|
647
|
+
for (const line of lines.slice(1)) {
|
|
648
|
+
write(`${nextPrefix}${line}${RST}\n`);
|
|
649
|
+
}
|
|
650
|
+
write('\n');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function wrapSafetyText(text, firstWidth, nextWidth) {
|
|
654
|
+
const words = String(text || '').replace(/\s+/g, ' ').trim().split(' ').filter(Boolean);
|
|
655
|
+
if (!words.length) return [''];
|
|
656
|
+
|
|
657
|
+
const lines = [];
|
|
658
|
+
let line = '';
|
|
659
|
+
let width = firstWidth;
|
|
660
|
+
|
|
661
|
+
for (const word of words) {
|
|
662
|
+
if (!line) {
|
|
663
|
+
if (word.length <= width) {
|
|
664
|
+
line = word;
|
|
665
|
+
} else {
|
|
666
|
+
const chunks = splitWord(word, width);
|
|
667
|
+
lines.push(...chunks.slice(0, -1));
|
|
668
|
+
line = chunks[chunks.length - 1] || '';
|
|
669
|
+
}
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (line.length + 1 + word.length <= width) {
|
|
674
|
+
line += ` ${word}`;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
lines.push(line);
|
|
679
|
+
width = nextWidth;
|
|
680
|
+
if (word.length <= width) {
|
|
681
|
+
line = word;
|
|
682
|
+
} else {
|
|
683
|
+
const chunks = splitWord(word, width);
|
|
684
|
+
lines.push(...chunks.slice(0, -1));
|
|
685
|
+
line = chunks[chunks.length - 1] || '';
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (line) lines.push(line);
|
|
690
|
+
return lines;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function splitWord(word, width) {
|
|
694
|
+
const size = Math.max(8, width);
|
|
695
|
+
const chunks = [];
|
|
696
|
+
for (let i = 0; i < word.length; i += size) {
|
|
697
|
+
chunks.push(word.slice(i, i + size));
|
|
698
|
+
}
|
|
699
|
+
return chunks;
|
|
700
|
+
}
|
package/src/core/file-diff.mjs
CHANGED