@bahulam/code 2.6.14 → 2.6.16
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 +5 -5
- package/src/auth/tarang-auth.mjs +6 -0
- package/src/config/cli-args.mjs +7 -1
- package/src/config/model-catalog.mjs +57 -0
- package/src/core/approval-log.mjs +23 -0
- package/src/core/approval.mjs +93 -17
- package/src/core/bundled-runtime.mjs +12 -0
- package/src/core/error-guidance.mjs +8 -4
- package/src/core/file-diff.mjs +1 -1
- package/src/core/local-agent.mjs +3 -3
- 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 +7 -3
- package/src/permissions/command-classifier.mjs +50 -2
- package/src/telemetry/index.mjs +97 -71
- package/src/terminal/main.mjs +40 -0
- package/src/terminal/repl-format.mjs +64 -4
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +256 -33
- package/src/terminal/repl-resume.mjs +27 -12
- package/src/terminal/repl-state.mjs +16 -0
- package/src/terminal/repl.mjs +644 -85
- package/src/terminal/tool-display.mjs +20 -1
- package/src/ui/approval.mjs +33 -5
- package/src/ui/input-dock.mjs +192 -16
- package/src/ui/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +2 -0
- package/src/ui/sub-agent.mjs +17 -2
- 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.16",
|
|
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": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"start": "node src/terminal/main.mjs",
|
|
17
|
-
"test": "BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-backend-url.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-agent-history.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-sse-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-tool-executor.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-project-artifacts.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-work-scope.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-skills.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-callback.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-rate-limit-display.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-preflight.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-formatter.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-terminal-rendering.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-input-dock.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-live-steering-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-slash-commands.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval-log.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-kepler-contract.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-session-manager.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-safety.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-jsonl-writer.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-analytics.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-stagnation.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-attachments.mjs"
|
|
17
|
+
"test": "BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-backend-url.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-agent-history.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-sse-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-tool-executor.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-project-artifacts.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-work-scope.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-skills.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-callback.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-rate-limit-display.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-preflight.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-formatter.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-terminal-rendering.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-render-queue.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-input-dock.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-live-steering-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-slash-commands.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval-log.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-kepler-contract.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-session-manager.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-safety.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-jsonl-writer.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-analytics.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-stagnation.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-attachments.mjs"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=18.0.0"
|
|
@@ -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.16",
|
|
53
|
+
"@bahulam/runtime-linux-x64": "2.6.16",
|
|
54
|
+
"@bahulam/runtime-win32-x64": "2.6.16"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/auth/tarang-auth.mjs
CHANGED
|
@@ -76,6 +76,12 @@ export class TarangAuth {
|
|
|
76
76
|
models: this._config.models || {},
|
|
77
77
|
configuredProviders: this._config.configured_providers || [],
|
|
78
78
|
gatewayConfig: this._config.gateway_config || {},
|
|
79
|
+
// PRD-076 W7: persisted /model picks. `modelConfig` mirrors the
|
|
80
|
+
// Python-side key read_local_model_config() looks for; keep the
|
|
81
|
+
// snake_case JSON key so the runtime picks it up unchanged.
|
|
82
|
+
modelConfig: this._config.model_config || {},
|
|
83
|
+
modelMode: this._config.model_mode || null,
|
|
84
|
+
routePreference: this._config.route_preference || null,
|
|
79
85
|
};
|
|
80
86
|
}
|
|
81
87
|
|
package/src/config/cli-args.mjs
CHANGED
|
@@ -29,6 +29,7 @@ export function parseArgs(args) {
|
|
|
29
29
|
timeout: null,
|
|
30
30
|
allowedTools: null,
|
|
31
31
|
disallowedTools: null,
|
|
32
|
+
route: null,
|
|
32
33
|
resume: false,
|
|
33
34
|
resumeSessionId: null,
|
|
34
35
|
headless: false,
|
|
@@ -49,6 +50,10 @@ export function parseArgs(args) {
|
|
|
49
50
|
result.model = args[++i];
|
|
50
51
|
break;
|
|
51
52
|
|
|
53
|
+
case '--route':
|
|
54
|
+
result.route = args[++i];
|
|
55
|
+
break;
|
|
56
|
+
|
|
52
57
|
case '--permission-mode':
|
|
53
58
|
result.permissionMode = args[++i];
|
|
54
59
|
break;
|
|
@@ -169,7 +174,8 @@ export function getUsageText() {
|
|
|
169
174
|
Usage: occ [options] [prompt]
|
|
170
175
|
|
|
171
176
|
Options:
|
|
172
|
-
--model, -m <
|
|
177
|
+
--model, -m <id|role=id> Session model override (also named modes: fast|thinking|extra|max)
|
|
178
|
+
--route <platform|byok> Model route; platform validates against the curated catalog
|
|
173
179
|
--permission-mode <mode> Permission mode (bypassPermissions, acceptEdits, plan, auto, dontAsk)
|
|
174
180
|
--print, -p <prompt> Non-interactive mode: run prompt and exit
|
|
175
181
|
--output-format <fmt> Output format: text, json, stream-json
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped model catalog snapshot — Node side of PRD-076 W7.
|
|
3
|
+
*
|
|
4
|
+
* The canonical source is `codekepler-backend/app/services/
|
|
5
|
+
* model_catalog_snapshot.json` (admin dashboard edits it and publishes to
|
|
6
|
+
* Supabase). `stage-runtime-app.sh` copies that file into every runtime
|
|
7
|
+
* wheel at `runtime/app/services/model_catalog_snapshot.json`, so when
|
|
8
|
+
* `@bahulam/runtime-<platform>-<arch>` is installed as a sibling optional
|
|
9
|
+
* dep of `@bahulam/code`, the file is already on disk — we read from
|
|
10
|
+
* there instead of maintaining a third copy in the CLI package.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import { runtimeSnapshotPath } from '../core/bundled-runtime.mjs';
|
|
15
|
+
|
|
16
|
+
let _cache = null;
|
|
17
|
+
|
|
18
|
+
// Snapshot rows are camelCase (matches how the backend delivers the
|
|
19
|
+
// static file); the /api/models response is snake_case. Normalize once
|
|
20
|
+
// here so downstream consumers can treat both sources identically.
|
|
21
|
+
function normalizeSnapshotRow(row) {
|
|
22
|
+
const id = row.value || row.id;
|
|
23
|
+
if (!id) return null;
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
provider: row.provider || (id.includes('/') ? id.split('/', 1)[0] : 'unknown'),
|
|
27
|
+
label: row.label || id,
|
|
28
|
+
input_cost_usd_per_m: row.inputCost ?? row.input_cost_usd_per_m ?? null,
|
|
29
|
+
output_cost_usd_per_m: row.outputCost ?? row.output_cost_usd_per_m ?? null,
|
|
30
|
+
context_length: row.context ?? row.context_length ?? null,
|
|
31
|
+
max_output: row.maxOutput ?? row.max_output ?? null,
|
|
32
|
+
supports_tools: row.supportsTools ?? row.supports_tools ?? false,
|
|
33
|
+
supports_reasoning: row.supportsReasoning ?? row.supports_reasoning ?? false,
|
|
34
|
+
harness_validated: row.harnessValidated ?? row.harness_validated ?? false,
|
|
35
|
+
cache_profile: row.cacheProfile ?? row.cache_profile ?? null,
|
|
36
|
+
platform_access_tier: row.platformAccessTier ?? row.platform_access_tier ?? [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read the shipped catalog snapshot from the installed runtime bundle.
|
|
42
|
+
* Returns an array of normalized rows (API-shape) or null if the runtime
|
|
43
|
+
* isn't installed / the file is unreadable. Cached after first success.
|
|
44
|
+
*/
|
|
45
|
+
export function readShippedCatalog() {
|
|
46
|
+
if (_cache !== null) return _cache;
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(runtimeSnapshotPath(), 'utf-8');
|
|
49
|
+
const data = JSON.parse(raw);
|
|
50
|
+
const rows = Array.isArray(data?.models) ? data.models : [];
|
|
51
|
+
_cache = rows.map(normalizeSnapshotRow).filter(Boolean);
|
|
52
|
+
return _cache;
|
|
53
|
+
} catch {
|
|
54
|
+
_cache = null;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -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';
|
|
@@ -364,6 +364,18 @@ export function isRuntimeInstalled() {
|
|
|
364
364
|
return fs.existsSync(_runtimeBin());
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Absolute path to the shipped model catalog snapshot inside the runtime
|
|
369
|
+
* bundle. Backend edits `app/services/model_catalog_snapshot.json` as the
|
|
370
|
+
* canonical source (admin dashboard publishes it to Supabase);
|
|
371
|
+
* stage-runtime-app.sh copies that same file into every runtime wheel at
|
|
372
|
+
* `runtime/app/services/model_catalog_snapshot.json`. The CLI reads from
|
|
373
|
+
* there so there is no third copy to keep in sync.
|
|
374
|
+
*/
|
|
375
|
+
export function runtimeSnapshotPath() {
|
|
376
|
+
return path.join(_runtimeRoot(), 'runtime', 'app', 'services', 'model_catalog_snapshot.json');
|
|
377
|
+
}
|
|
378
|
+
|
|
367
379
|
/** Diagnostic snapshot for `bahulam doctor`. */
|
|
368
380
|
export function runtimeInfo() {
|
|
369
381
|
return {
|
|
@@ -251,12 +251,16 @@ export function formatAgentErrorGuidance(data = {}) {
|
|
|
251
251
|
lines.push('Upgrade your plan for a larger 5-hour message window, or switch to BYOK if available.');
|
|
252
252
|
if (pricingUrl) lines.push(`Open ${pricingUrl} to upgrade.`);
|
|
253
253
|
} else if (phase === 'gateway' || code.includes('gateway')) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
// Generic first: on the platform route the user has NO provider key —
|
|
255
|
+
// telling them to "check your DeepSeek API key" is wrong and alarming.
|
|
256
|
+
// Provider-key guidance only applies when we know the route is BYOK.
|
|
257
|
+
const label = providerGuidance?.label || compact(data.provider) || 'model provider';
|
|
258
|
+
lines.push(`The upstream ${label} request failed before the agent could respond — usually a transient provider-side error.`);
|
|
259
|
+
lines.push('Retry the turn. If it persists, switch models with /model and try again.');
|
|
260
|
+
if (compact(data.route) === 'byok' && providerGuidance) {
|
|
257
261
|
lines.push(...providerGuidance.lines);
|
|
258
262
|
} else {
|
|
259
|
-
lines.push('
|
|
263
|
+
lines.push('Using your own key (BYOK)? Verify it is saved and active in settings, then run /login so provider settings sync.');
|
|
260
264
|
}
|
|
261
265
|
} else if (/authentication|token/i.test(message)) {
|
|
262
266
|
lines.push('Run /login to re-authenticate.');
|
package/src/core/file-diff.mjs
CHANGED
package/src/core/local-agent.mjs
CHANGED
|
@@ -21,11 +21,11 @@ const MAX_ITERATIONS = 50;
|
|
|
21
21
|
const TOOL_SCHEMAS = [
|
|
22
22
|
{
|
|
23
23
|
name: 'shell',
|
|
24
|
-
description: 'Run one non-interactive shell command and return stdout/stderr.
|
|
24
|
+
description: 'Run one non-interactive shell command and return stdout/stderr. Command substitution (backticks, $()) is allowed but triggers a user-approval prompt; prefer plain commands when equivalent.',
|
|
25
25
|
input_schema: {
|
|
26
26
|
type: 'object',
|
|
27
27
|
properties: {
|
|
28
|
-
command: { type: 'string', description: 'The command to execute.
|
|
28
|
+
command: { type: 'string', description: 'The command to execute. Backticks/$() are allowed (user approval required); use them when splitting would be awkward.' },
|
|
29
29
|
timeout: { type: 'number', description: 'Timeout in milliseconds (default: 120000)' },
|
|
30
30
|
},
|
|
31
31
|
required: ['command'],
|
|
@@ -478,7 +478,7 @@ export class LocalAgent {
|
|
|
478
478
|
'You are Bahulam Code, Bahulam\'s AI coding agent running in local mode.',
|
|
479
479
|
'You have access to tools for reading, writing, and executing code.',
|
|
480
480
|
'Use tools to accomplish the user\'s request. Be concise and direct.',
|
|
481
|
-
'
|
|
481
|
+
'Shell command substitution (backticks, $()) is allowed but requires user approval — use it when it is the natural expression; otherwise prefer plain commands.',
|
|
482
482
|
];
|
|
483
483
|
if (context.cwd) parts.push(`Working directory: ${context.cwd}`);
|
|
484
484
|
if (context.gitBranch) parts.push(`Git branch: ${context.gitBranch}`);
|
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
|
+
}
|