mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] settings: legible secret-masked settings-change chip in the agent transcript
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
// Render an agent's SettingsWrite/SettingsRead so the transcript shows WHAT it
|
||||
// touched at a glance, with secrets masked. The agent's raw `changes` input can
|
||||
// carry a key it's trying to set, and the generic MCP renderer would paint it; a
|
||||
// settings value is the one new place a secret could land on screen, so it's the
|
||||
// one place we mask. Mirrors the backend's name rule (redaction.is_secret_field).
|
||||
|
||||
const SECRET_NAME_RE = /_(key|token|secret)$/i;
|
||||
// Narrow, prefix-anchored so it can't mask a long path or system prompt (those
|
||||
// have slashes/spaces); it only catches a real key pasted into a non-secret field.
|
||||
const KEYISH_VALUE_RE = /^(sk-|sk-ant-|AIza|ghp_|gho_|github_pat_|xox[baprs]-)/;
|
||||
|
||||
export function isSettingsWriteTool(toolName: string): boolean {
|
||||
return /__SettingsWrite$/.test(toolName);
|
||||
}
|
||||
export function isSettingsReadTool(toolName: string): boolean {
|
||||
return /__SettingsRead$/.test(toolName);
|
||||
}
|
||||
|
||||
function isSecretField(name: string): boolean {
|
||||
return SECRET_NAME_RE.test(name) || name === 'installation_id';
|
||||
}
|
||||
|
||||
function maskValue(key: string, value: unknown, cap: number): string {
|
||||
if (isSecretField(key)) return '••••';
|
||||
if (typeof value === 'string') {
|
||||
if (KEYISH_VALUE_RE.test(value.trim())) return '••••';
|
||||
return value.length > cap ? value.slice(0, cap) + '…' : value;
|
||||
}
|
||||
if (value === null || value === undefined) return 'none';
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
default_model: 'Default model',
|
||||
default_mode: 'Default mode',
|
||||
default_system_prompt: 'System prompt',
|
||||
default_thinking_level: 'Thinking level',
|
||||
default_max_turns: 'Max turns',
|
||||
default_folder: 'Default folder',
|
||||
theme: 'Theme',
|
||||
browser_homepage: 'Browser homepage',
|
||||
new_agent_shortcut: 'New-agent shortcut',
|
||||
zoom_sensitivity: 'Zoom sensitivity',
|
||||
};
|
||||
|
||||
function humanizeKey(key: string): string {
|
||||
return LABELS[key] || key.replace(/_/g, ' ').replace(/^\w/, (ch) => ch.toUpperCase());
|
||||
}
|
||||
|
||||
interface SettingsChangeRow {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function settingsChangeRows(input: unknown, cap: number): SettingsChangeRow[] {
|
||||
const changes = (input as { changes?: unknown })?.changes;
|
||||
if (!changes || typeof changes !== 'object') return [];
|
||||
return Object.entries(changes as Record<string, unknown>)
|
||||
// some models tack a free-form 'reason' onto the args; it isn't a setting.
|
||||
.filter(([k]) => k !== 'reason')
|
||||
.map(([k, v]) => ({ label: humanizeKey(k), value: maskValue(k, v, cap) }));
|
||||
}
|
||||
|
||||
/** One-line header preview, e.g. "Theme → Light · Default model → Sonnet". */
|
||||
export function settingsWriteSummary(input: unknown): string {
|
||||
return settingsChangeRows(input, 40).map((r) => `${r.label} → ${r.value}`).join(' · ');
|
||||
}
|
||||
|
||||
/** Expanded body: one masked change per line. Replaces the raw-JSON render. */
|
||||
export function settingsWriteDisplay(input: unknown): string {
|
||||
const rows = settingsChangeRows(input, 200);
|
||||
return rows.length === 0 ? 'No changes.' : rows.map((r) => `${r.label}: ${r.value}`).join('\n');
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AgentMessage } from '@/shared/state/agentsSlice';
|
||||
import { prettyPath, prettyUrl, quoteQuery, bashCommandDetail } from './toolLabels';
|
||||
import { parseMcpToolName, getMcpInputSummary, getGmailHeader } from '@/shared/mcpToolMeta';
|
||||
import { isSettingsWriteTool, isSettingsReadTool, settingsWriteSummary, settingsWriteDisplay } from './settingsToolMeta';
|
||||
|
||||
export function getToolData(call: AgentMessage) {
|
||||
const content = typeof call.content === 'object' ? call.content : {};
|
||||
@@ -18,6 +19,11 @@ export function isBashTool(name: string) {
|
||||
|
||||
export function getInputSummary(toolName: string, input: any): string {
|
||||
try {
|
||||
// Settings tool first: legible change list, secrets masked. Must precede the
|
||||
// generic MCP path (it is an MCP tool) or it renders raw changes JSON.
|
||||
if (isSettingsWriteTool(toolName)) return settingsWriteSummary(input);
|
||||
if (isSettingsReadTool(toolName)) return '';
|
||||
|
||||
const mcp = parseMcpToolName(toolName);
|
||||
if (mcp.isMcp) return getMcpInputSummary(input);
|
||||
|
||||
@@ -60,6 +66,11 @@ function formatMcpInputDisplay(input: any): string {
|
||||
|
||||
export function formatInputDisplay(toolName: string, input: any): string {
|
||||
try {
|
||||
// Masked, one-per-line change list instead of raw changes JSON (which would
|
||||
// paint a secret value the agent tried to set).
|
||||
if (isSettingsWriteTool(toolName)) return settingsWriteDisplay(input);
|
||||
if (isSettingsReadTool(toolName)) return '';
|
||||
|
||||
const mcp = parseMcpToolName(toolName);
|
||||
if (mcp.isMcp) return formatMcpInputDisplay(input);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { GoogleServiceIcon } from '../mcp-cards/GoogleServiceIcon';
|
||||
import { ElapsedTimer, formatElapsed } from '../parsing/toolBubbleChrome';
|
||||
import { useTermColors } from '../parsing/toolColorize';
|
||||
import { ParsedResult } from '../parsing/toolResultParsing';
|
||||
import { isSettingsWriteTool, settingsWriteSummary } from '../parsing/settingsToolMeta';
|
||||
import { McpToolInfo, getMcpShortAction } from '@/shared/mcpToolMeta';
|
||||
import { McpResultCard } from '../mcp-cards/McpResultCard';
|
||||
|
||||
@@ -56,6 +57,9 @@ export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
|
||||
const ServiceIcon = mcpInfo.isMcp && mcpInfo.service
|
||||
? <GoogleServiceIcon service={mcpInfo.service} size={14} />
|
||||
: null;
|
||||
// A grouped settings write shows the masked change list (input-derived, so it
|
||||
// reads even while pending) instead of the generic "Applied: theme" result line.
|
||||
const headerSummary = isSettingsWriteTool(toolName) ? settingsWriteSummary(input) : resultSummary;
|
||||
|
||||
return (
|
||||
<Box {...selectAttrs} sx={{ my: 0 }}>
|
||||
@@ -83,7 +87,7 @@ export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
|
||||
>
|
||||
{serviceLabel}
|
||||
</Typography>
|
||||
{resultSummary && !isError && (
|
||||
{headerSummary && !isError && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.text.secondary,
|
||||
@@ -95,10 +99,10 @@ export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
|
||||
: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }),
|
||||
}}
|
||||
>
|
||||
{resultSummary}
|
||||
{headerSummary}
|
||||
</Typography>
|
||||
)}
|
||||
{!resultSummary && !showTimer && <Box sx={{ flex: 1 }} />}
|
||||
{!headerSummary && !showTimer && <Box sx={{ flex: 1 }} />}
|
||||
{showTimer && (
|
||||
<>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
Reference in New Issue
Block a user