Menu
popagent
publicLatest change 1ce5ab5f17eefb3cef070f9d98b28f2bb1e5c879 - Secure agent secrets behind origin-bound browser use by AkurAI Build
import type { ChatMessage } from "./api-types";
type TextPart = { type: "text"; text: string } & Record<string, unknown>;
function isTextPart(part: unknown): part is TextPart {
return typeof part === "object" && part !== null
&& "type" in part && "text" in part
&& part.type === "text" && typeof part.text === "string";
}
export function latestUserText(messages: ChatMessage[]): string | undefined {
for (let index = messages.length - 1; index >= 0; index--) {
const message = messages[index];
if (!message || message.role !== "user") continue;
const text = message.parts.filter(isTextPart).map((part) => part.text).join("\n").trim();
if (text) return text;
}
}
export function replaceLatestUserText(messages: ChatMessage[], replacement: string): void {
for (let index = messages.length - 1; index >= 0; index--) {
const message = messages[index];
if (!message || message.role !== "user") continue;
let replaced = false;
message.parts = message.parts.filter((part) => {
if (!isTextPart(part)) return true;
if (replaced) return false;
part.text = replacement;
replaced = true;
return true;
});
return;
}
}
const PLAINTEXT_SECRET_TOOL_NAMES: Record<string, true> = {
storeSecret: true,
recallSecret: true,
updateSecret: true,
"store-secret": true,
"recall-secret": true,
"update-secret": true,
};
const REDACTED = "[redacted]";
export function isPlaintextSecretToolName(value: unknown): boolean {
if (typeof value !== "string") return false;
const name = value.startsWith("tool-") ? value.slice(5) : value;
return PLAINTEXT_SECRET_TOOL_NAMES[name] === true;
}
function sanitizeSensitiveToolValue(value: unknown): { value: unknown; changed: boolean } {
if (Array.isArray(value)) {
let changed = false;
const items = value.map((item) => {
const sanitized = sanitizeSensitiveToolValue(item);
changed ||= sanitized.changed;
return sanitized.value;
});
return { value: changed ? items : value, changed };
}
if (!value || typeof value !== "object") return { value, changed: false };
const record = value as Record<string, unknown>;
const invocation = record.toolInvocation && typeof record.toolInvocation === "object"
? record.toolInvocation as Record<string, unknown>
: undefined;
const sensitive = isPlaintextSecretToolName(record.type)
|| isPlaintextSecretToolName(record.toolName)
|| isPlaintextSecretToolName(invocation?.toolName);
if (sensitive) {
const sanitized = { ...record };
for (const key of ["input", "output", "args", "result", "rawInput", "rawOutput", "value"]) {
if (key in sanitized) sanitized[key] = REDACTED;
}
if ("errorText" in sanitized) sanitized.errorText = "Sensitive tool failed";
if (invocation) {
const safeInvocation = { ...invocation };
for (const key of ["input", "output", "args", "result", "rawInput", "rawOutput", "value"]) {
if (key in safeInvocation) safeInvocation[key] = REDACTED;
}
if ("errorText" in safeInvocation) safeInvocation.errorText = "Sensitive tool failed";
sanitized.toolInvocation = safeInvocation;
}
return { value: sanitized, changed: true };
}
let changed = false;
const sanitized: Record<string, unknown> = {};
for (const [key, item] of Object.entries(record)) {
const result = sanitizeSensitiveToolValue(item);
sanitized[key] = result.value;
changed ||= result.changed;
}
return { value: changed ? sanitized : value, changed };
}
export function sanitizeSensitiveToolMessages<Message>(messages: Message[]): Message[] {
const sanitized = sanitizeSensitiveToolValue(messages);
return sanitized.changed ? sanitized.value as Message[] : messages;
}