AkurAI Build
Menu

popagent

public

Latest 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;
}