Files
goodbuddy/src/main/agent/approval-summary.ts
T

204 lines
4.8 KiB
TypeScript

const sensitiveKey = /token|secret|password|api.?key|authorization/iu
function redactValue(
value: unknown,
seen: WeakSet<object>,
depth = 0
): unknown {
if (depth > 8) {
return '[TRUNCATED]'
}
if (typeof value === 'string') {
return redactSensitiveText(value)
}
if (!value || typeof value !== 'object') {
return value
}
if (seen.has(value)) {
return '[CIRCULAR]'
}
seen.add(value)
if (Array.isArray(value)) {
return value
.slice(0, 100)
.map((item) => redactValue(item, seen, depth + 1))
}
return Object.fromEntries(
Object.entries(value)
.slice(0, 100)
.map(([key, item]) => [
key,
sensitiveKey.test(key)
? '[REDACTED]'
: redactValue(item, seen, depth + 1)
])
)
}
export function redactSensitiveText(value: string): string {
return value
.replace(
/\bAuthorization\b(\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\r\n,;}]+)/giu,
'Authorization$1[REDACTED]'
)
.replace(/\bBearer\s+\S+/giu, 'Bearer [REDACTED]')
.replace(
/(["']?)(api[-_ ]?key|token|secret|password|authorization)\1(\s*[:=]\s*)"[^"\r\n]*"/giu,
'$1$2$1$3"[REDACTED]"'
)
.replace(
/(["']?)(api[-_ ]?key|token|secret|password|authorization)\1(\s*[:=]\s*)'[^'\r\n]*'/giu,
"$1$2$1$3'[REDACTED]'"
)
.replace(
/\b(api[-_ ]?key|token|secret|password|authorization)\b(\s*[:=]\s*|\s+)(["']?)[^\s"',}]+/giu,
'$1$2[REDACTED]'
)
}
export function safeToolErrorDetail(
value: unknown,
maximum = 2_000
): string | undefined {
if (!Number.isSafeInteger(maximum) || maximum < 1) {
return undefined
}
const parts: string[] = []
let remaining = maximum
const seen = new WeakSet<object>()
const append = (value: string): void => {
const text = [...value]
.filter((character) => {
const code = character.charCodeAt(0)
return (
code === 9 ||
code === 10 ||
code === 13 ||
(code > 31 && code !== 127)
)
})
.join('')
.trim()
if (!text || remaining <= 0) {
return
}
const separator = parts.length > 0 ? '\n' : ''
const available = Math.max(0, remaining - separator.length)
if (available === 0) {
return
}
const bounded = text.slice(0, available)
parts.push(`${separator}${bounded}`)
remaining -= separator.length + bounded.length
}
const collect = (candidate: unknown, depth = 0): void => {
if (remaining <= 0 || depth > 4 || candidate === undefined) {
return
}
if (typeof candidate === 'string') {
const boundedCandidate = candidate.slice(
0,
Math.min(candidate.length, remaining * 4)
)
append(boundedCandidate)
return
}
if (!candidate || typeof candidate !== 'object') {
return
}
if (seen.has(candidate)) {
return
}
seen.add(candidate)
if (Array.isArray(candidate)) {
for (const item of candidate.slice(0, 20)) {
collect(item, depth + 1)
}
return
}
const record = candidate as Record<string, unknown>
collect(record.message, depth + 1)
for (const key of [
'code',
'errno',
'syscall',
'hostname',
'address',
'port',
'status',
'statusCode'
]) {
const metadata = record[key]
if (
typeof metadata === 'string' ||
typeof metadata === 'number'
) {
append(`${key}: ${metadata}`)
}
}
if (record.cause !== undefined) {
append('cause:')
collect(record.cause, depth + 1)
}
for (const key of [
'content',
'error',
'errors',
'stderr',
'detail',
'data'
]) {
collect(record[key], depth + 1)
}
}
collect(value)
return parts.join('').trim() || undefined
}
export function safeToolArgumentSummary(
toolArguments: Record<string, unknown>,
preview?: unknown[],
maximum = 1_000
): string {
const previewText = preview
?.slice(0, 100)
.flatMap((item) => {
if (!item || typeof item !== 'object') {
return []
}
const content = (item as Record<string, unknown>).content
return typeof content === 'string' ? [content] : []
})
.join(' ')
.trim()
if (previewText) {
return redactSensitiveText(previewText).slice(0, maximum)
}
return JSON.stringify(
redactValue(toolArguments, new WeakSet())
).slice(0, maximum)
}
export function boundedToolDetail(
value: unknown,
maximum: number
): string | undefined {
if (!Number.isSafeInteger(maximum) || maximum < 1 || value === undefined) {
return undefined
}
let text: string | undefined
if (typeof value === 'string') {
text = value
} else {
try {
text = JSON.stringify(value, null, 2)
} catch {
return undefined
}
}
return text ? text.slice(0, maximum) : undefined
}