refactor(security): unify elevated mode handling across surfaces
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user