@blockrun/franklin 3.15.100 → 3.15.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/permissions.d.ts +15 -0
- package/dist/agent/permissions.js +54 -1
- package/dist/ui/app.js +1 -1
- package/package.json +1 -1
|
@@ -30,6 +30,21 @@ export declare class PermissionManager {
|
|
|
30
30
|
* Returns true if allowed, false if denied.
|
|
31
31
|
*/
|
|
32
32
|
promptUser(toolName: string, input: Record<string, unknown>, pendingCount?: number): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Persist a tool name to the user's allow rules so future sessions
|
|
35
|
+
* skip the prompt. Idempotent: appends to the existing
|
|
36
|
+
* `allow: []` array only if not already present.
|
|
37
|
+
*
|
|
38
|
+
* Why this exists: pre-2026-05-12, "always" in the UI prompt was a
|
|
39
|
+
* misnomer — it only added the tool to the in-memory `sessionAllowed`
|
|
40
|
+
* Set, which evaporated on every `franklin start`. Users reported
|
|
41
|
+
* being prompted repeatedly across sessions despite hitting [a] each
|
|
42
|
+
* time. Persistence here makes "always" actually mean always.
|
|
43
|
+
*
|
|
44
|
+
* Best-effort writes (try/catch around fs) — a logging failure should
|
|
45
|
+
* never block the paid call that just got approved.
|
|
46
|
+
*/
|
|
47
|
+
private persistAllowRule;
|
|
33
48
|
private loadRules;
|
|
34
49
|
private matchesRule;
|
|
35
50
|
private getPrimaryInputValue;
|
|
@@ -122,7 +122,15 @@ export class PermissionManager {
|
|
|
122
122
|
if (this.promptFn) {
|
|
123
123
|
const result = await this.promptFn(toolName, hint);
|
|
124
124
|
if (result === 'always') {
|
|
125
|
+
// "Always" must mean ALWAYS — including after the user restarts
|
|
126
|
+
// franklin. Pre-fix it was only in-memory: every `franklin start`
|
|
127
|
+
// re-asked the same prompts even after the user had pressed [a]
|
|
128
|
+
// explicitly. Verified 2026-05-12: ~/.blockrun/franklin-permissions.json
|
|
129
|
+
// was non-existent despite the user reporting repeated prompts.
|
|
130
|
+
// Persist to disk and update in-memory rules so subsequent
|
|
131
|
+
// checks short-circuit at the allow-rule stage (line 124).
|
|
125
132
|
this.sessionAllowed.add(toolName);
|
|
133
|
+
this.persistAllowRule(toolName);
|
|
126
134
|
return true;
|
|
127
135
|
}
|
|
128
136
|
return result === 'yes';
|
|
@@ -140,7 +148,8 @@ export class PermissionManager {
|
|
|
140
148
|
const normalized = answer.trim().toLowerCase();
|
|
141
149
|
if (normalized === 'a' || normalized === 'always') {
|
|
142
150
|
this.sessionAllowed.add(toolName);
|
|
143
|
-
|
|
151
|
+
this.persistAllowRule(toolName);
|
|
152
|
+
console.error(chalk.green(` ✓ ${toolName} allowed (saved to ~/.blockrun/franklin-permissions.json)`));
|
|
144
153
|
return true;
|
|
145
154
|
}
|
|
146
155
|
if (normalized === 'y' || normalized === 'yes' || normalized === '') {
|
|
@@ -150,6 +159,50 @@ export class PermissionManager {
|
|
|
150
159
|
return false;
|
|
151
160
|
}
|
|
152
161
|
// ─── Internal ──────────────────────────────────────────────────────────
|
|
162
|
+
/**
|
|
163
|
+
* Persist a tool name to the user's allow rules so future sessions
|
|
164
|
+
* skip the prompt. Idempotent: appends to the existing
|
|
165
|
+
* `allow: []` array only if not already present.
|
|
166
|
+
*
|
|
167
|
+
* Why this exists: pre-2026-05-12, "always" in the UI prompt was a
|
|
168
|
+
* misnomer — it only added the tool to the in-memory `sessionAllowed`
|
|
169
|
+
* Set, which evaporated on every `franklin start`. Users reported
|
|
170
|
+
* being prompted repeatedly across sessions despite hitting [a] each
|
|
171
|
+
* time. Persistence here makes "always" actually mean always.
|
|
172
|
+
*
|
|
173
|
+
* Best-effort writes (try/catch around fs) — a logging failure should
|
|
174
|
+
* never block the paid call that just got approved.
|
|
175
|
+
*/
|
|
176
|
+
persistAllowRule(toolName) {
|
|
177
|
+
const configPath = path.join(BLOCKRUN_DIR, 'franklin-permissions.json');
|
|
178
|
+
try {
|
|
179
|
+
// Read current state (may not exist). Treat missing/malformed as
|
|
180
|
+
// empty rules — never throw on the user's tool execution path.
|
|
181
|
+
let current = { allow: [], deny: [], ask: [] };
|
|
182
|
+
if (fs.existsSync(configPath)) {
|
|
183
|
+
try {
|
|
184
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
185
|
+
current = {
|
|
186
|
+
allow: Array.isArray(raw.allow) ? raw.allow : [],
|
|
187
|
+
deny: Array.isArray(raw.deny) ? raw.deny : [],
|
|
188
|
+
ask: Array.isArray(raw.ask) ? raw.ask : [],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch { /* malformed — reset */ }
|
|
192
|
+
}
|
|
193
|
+
if (current.allow.includes(toolName))
|
|
194
|
+
return; // already saved
|
|
195
|
+
current.allow.push(toolName);
|
|
196
|
+
// Update in-memory rules too so subsequent checks short-circuit
|
|
197
|
+
// at line 124 (matchesRule against allow) without re-prompting.
|
|
198
|
+
if (!this.rules.allow.includes(toolName)) {
|
|
199
|
+
this.rules.allow.push(toolName);
|
|
200
|
+
}
|
|
201
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
202
|
+
fs.writeFileSync(configPath, JSON.stringify(current, null, 2));
|
|
203
|
+
}
|
|
204
|
+
catch { /* best-effort */ }
|
|
205
|
+
}
|
|
153
206
|
loadRules() {
|
|
154
207
|
const configPath = path.join(BLOCKRUN_DIR, 'franklin-permissions.json');
|
|
155
208
|
const legacyPath = path.join(BLOCKRUN_DIR, 'runcode-permissions.json');
|
package/dist/ui/app.js
CHANGED
|
@@ -1060,7 +1060,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
1060
1060
|
: `${formatTokens(r.tokens.input)} in / ${formatTokens(r.tokens.output)} out`, r.cost > 0 ? ` · $${r.cost.toFixed(4)}` : '', r.savings !== undefined && r.savings > 0 ? _jsxs(Text, { color: "green", children: [" saved ", Math.round(r.savings * 100), "%"] }) : '', r.ctxPct !== undefined && r.ctxPct >= 5
|
|
1061
1061
|
? _jsxs(Text, { color: r.ctxPct >= 80 ? 'red' : r.ctxPct >= 50 ? 'yellow' : undefined, dimColor: r.ctxPct < 50, children: [" \u00B7 ctx ", r.ctxPct, "%"] })
|
|
1062
1062
|
: ''] }) }))] }, r.key));
|
|
1063
|
-
} }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "red", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ACTION REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "yellow", children: "\u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: ["\u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "magenta", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ANSWER REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "cyan", children: "\u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: ["\u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
|
|
1063
|
+
} }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "red", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ACTION REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "yellow", children: "\u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: ["\u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { dimColor: true, italic: true, children: "(saved across sessions)" }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "magenta", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ANSWER REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "cyan", children: "\u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: ["\u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
|
|
1064
1064
|
// resolveAskUserAnswer translates "1" / "2" / ... into the
|
|
1065
1065
|
// matching label string when the dialog showed a numbered
|
|
1066
1066
|
// option list. Without it, every onAskUser caller's
|
package/package.json
CHANGED