File size: 3,860 Bytes
7a1ad33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | /**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { SHELL_TOOL_NAME } from '../tools/definitions/base-declarations.js';
const WORKFLOW_PART_SEPARATOR = ' | ';
const TOOL_SEQUENCE_SEPARATOR = ' -> ';
const SHELL_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=/;
const SAFE_COMMAND_NAME_REGEX = /^[A-Za-z0-9_.@+-]+$/;
const SAFE_TOOL_SEQUENCE_ENTRY_REGEX = /^[A-Za-z_][A-Za-z0-9_:.]*$/;
function tokenizeShellCommand(command: string): string[] {
const tokens: string[] = [];
let currentToken = '';
let quote: '"' | "'" | '`' | undefined;
for (let i = 0; i < command.length; i++) {
const char = command[i];
if (quote) {
if (char === quote) {
quote = undefined;
continue;
}
if (quote === '"' && char === '\\' && i + 1 < command.length) {
currentToken += command[i + 1];
i++;
continue;
}
currentToken += char;
continue;
}
if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
if (currentToken) {
tokens.push(currentToken);
currentToken = '';
}
continue;
}
if (char === '"' || char === "'" || char === '`') {
quote = char;
continue;
}
currentToken += char;
}
if (currentToken) {
tokens.push(currentToken);
}
return tokens;
}
function getSafeCommandName(token: string): string | undefined {
if (!token || SHELL_ASSIGNMENT_REGEX.test(token)) {
return undefined;
}
const pathParts = token.split(/[/\\]/).filter(Boolean);
const basename = pathParts[pathParts.length - 1] ?? token;
if (!basename || basename.includes('://')) {
return 'shell';
}
return SAFE_COMMAND_NAME_REGEX.test(basename) ? basename : 'shell';
}
export function summarizeShellCommandForScratchpad(
command: string,
): string | undefined {
const normalized = command.replace(/\s+/g, ' ').trim();
if (normalized.length === 0) {
return undefined;
}
for (const token of tokenizeShellCommand(normalized)) {
const commandName = getSafeCommandName(token);
if (commandName) {
return commandName;
}
}
return undefined;
}
function sanitizeWorkflowToolSequenceEntry(entry: string): string | undefined {
const trimmed = entry.trim();
if (!trimmed) {
return undefined;
}
const shellPrefix = `${SHELL_TOOL_NAME}:`;
if (trimmed.startsWith(shellPrefix)) {
const command = trimmed.slice(shellPrefix.length).trim();
const commandSummary = summarizeShellCommandForScratchpad(command);
return commandSummary
? `${SHELL_TOOL_NAME}: ${commandSummary}`
: SHELL_TOOL_NAME;
}
if (
trimmed === SHELL_TOOL_NAME ||
SAFE_TOOL_SEQUENCE_ENTRY_REGEX.test(trimmed)
) {
return trimmed;
}
return undefined;
}
export function sanitizeWorkflowSummaryForScratchpad(summary: string): string {
const normalized = summary.replace(/\s+/g, ' ').trim();
if (!normalized.includes(`${SHELL_TOOL_NAME}:`)) {
return normalized;
}
const sanitizedParts: string[] = [];
for (const part of normalized.split(WORKFLOW_PART_SEPARATOR)) {
const trimmed = part.trim();
if (!trimmed) {
continue;
}
if (trimmed.includes(`${SHELL_TOOL_NAME}:`)) {
const sanitizedToolSequence = trimmed
.split(TOOL_SEQUENCE_SEPARATOR)
.map(sanitizeWorkflowToolSequenceEntry)
.filter((entry): entry is string => Boolean(entry));
if (sanitizedToolSequence.length > 0) {
sanitizedParts.push(
sanitizedToolSequence.join(TOOL_SEQUENCE_SEPARATOR),
);
}
continue;
}
if (
trimmed.startsWith('paths ') ||
trimmed === 'validated' ||
trimmed === 'validation failed'
) {
sanitizedParts.push(trimmed);
}
}
return sanitizedParts.join(WORKFLOW_PART_SEPARATOR);
}
|