380 lines · 14.3 KB · TypeScript
RawDownload
import type {  ProviderApprovalDecision,  ProviderApprovalOption,  ProviderUserInputAnswers,  UserInputQuestion,} from "@t3tools/contracts";import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview";import * as Option from "effect/Option";import * as Predicate from "effect/Predicate";import * as Schema from "effect/Schema";import * as EffectAcpSchema from "effect-acp/schema"; import type { AcpToolCallState } from "./AcpRuntimeModel.ts"; const TOOL_TEXT_LIMIT = 8_000;const TOOL_TEXT_TRUNCATED = "[Earlier output truncated]\n\n";const QUESTION_LABEL_LIMIT = 512; const NativeToolFields = Schema.Struct({  command: Schema.optional(Schema.String),  CommandLine: Schema.optional(Schema.String),  command_line: Schema.optional(Schema.String),  commandLine: Schema.optional(Schema.String),  cwd: Schema.optional(Schema.String),  Cwd: Schema.optional(Schema.String),  WorkingDirectory: Schema.optional(Schema.String),  working_dir: Schema.optional(Schema.String),  workingDir: Schema.optional(Schema.String),  combinedOutput: Schema.optional(Schema.String),  combined_output: Schema.optional(Schema.String),  exitCode: Schema.optional(Schema.Int),  exit_code: Schema.optional(Schema.Int),  imagePath: Schema.optional(Schema.String),});const decodeNativeToolFields = Schema.decodeUnknownOption(NativeToolFields);const decodeSingleAnswer = Schema.decodeUnknownOption(  Schema.Union([Schema.String, Schema.Tuple([Schema.String])]),);const decodeToolCallContent = Schema.decodeUnknownOption(EffectAcpSchema.ToolCallContent); /** Native questions share the permission method, but their choices are not approvals. */export function isAntigravityUserInputRequest(  request: EffectAcpSchema.RequestPermissionRequest,): boolean {  return request.toolCall.toolCallId.startsWith("interaction_");} export function selectAntigravityPermissionOptionId(  request: EffectAcpSchema.RequestPermissionRequest,  decision: ProviderApprovalDecision,): string | undefined {  if (decision === "cancel" || isAntigravityUserInputRequest(request)) {    return undefined;  }  const kind =    decision === "accept" ? "allow_once" : decision === "decline" ? "reject_once" : "allow_always";  const option = request.options.find((entry) => entry.kind === kind);  return option?.optionId.trim() ? option.optionId : undefined;} /** Copy truncated text so V8 cannot retain the original large string. */const SECURITY_WARNING_META_KEY = "agy.security.warning";const WARNING_TEXT_LIMIT = 512;const decodeSecurityWarning = Schema.decodeUnknownOption(  Schema.Struct({    title: Schema.optional(Schema.String),    message: Schema.optional(Schema.String),  }),); /** * The agent marks "Allow Always" on shell and web tools with a prompt injection * warning in `_meta`. Surface it as option text so both clients can show it. */function antigravitySecurityWarning(option: EffectAcpSchema.PermissionOption): string | undefined {  const meta = option._meta;  if (!Predicate.isObject(meta)) return undefined;  const warning = Option.getOrUndefined(decodeSecurityWarning(meta[SECURITY_WARNING_META_KEY]));  const text = warning?.message?.trim() || warning?.title?.trim();  if (!text) return undefined;  return text.length > WARNING_TEXT_LIMIT    ? copyBoundedText(`${text.slice(0, WARNING_TEXT_LIMIT - 3)}...`)    : text;} /** Only advertise decisions that the native request can honor. */export function antigravityApprovalOptions(  request: EffectAcpSchema.RequestPermissionRequest,): ReadonlyArray<ProviderApprovalOption> {  if (isAntigravityUserInputRequest(request)) return [];  const options: ProviderApprovalOption[] = [];  const optionWithKind = (kind: EffectAcpSchema.PermissionOption["kind"]) =>    request.options.find((entry) => entry.kind === kind && entry.optionId.trim());  const once = optionWithKind("allow_once");  if (once) {    options.push({ decision: "accept", label: "Allow once" });  }  const always = optionWithKind("allow_always");  if (always) {    const warning = antigravitySecurityWarning(always);    options.push({      decision: "acceptForSession",      label: "Allow for this thread",      ...(warning ? { warning } : {}),    });  }  if (optionWithKind("reject_once")) {    options.push({ decision: "decline", label: "Deny" });  }  options.push({ decision: "cancel", label: "Cancel" });  return options;} function copyBoundedText(text: string): string {  return Buffer.from(text, "utf16le").toString("utf16le");} function questionLabel(option: EffectAcpSchema.PermissionOption): string {  const label = option.name.trim() || option.optionId;  return label.length > QUESTION_LABEL_LIMIT    ? copyBoundedText(`${label.slice(0, QUESTION_LABEL_LIMIT - 3)}...`)    : label;} export function extractAntigravityUserInputQuestion(  request: EffectAcpSchema.RequestPermissionRequest,): UserInputQuestion | undefined {  if (!isAntigravityUserInputRequest(request) || request.options.length === 0) {    return undefined;  }  const ids = new Set<string>();  for (const option of request.options) {    if (!option.optionId.trim() || ids.has(option.optionId)) {      return undefined;    }    ids.add(option.optionId);  }  const question = request.toolCall.title?.trim() || "Choose an option.";  return {    id: request.toolCall.toolCallId,    header: "Question",    question:      question.length > TOOL_TEXT_LIMIT        ? copyBoundedText(`${question.slice(0, TOOL_TEXT_LIMIT - 3)}...`)        : question,    multiSelect: false,    allowCustomAnswer: false,    options: request.options.map((option) => ({      value: option.optionId,      label: questionLabel(option),      description: questionLabel(option),    })),  };} /** Return undefined for an invalid answer so the adapter keeps the question open. */export function makeAntigravityUserInputResponse(  request: EffectAcpSchema.RequestPermissionRequest,  answers: ProviderUserInputAnswers,): EffectAcpSchema.RequestPermissionResponse | undefined {  if (extractAntigravityUserInputQuestion(request) === undefined) {    return undefined;  }  const answer = Option.getOrUndefined(decodeSingleAnswer(answers[request.toolCall.toolCallId]));  const value = typeof answer === "string" ? answer : answer?.[0];  if (value === undefined) {    return undefined;  }  const exact = request.options.find((option) => option.optionId === value);  if (exact) {    return { outcome: { outcome: "selected", optionId: exact.optionId } };  }  const matchingLabels = request.options.filter((option) => questionLabel(option) === value);  const option = matchingLabels.length === 1 ? matchingLabels[0] : undefined;  return option ? { outcome: { outcome: "selected", optionId: option.optionId } } : undefined;} function boundText(text: string, limit = TOOL_TEXT_LIMIT): string {  return text.length <= limit    ? text    : copyBoundedText(`${TOOL_TEXT_TRUNCATED}${text.slice(-limit)}`);} interface ToolPayloadBudget {  nodes: number;  text: number;} function sanitizeToolValue(value: unknown, budget: ToolPayloadBudget, depth: number): unknown {  if (depth > 12 || budget.nodes-- <= 0) {    return undefined;  }  if (typeof value === "string") {    if (/^data:image\//i.test(value) || budget.text <= 0) {      return undefined;    }    const text = boundText(value, Math.min(TOOL_TEXT_LIMIT, budget.text));    budget.text -= text.length;    return text;  }  if (Array.isArray(value)) {    const result: unknown[] = [];    for (const entry of value) {      if (budget.nodes <= 0) break;      const sanitized = sanitizeToolValue(entry, budget, depth + 1);      if (sanitized !== undefined) result.push(sanitized);    }    return result;  }  if (!Predicate.isObject(value)) {    return value;  }  const entries: Array<readonly [string, unknown]> = [];  for (const [key, entry] of Object.entries(value)) {    if (budget.nodes <= 0) break;    if (      (value.type === "image" && (key === "data" || key === "blob")) ||      (key === "blob" &&        typeof value.mimeType === "string" &&        value.mimeType.startsWith("image/")) ||      ((key === "formatted_output" || key === "formattedOutput") &&        (entry === value.combinedOutput || entry === value.combined_output))    ) {      continue;    }    const sanitized = sanitizeToolValue(entry, budget, depth + 1);    if (sanitized !== undefined) entries.push([key, sanitized]);  }  return Object.fromEntries(entries);} /** Bound both retained raw events and display data before they enter the event stream. */export function sanitizeAntigravityToolPayload(payload: unknown): unknown {  return sanitizeToolValue(payload, { nodes: 512, text: 64_000 }, 0);} /** The runtime uses this before it retains tool state or dispatches raw callbacks. */export function normalizeAntigravitySessionUpdate(  notification: EffectAcpSchema.SessionNotification,): EffectAcpSchema.SessionNotification {  const update = notification.update;  if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {    return notification;  }  const contentBudget = { nodes: 512, text: 32_000 };  const content = update.content?.flatMap((entry) => {    const decoded = Option.getOrUndefined(      decodeToolCallContent(sanitizeToolValue(entry, contentBudget, 0)),    );    return decoded === undefined ? [] : [decoded];  });  const meta = sanitizeAntigravityToolPayload(update._meta);  return {    ...notification,    update: {      ...update,      ...(typeof update.title === "string" ? { title: boundText(update.title) } : {}),      ...(update.rawInput !== undefined        ? { rawInput: sanitizeAntigravityToolPayload(update.rawInput) }        : {}),      ...(update.rawOutput !== undefined        ? { rawOutput: sanitizeAntigravityToolPayload(update.rawOutput) }        : {}),      ...(update.content !== undefined ? { content: content ?? [] } : {}),      ...(update._meta !== undefined ? { _meta: Predicate.isObject(meta) ? meta : null } : {}),    },  };} function localImagePath(imagePath: string | undefined): string | undefined {  if (!imagePath || imagePath.length > TOOL_TEXT_LIMIT) {    return undefined;  }  const path = imagePath.trim();  if (!isWorkspaceImagePreviewPath(path)) {    return undefined;  }  if (/^file:\/\//i.test(path)) {    try {      const url = new URL(path);      if (url.hostname && url.hostname !== "localhost") return undefined;      const pathname = decodeURIComponent(url.pathname);      return /^\/[a-z]:\//i.test(pathname) ? pathname.slice(1) : pathname;    } catch {      return undefined;    }  }  return /^[a-z][a-z\d+.-]*:/i.test(path) && !/^[a-z]:[\\/]/i.test(path) ? undefined : path;} export function normalizeAntigravityToolCall(toolCall: AcpToolCallState): AcpToolCallState {  const input = Option.getOrUndefined(decodeNativeToolFields(toolCall.data.rawInput));  const output = Option.getOrUndefined(decodeNativeToolFields(toolCall.data.rawOutput));  const nativeCommand =    input?.CommandLine ??    input?.command_line ??    input?.commandLine ??    input?.command ??    output?.commandLine ??    output?.command_line ??    toolCall.command;  const command = nativeCommand?.trim() ? boundText(nativeCommand.trim()) : undefined;  const nativeCwd =    input?.Cwd ??    input?.WorkingDirectory ??    input?.working_dir ??    input?.workingDir ??    input?.cwd ??    output?.workingDir ??    output?.working_dir;  const cwd = nativeCwd?.trim() ? boundText(nativeCwd.trim()) : undefined;  const nativeOutput = output?.combinedOutput ?? output?.combined_output;  const aggregatedOutput = nativeOutput === undefined ? undefined : boundText(nativeOutput);  const exitCode = output?.exitCode ?? output?.exit_code;  const imagePath = localImagePath(output?.imagePath);  const sanitizedData = sanitizeAntigravityToolPayload(toolCall.data);  const data: Record<string, unknown> = Predicate.isObject(sanitizedData) ? sanitizedData : {};  const kind = toolCall.kind ?? (command !== undefined ? "execute" : undefined);  if (kind !== undefined) data.kind = kind;  if (command !== undefined) data.command = command;  if (cwd !== undefined) data.cwd = cwd;  if (imagePath !== undefined) data.imagePath = imagePath;  if (kind === "execute") {    data.item = {      ...(Predicate.isObject(data.item) ? data.item : {}),      ...(command !== undefined ? { command } : {}),      ...(cwd !== undefined ? { cwd } : {}),      ...(aggregatedOutput !== undefined ? { aggregatedOutput } : {}),      ...(exitCode !== undefined ? { exitCode } : {}),    };  }  return {    ...toolCall,    ...(kind !== undefined ? { kind } : {}),    ...(command !== undefined ? { command } : {}),    ...(toolCall.title !== undefined ? { title: boundText(toolCall.title) } : {}),    ...(command !== undefined      ? { detail: command }      : toolCall.detail !== undefined        ? { detail: boundText(toolCall.detail) }        : {}),    data,  };} /** Only commands still running after end_turn become background tasks. */export function isAntigravityOpenCommand(toolCall: AcpToolCallState): boolean {  return toolCall.kind === "execute" && toolCall.status === "inProgress";} /** ACP 1.1.1 exposes subagent invocations as ordinary tools, without child IDs or models. */export function classifyAntigravitySubagentToolCall(  toolCall: AcpToolCallState,  rawPayload: unknown,): "subagent" | "mcp" | undefined {  if (    (toolCall.kind !== undefined && toolCall.kind !== "other") ||    (toolCall.title !== "Running start_subagent" && toolCall.title !== "Run start_subagent?")  )    return undefined;  const update = Predicate.isObject(rawPayload) ? rawPayload.update : undefined;  const meta = Predicate.isObject(update) ? update._meta : undefined;  return Predicate.isObject(meta) && meta.is_mcp_tool_call === true ? "mcp" : "subagent";} /** History sends a completed start before the separate result and its final status. */export function isAntigravitySubagentReplayStart(rawPayload: unknown): boolean {  const update = Predicate.isObject(rawPayload) ? rawPayload.update : undefined;  return (    Predicate.isObject(update) &&    update.sessionUpdate === "tool_call" &&    update.status === "completed" &&    (update.rawOutput === undefined || update.rawOutput === null)  );} export function antigravitySubagentOutput(toolCall: AcpToolCallState): string | undefined {  const output = toolCall.data.rawOutput;  return typeof output === "string" && output.trim() ? boundText(output.trim()) : undefined;}