refactor(security): unify elevated mode handling across surfaces

This commit is contained in:
William Valentin
2026-02-19 11:41:53 -08:00
parent 7cb647cbb8
commit baa53f91d9
10 changed files with 467 additions and 403 deletions
+7 -81
View File
@@ -16,6 +16,7 @@ import { estimateMessageTokens, getContextWindow } from '../../../context/tokens
import type { PairingManager } from '../../../channels/pairing.js';
import { loginGitHub, loginOpenAI } from '../../../auth/index.js';
import { OllamaClient, LlamaCppClient } from '../../../models/index.js';
import { getElevationStatusMessage, setElevationFromInput } from '../../../security/elevation.js';
/** Format a tool name like "gmail.list" -> "Gmail: List" */
function formatToolName(name: string): string {
@@ -226,23 +227,6 @@ export function App({
return null;
}, []);
const parseDurationToMs = useCallback((value: string): number | null => {
const m = value.match(/^(\d+)([smhd])$/i);
if (!m) {
return null;
}
const n = Number.parseInt(m[1], 10);
if (!Number.isFinite(n) || n <= 0) {
return null;
}
const unit = m[2].toLowerCase();
if (unit === 's') {return n * 1000;}
if (unit === 'm') {return n * 60_000;}
if (unit === 'h') {return n * 3_600_000;}
if (unit === 'd') {return n * 86_400_000;}
return null;
}, []);
const handleSubmit = useCallback(async (value: string) => {
const command = parseCommand(value);
if (!command) {return;}
@@ -664,75 +648,18 @@ export function App({
}
case 'elevate': {
const untilRaw = session.getConfig('elevation.until_ms');
const reason = session.getConfig('elevation.reason') ?? '';
const id = session.getConfig('elevation.id') ?? '';
const showStatus = () => {
if (!untilRaw || !id) {
pushAssistantMessage('Elevated mode: off');
return;
}
const untilMs = Number.parseInt(untilRaw, 10);
if (!Number.isFinite(untilMs) || untilMs <= Date.now()) {
session.deleteConfig('elevation.until_ms');
session.deleteConfig('elevation.reason');
session.deleteConfig('elevation.id');
pushAssistantMessage('Elevated mode: off');
return;
}
const remainingSec = Math.ceil((untilMs - Date.now()) / 1000);
pushAssistantMessage(`Elevated mode: on (${remainingSec}s remaining)${reason ? ` - ${reason}` : ''}`);
const store = {
get: (key: string) => session.getConfig(key),
set: (key: string, value: string) => session.setConfig(key, value),
delete: (key: string) => session.deleteConfig(key),
};
const raw = (command.args ?? '').trim();
if (!raw) {
showStatus();
pushAssistantMessage(getElevationStatusMessage(store, { reasonSeparator: ' - ' }));
return;
}
const parts = raw.split(/\s+/);
const hasYes = parts.includes('--yes') || parts.includes('--confirm');
const filtered = parts.filter((p) => p !== '--yes' && p !== '--confirm');
if (filtered.length === 0) {
pushAssistantMessage('Usage: /elevate <duration> <reason...> --yes | /elevate off --yes');
return;
}
if (filtered[0] === 'off') {
if (!hasYes) {
pushAssistantMessage('Refusing to disable elevation without explicit confirmation. Use: /elevate off --yes');
return;
}
session.deleteConfig('elevation.until_ms');
session.deleteConfig('elevation.reason');
session.deleteConfig('elevation.id');
pushAssistantMessage('Elevated mode: off');
return;
}
if (!hasYes) {
pushAssistantMessage('Refusing to enable elevation without explicit confirmation. Use: /elevate <duration> <reason...> --yes');
return;
}
const ttlMs = parseDurationToMs(filtered[0]);
if (!ttlMs) {
pushAssistantMessage('Invalid duration. Use one of: 30s, 10m, 1h, 1d');
return;
}
const reasonText = filtered.slice(1).join(' ').trim();
const untilMs = Date.now() + ttlMs;
const newId = `${untilMs}`;
session.setConfig('elevation.until_ms', String(untilMs));
session.setConfig('elevation.id', newId);
if (reasonText) {
session.setConfig('elevation.reason', reasonText);
} else {
session.deleteConfig('elevation.reason');
}
pushAssistantMessage(`Elevated mode: on until ${new Date(untilMs).toISOString()}`);
pushAssistantMessage(setElevationFromInput(store, raw));
return;
}
@@ -836,7 +763,6 @@ export function App({
pushAssistantMessage,
getAvailableBackends,
createLocalClient,
parseDurationToMs,
localProviders,
currentLocalProvider,
pairingManager,
+9 -83
View File
@@ -26,6 +26,7 @@ import type { PairingManager } from '../../channels/pairing.js';
import { getColoredBanner } from './banner.js';
import type { HookEngine } from '../../hooks/index.js';
import { estimateMessageTokens, getContextWindow } from '../../context/tokens.js';
import { getElevationStatusMessage, setElevationFromInput } from '../../security/elevation.js';
export { parseCommand, type Command };
@@ -646,94 +647,19 @@ export class MinimalTui {
}
private handleElevateCommand(args?: string): void {
const untilRaw = this.config.session.getConfig('elevation.until_ms');
const reason = this.config.session.getConfig('elevation.reason') ?? '';
const id = this.config.session.getConfig('elevation.id') ?? '';
const showStatus = () => {
if (!untilRaw || !id) {
console.log(`${colors.gray}Elevated mode: off${colors.reset}\n`);
return;
}
const untilMs = Number.parseInt(untilRaw, 10);
if (!Number.isFinite(untilMs) || untilMs <= Date.now()) {
this.config.session.deleteConfig('elevation.until_ms');
this.config.session.deleteConfig('elevation.reason');
this.config.session.deleteConfig('elevation.id');
console.log(`${colors.gray}Elevated mode: off${colors.reset}\n`);
return;
}
const remainingSec = Math.ceil((untilMs - Date.now()) / 1000);
console.log(`${colors.gray}Elevated mode: on (${remainingSec}s remaining)${reason ? ` - ${reason}` : ''}${colors.reset}\n`);
const store = {
get: (key: string) => this.config.session.getConfig(key),
set: (key: string, value: string) => this.config.session.setConfig(key, value),
delete: (key: string) => this.config.session.deleteConfig(key),
};
const raw = (args ?? '').trim();
if (!raw) {
showStatus();
const status = getElevationStatusMessage(store, { reasonSeparator: ' - ' });
console.log(`${colors.gray}${status}${colors.reset}\n`);
return;
}
const parts = raw.split(/\s+/);
const hasYes = parts.includes('--yes') || parts.includes('--confirm');
const filtered = parts.filter((p) => p !== '--yes' && p !== '--confirm');
if (filtered.length === 0) {
console.log(`${colors.gray}Usage: /elevate <duration> <reason...> --yes | /elevate off --yes${colors.reset}\n`);
return;
}
if (filtered[0] === 'off') {
if (!hasYes) {
console.log(`${colors.gray}Refusing to disable elevation without explicit confirmation. Use: /elevate off --yes${colors.reset}\n`);
return;
}
this.config.session.deleteConfig('elevation.until_ms');
this.config.session.deleteConfig('elevation.reason');
this.config.session.deleteConfig('elevation.id');
console.log(`${colors.gray}Elevated mode: off${colors.reset}\n`);
return;
}
if (!hasYes) {
console.log(`${colors.gray}Refusing to enable elevation without explicit confirmation. Use: /elevate <duration> <reason...> --yes${colors.reset}\n`);
return;
}
const ttlMs = this.parseDurationToMs(filtered[0]);
if (!ttlMs) {
console.log(`${colors.gray}Invalid duration. Use one of: 30s, 10m, 1h, 1d${colors.reset}\n`);
return;
}
const reasonText = filtered.slice(1).join(' ').trim();
const untilMs = Date.now() + ttlMs;
const newId = `${untilMs}`;
this.config.session.setConfig('elevation.until_ms', String(untilMs));
this.config.session.setConfig('elevation.id', newId);
if (reasonText) {
this.config.session.setConfig('elevation.reason', reasonText);
} else {
this.config.session.deleteConfig('elevation.reason');
}
console.log(`${colors.gray}Elevated mode: on until ${new Date(untilMs).toISOString()}${colors.reset}\n`);
}
private parseDurationToMs(value: string): number | null {
const m = value.match(/^(\d+)([smhd])$/i);
if (!m) {
return null;
}
const n = Number.parseInt(m[1], 10);
if (!Number.isFinite(n) || n <= 0) {
return null;
}
const unit = m[2].toLowerCase();
if (unit === 's') {return n * 1000;}
if (unit === 'm') {return n * 60_000;}
if (unit === 'h') {return n * 3_600_000;}
if (unit === 'd') {return n * 86_400_000;}
return null;
const output = setElevationFromInput(store, raw);
console.log(`${colors.gray}${output}${colors.reset}\n`);
}
private handleModelCommand(name?: string, providerModel?: string): void {