CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
shell-semantics.ts2355 linesDownload Raw Back to permissions
1/**2 * @license3 * Copyright 2025 Qwen team4 * SPDX-License-Identifier: Apache-2.05 */6 7/**8 * Shell command semantic analysis for permission matching.9 *10 * Analyzes simple shell commands to extract "virtual tool operations" so that11 * Read / Edit / Write / WebFetch / ListFiles permission rules can match their12 * shell equivalents and prevent bypass via the shell tool.13 *14 * @example15 *   extractShellOperations('cat /etc/passwd', '/home/user')16 *   // → [{ virtualTool: 'read_file', filePath: '/etc/passwd' }]17 *18 * @example19 *   extractShellOperations('curl https://example.com/api', '/home/user')20 *   // → [{ virtualTool: 'web_fetch', domain: 'example.com' }]21 *22 * @example23 *   extractShellOperations('echo hi > /etc/motd', '/home/user')24 *   // → [{ virtualTool: 'write_file', filePath: '/etc/motd' }]25 *26 * Known limitations (cannot be statically analysed):27 *   - Shell variable expansion: `cat $FILE`28 *   - Command substitution: `cat $(find .)`29 *   - Interpreter scripts: `python script.py`, `node x.js`30 *   - Pipe targets: `find . | xargs cat`31 *   - Complex dynamic expressions: `eval "cat $f"`32 */33 34import nodePath from 'node:path';35import os from 'node:os';36import { stripShellWrapper } from '../utils/shell-utils.js';37import { createDebugLogger } from '../utils/debugLogger.js';38import { splitCompoundCommand } from './rule-parser.js';39 40const shellSemanticsDebugLogger = createDebugLogger('SHELL_SEMANTICS');41 42// ─────────────────────────────────────────────────────────────────────────────43// Types44// ─────────────────────────────────────────────────────────────────────────────45 46/**47 * A virtual file or network operation extracted from a shell command.48 * Used to match Read / Edit / Write / WebFetch / ListFiles permission rules49 * against shell commands that perform equivalent operations.50 */51export interface ShellOperation {52  /**53   * The virtual tool this operation maps to.54   * Matches the canonical tool names used in the permission system.55   */56  virtualTool:57    | 'read_file'58    | 'list_directory'59    | 'edit'60    | 'write_file'61    | 'web_fetch'62    | 'grep_search';63  /** Absolute file or directory path (for file operations). */64  filePath?: string;65  /** Domain name without port (for web_fetch operations). */66  domain?: string;67  /**68   * True when this operation was extracted after a dynamic `cd` whose target69   * cannot be statically resolved. Consumers that enforce protected relative70   * paths should treat this as conservative signal, not as a concrete path.71   */72  cwdUnknown?: boolean;73  /**74   * True when `cwdUnknown` may affect the extracted file path. Absolute paths75   * do not depend on cwd; relative redirect/path arguments do.76   */77  pathMayDependOnCwd?: boolean;78}79 80// ─────────────────────────────────────────────────────────────────────────────81// Tokenizer82// ─────────────────────────────────────────────────────────────────────────────83 84/**85 * Tokenize a shell command string, respecting single/double quotes and86 * backslash escapes, splitting on unquoted whitespace.87 *88 * The input should be a single simple command (already split from compound89 * commands via `splitCompoundCommand`).90 */91function tokenize(command: string): string[] {92  const tokens: string[] = [];93  let current = '';94  let inSingle = false;95  let inDouble = false;96  let escaped = false;97 98  for (let i = 0; i < command.length; i++) {99    const ch = command[i]!;100 101    if (escaped) {102      current += ch;103      escaped = false;104      continue;105    }106    if (ch === '\\' && !inSingle) {107      escaped = true;108      continue;109    }110    if (ch === "'" && !inDouble) {111      inSingle = !inSingle;112      continue;113    }114    if (ch === '"' && !inSingle) {115      inDouble = !inDouble;116      continue;117    }118    if (!inSingle && !inDouble && (ch === ' ' || ch === '\t')) {119      if (current) {120        pushToken(tokens, current);121        current = '';122      }123      continue;124    }125    current += ch;126  }127  if (current) pushToken(tokens, current);128  return tokens;129}130 131function pushToken(tokens: string[], token: string): void {132  if (token === '{' || token === '}') return;133  const normalized = trimShellSyntax(token);134  if (normalized) tokens.push(normalized);135}136 137function trimShellSyntax(token: string): string {138  let start = 0;139  let end = token.length;140 141  while (start < end && token[start] === '(') {142    start++;143  }144  while (end > start) {145    const ch = token[end - 1];146    if (ch !== ')' && ch !== '&') break;147    end--;148  }149 150  return token.slice(start, end);151}152 153// ─────────────────────────────────────────────────────────────────────────────154// Path helpers155// ─────────────────────────────────────────────────────────────────────────────156 157/**158 * Resolve a path argument to an absolute POSIX-style path.159 * Handles `~` home-directory expansion and relative paths.160 *161 * Always returns paths with forward-slash separators so that the resolved162 * paths are consistent across platforms and compatible with picomatch / the163 * permission rule matching system.164 */165function resolvePath(p: string, cwd: string): string {166  // Normalize inputs to forward slashes for consistent cross-platform handling167  const normP = p.replace(/\\/g, '/');168  const normCwd = cwd.replace(/\\/g, '/');169 170  if (normP === '~' || normP.startsWith('~/')) {171    const homeDir = os.homedir().replace(/\\/g, '/');172    const rest = normP.slice(1); // '' or '/some/path'173    // nodePath.posix.join handles the rest correctly:174    // join('C:/Users/foo', '/.ssh/id_rsa') → 'C:/Users/foo/.ssh/id_rsa'175    return rest ? nodePath.posix.join(homeDir, rest) : homeDir;176  }177  if (isShellAbsolutePath(normP)) {178    return normP;179  }180  return nodePath.posix.join(normCwd, normP);181}182 183function isShellAbsolutePath(p: string): boolean {184  return p.startsWith('/') || /^[A-Za-z]:\//.test(p.replace(/\\/g, '/'));185}186 187function isEnvAssignmentToken(token: string): boolean {188  return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);189}190 191/**192 * Return true if a token looks like a file/directory path argument, as193 * opposed to a flag, shell variable, number, or script expression.194 */195function looksLikePath(s: string): boolean {196  if (!s) return false;197  // Shell variable references198  if (s.startsWith('$')) return false;199  // Flags200  if (s.startsWith('-')) return false;201  // Pure integers — likely a count/size/mode argument (e.g. -n 10, chmod 755)202  if (/^\d+$/.test(s)) return false;203  // Script-like expressions (awk/sed programs, brace expansions)204  if (s.includes('{') || s.includes('}')) return false;205  // URLs are handled separately by the web-fetch handlers206  if (s.includes('://')) return false;207  return true;208}209 210// ─────────────────────────────────────────────────────────────────────────────211// Redirect extraction212// ─────────────────────────────────────────────────────────────────────────────213 214interface RedirectResult {215  readFiles: string[];216  writeFiles: string[];217}218 219/**220 * A bash /dev/tcp/<host>/<port> or /dev/udp/... redirect target opens a221 * network socket, not a file. Such targets must not be reported as file222 * reads/writes.223 */224function isNetworkPseudoDevice(target: string): boolean {225  return /^\/dev\/(tcp|udp)\//.test(target);226}227 228/**229 * Extract I/O redirections from a token array.230 *231 * Modifies `tokens` in-place to remove redirect operators and their targets.232 * Returns the absolute paths of redirect targets as read / write operations.233 *234 * Handles:235 *   `> file`   `>> file`  `< file`   (with or without space)236 *   `2> file`  `2>> file` `&> file`  `&>> file`237 *   Combined forms: `>file`, `>>file`, `1>file`, `2>/dev/null`238 */239function extractRedirects(tokens: string[], cwd: string): RedirectResult {240  const readFiles: string[] = [];241  const writeFiles: string[] = [];242  const toRemove = new Set<number>();243 244  for (let i = 0; i < tokens.length; i++) {245    const tok = tokens[i]!;246 247    // ── Separate-token redirect operators ─────────────────────────────────248    if (tok === '>' || tok === '1>') {249      const target = tokens[i + 1];250      if (target && looksLikePath(target)) {251        if (!isNetworkPseudoDevice(target)) {252          writeFiles.push(resolvePath(target, cwd));253        }254        toRemove.add(i);255        toRemove.add(i + 1);256        i++;257      }258    } else if (tok === '>>' || tok === '1>>') {259      const target = tokens[i + 1];260      if (target && looksLikePath(target)) {261        if (!isNetworkPseudoDevice(target)) {262          writeFiles.push(resolvePath(target, cwd));263        }264        toRemove.add(i);265        toRemove.add(i + 1);266        i++;267      }268    } else if (tok === '<<' || tok === '<<-') {269      toRemove.add(i);270      if (tokens[i + 1]) {271        toRemove.add(i + 1);272        i++;273      }274    } else if (tok === '<') {275      const target = tokens[i + 1];276      if (target && looksLikePath(target)) {277        if (!isNetworkPseudoDevice(target)) {278          readFiles.push(resolvePath(target, cwd));279        }280        toRemove.add(i);281        toRemove.add(i + 1);282        i++;283      }284    } else if (tok === '2>' || tok === '2>>' || tok === '&>' || tok === '&>>') {285      // stderr / combined redirect — consume target286      const target = tokens[i + 1];287      if (target) {288        if (289          target !== '/dev/null' &&290          looksLikePath(target) &&291          !isNetworkPseudoDevice(target)292        ) {293          writeFiles.push(resolvePath(target, cwd));294        }295        toRemove.add(i);296        toRemove.add(i + 1);297        i++;298      }299    }300    // ── Combined redirect tokens without space: `>file`, `>>file`, etc. ───301    else {302      const m = tok.match(/^(<<-?|1>>|1>|>>|>|2>>|2>|&>>|&>|<)(.+)$/);303      if (m) {304        const op = m[1]!;305        const target = m[2]!;306        if (op.startsWith('<<')) {307          toRemove.add(i);308          continue;309        }310        if (311          target !== '/dev/null' &&312          looksLikePath(target) &&313          !isNetworkPseudoDevice(target)314        ) {315          if (op === '<') {316            readFiles.push(resolvePath(target, cwd));317          } else {318            writeFiles.push(resolvePath(target, cwd));319          }320        }321        toRemove.add(i);322      }323    }324  }325 326  // Remove redirect tokens from the array in-place327  const filtered = tokens.filter((_, idx) => !toRemove.has(idx));328  tokens.length = 0;329  tokens.push(...filtered);330 331  return { readFiles, writeFiles };332}333 334// ─────────────────────────────────────────────────────────────────────────────335// Argument parsing336// ─────────────────────────────────────────────────────────────────────────────337 338/**339 * Extract positional (non-flag) arguments from a token list.340 *341 * Flags starting with `-` are skipped. Flags listed in `flagsWithValue`342 * also consume the immediately following token (their value).343 */344function getPositionalArgs(345  args: string[],346  flagsWithValue: ReadonlySet<string> = new Set(),347): string[] {348  const positional: string[] = [];349  let skipNext = false;350 351  for (const arg of args) {352    if (skipNext) {353      skipNext = false;354      continue;355    }356    if (!arg.startsWith('-')) {357      positional.push(arg);358      continue;359    }360    const equalsIndex = arg.indexOf('=');361    if (equalsIndex > 0 && flagsWithValue.has(arg.slice(0, equalsIndex))) {362      continue;363    }364    // Flag: check if it consumes the next token365    if (flagsWithValue.has(arg)) {366      skipNext = true;367      continue;368    }369    for (const flag of flagsWithValue) {370      if (isAttachedShortFlagValue(arg, flag)) {371        break;372      }373      if (374        flag.startsWith('-') &&375        !flag.startsWith('--') &&376        flag.length === 2 &&377        hasCombinedShortFlag(arg, flag.slice(1))378      ) {379        skipNext = true;380        break;381      }382    }383    // Flags combined with their value in the same token (`-n10`) are ignored384    // because looksLikePath will filter out anything starting with `-`.385  }386 387  return positional;388}389 390function getFlagValue(391  args: string[],392  shortName: string,393  longName: string,394): string | undefined {395  for (let i = 0; i < args.length; i++) {396    const arg = args[i]!;397    if (arg === shortName || arg === longName) {398      return args[i + 1];399    }400    if (arg.startsWith(`${longName}=`)) {401      return arg.slice(longName.length + 1);402    }403    if (isAttachedShortFlagValue(arg, shortName)) {404      return arg.slice(shortName.length).replace(/^=/, '');405    }406    if (hasCombinedShortFlag(arg, shortName.slice(1))) {407      return args[i + 1];408    }409  }410  return undefined;411}412 413function targetDirectoryPath(args: string[], cwd: string): string | undefined {414  const target = getFlagValue(args, '-t', '--target-directory');415  if (!target || !looksLikePath(target)) return undefined;416  return resolvePath(target, cwd);417}418 419function targetDirectoryWrites(420  targetDir: string,421  sources: string[],422): ShellOperation[] {423  return sources.map((source) => ({424    virtualTool: 'write_file',425    filePath: nodePath.posix.join(426      targetDir,427      nodePath.posix.basename(source.replace(/\\/g, '/')),428    ),429  }));430}431 432function writeOpForFlag(433  args: string[],434  cwd: string,435  shortName: string,436  longName: string,437): ShellOperation | undefined {438  const target = getFlagValue(args, shortName, longName);439  if (!target || !looksLikePath(target)) return undefined;440  return { virtualTool: 'write_file', filePath: resolvePath(target, cwd) };441}442 443function hasCombinedShortFlag(arg: string, flag: string): boolean {444  return (445    arg.startsWith('-') && !arg.startsWith('--') && arg.slice(1).includes(flag)446  );447}448 449function isAttachedShortFlagValue(arg: string, flag: string): boolean {450  return (451    flag.startsWith('-') &&452    !flag.startsWith('--') &&453    flag.length === 2 &&454    arg.startsWith(flag) &&455    arg.length > flag.length456  );457}458 459// ─────────────────────────────────────────────────────────────────────────────460// Command handler helpers461// ─────────────────────────────────────────────────────────────────────────────462 463type CommandHandler = (args: string[], cwd: string) => ShellOperation[];464 465/** Build read_file operations from positional path arguments. */466function readOps(467  args: string[],468  cwd: string,469  flagsWithValue?: ReadonlySet<string>,470): ShellOperation[] {471  return getPositionalArgs(args, flagsWithValue)472    .filter(looksLikePath)473    .map((p) => ({474      virtualTool: 'read_file' as const,475      filePath: resolvePath(p, cwd),476    }));477}478 479/** Build list_directory operations from positional path arguments.480 *  Defaults to cwd when no path args are given. */481function listOps(482  args: string[],483  cwd: string,484  flagsWithValue?: ReadonlySet<string>,485): ShellOperation[] {486  const dirs = getPositionalArgs(args, flagsWithValue).filter(looksLikePath);487  if (dirs.length === 0)488    return [{ virtualTool: 'list_directory', filePath: cwd }];489  return dirs.map((p) => ({490    virtualTool: 'list_directory' as const,491    filePath: resolvePath(p, cwd),492  }));493}494 495/** Extract URL domain and return a web_fetch operation, or null on failure. */496function webOp(url: string): ShellOperation | null {497  try {498    const normalized = url.includes('://') ? url : `https://${url}`;499    const domain = new URL(normalized).hostname;500    return domain ? { virtualTool: 'web_fetch', domain } : null;501  } catch {502    return null;503  }504}505 506// ─────────────────────────────────────────────────────────────────────────────507// Command dispatch table508// ─────────────────────────────────────────────────────────────────────────────509 510const COMMANDS: Readonly<Record<string, CommandHandler>> = {511  // ── File-read commands ────────────────────────────────────────────────────512 513  cat: (a, d) => readOps(a, d),514  tac: (a, d) => readOps(a, d),515  nl: (a, d) => readOps(a, d),516  zcat: (a, d) => readOps(a, d),517  bzcat: (a, d) => readOps(a, d),518  xzcat: (a, d) => readOps(a, d),519  gzcat: (a, d) => readOps(a, d),520  lzcat: (a, d) => readOps(a, d),521  head: (a, d) => readOps(a, d, new Set(['-n', '-c', '--lines', '--bytes'])),522  tail: (a, d) =>523    readOps(524      a,525      d,526      new Set(['-n', '-c', '-s', '--lines', '--bytes', '--sleep-interval']),527    ),528  less: (a, d) =>529    readOps(530      a,531      d,532      new Set(['-b', '-h', '-j', '-p', '-x', '-y', '-z', '--shift', '--tabs']),533    ),534  more: (a, d) => readOps(a, d),535  most: (a, d) => readOps(a, d),536  wc: (a, d) => readOps(a, d),537  file: (a, d) =>538    readOps(539      a,540      d,541      new Set([542        '-m',543        '-e',544        '-F',545        '-P',546        '--magic-file',547        '--exclude',548        '--extension',549        '--separator',550      ]),551    ),552  stat: (a, d) =>553    readOps(554      a,555      d,556      new Set(['-c', '-f', '--format', '--printf', '--file-system']),557    ),558  readlink: (a, d) =>559    readOps(560      a,561      d,562      new Set([563        '-e',564        '-f',565        '-m',566        '-q',567        '-s',568        '-v',569        '-z',570        '--canonicalize',571        '--canonicalize-existing',572        '--canonicalize-missing',573        '--no-newline',574        '--quiet',575        '--silent',576        '--verbose',577        '--zero',578      ]),579    ),580  realpath: (a, d) =>581    readOps(582      a,583      d,584      new Set([585        '--relative-to',586        '--relative-base',587        '-e',588        '-m',589        '-s',590        '-z',591        '--canonicalize-existing',592        '--canonicalize-missing',593        '--logical',594        '--physical',595        '--no-symlinks',596        '--quiet',597        '--strip',598        '--zero',599      ]),600    ),601  diff: (a, d) =>602    readOps(603      a,604      d,605      new Set([606        '-u',607        '-U',608        '-c',609        '-C',610        '-I',611        '-x',612        '-X',613        '-W',614        '--label',615        '--to-file',616        '--from-file',617        '--width',618        '--horizon-lines',619        '--strip-trailing-cr',620        '--ignore-matching-lines',621        '--exclude',622        '--exclude-from',623      ]),624    ),625  diff3: (a, d) =>626    readOps(627      a,628      d,629      new Set([630        '-m',631        '-T',632        '-A',633        '-E',634        '-e',635        '-x',636        '-X',637        '-3',638        '-i',639        '--label',640      ]),641    ),642  sdiff: (a, d) =>643    readOps(644      a,645      d,646      new Set(['-o', '-w', '-W', '-s', '-i', '-b', '-B', '-E', '-H']),647    ),648  cmp: (a, d) =>649    readOps(650      a,651      d,652      new Set([653        '-i',654        '-l',655        '-n',656        '-s',657        '--ignore-initial',658        '--bytes',659        '--print-bytes',660        '--quiet',661        '--silent',662        '--verbose',663        '--zero',664      ]),665    ),666  md5sum: (a, d) => readOps(a, d),667  sha1sum: (a, d) => readOps(a, d),668  sha256sum: (a, d) => readOps(a, d),669  sha512sum: (a, d) => readOps(a, d),670  sha224sum: (a, d) => readOps(a, d),671  sha384sum: (a, d) => readOps(a, d),672  cksum: (a, d) => readOps(a, d),673  b2sum: (a, d) => readOps(a, d),674  sum: (a, d) => readOps(a, d),675  strings: (a, d) =>676    readOps(677      a,678      d,679      new Set([680        '-n',681        '-t',682        '-e',683        '-o',684        '-a',685        '--min-len',686        '--radix',687        '--encoding',688        '--file',689        '--print-file-name',690        '--data',691        '--all',692      ]),693    ),694  hexdump: (a, d) =>695    readOps(696      a,697      d,698      new Set([699        '-n',700        '-s',701        '-l',702        '-C',703        '-b',704        '-c',705        '-d',706        '-o',707        '-x',708        '-e',709        '-f',710        '-v',711      ]),712    ),713  xxd: (a, d) =>714    readOps(715      a,716      d,717      new Set([718        '-l',719        '-s',720        '-c',721        '-g',722        '-o',723        '-n',724        '-b',725        '-e',726        '-i',727        '-p',728        '-r',729        '-u',730        '-E',731      ]),732    ),733  od: (a, d) =>734    readOps(735      a,736      d,737      new Set([738        '-N',739        '-j',740        '-w',741        '-s',742        '-t',743        '-A',744        '-v',745        '--address-radix',746        '--endian',747        '--format',748        '--read-bytes',749        '--skip-bytes',750        '--strings',751        '--output-duplicates',752        '--width',753      ]),754    ),755  sort: (a, d) => {756    const output = writeOpForFlag(a, d, '-o', '--output');757    return [758      ...readOps(759        a,760        d,761        new Set([762          '-k',763          '-t',764          '-T',765          '--output',766          '-o',767          '--field-separator',768          '--key',769          '--temporary-directory',770          '--compress-program',771          '--batch-size',772          '--parallel',773          '--random-source',774          '--sort',775        ]),776      ),777      ...(output ? [output] : []),778    ];779  },780  uniq: (a, d) =>781    readOps(782      a,783      d,784      new Set([785        '-f',786        '-s',787        '-w',788        '-n',789        '--skip-fields',790        '--skip-chars',791        '--check-chars',792      ]),793    ),794  cut: (a, d) =>795    readOps(796      a,797      d,798      new Set([799        '-b',800        '-c',801        '-d',802        '-f',803        '--delimiter',804        '--fields',805        '--bytes',806        '--characters',807        '--output-delimiter',808      ]),809    ),810  paste: (a, d) =>811    readOps(a, d, new Set(['-d', '-s', '--delimiters', '--serial'])),812  join: (a, d) =>813    readOps(814      a,815      d,816      new Set([817        '-t',818        '-1',819        '-2',820        '-j',821        '-o',822        '-a',823        '-e',824        '--field',825        '--header',826        '--check-order',827        '--nocheck-order',828        '--zero-terminated',829      ]),830    ),831  column: (a, d) =>832    readOps(833      a,834      d,835      new Set([836        '-t',837        '-s',838        '-n',839        '-c',840        '-o',841        '-x',842        '--table',843        '--separator',844        '--output-separator',845        '--fillrows',846      ]),847    ),848  fold: (a, d) =>849    readOps(850      a,851      d,852      new Set(['-w', '-b', '-s', '--width', '--bytes', '--spaces']),853    ),854  expand: (a, d) => readOps(a, d, new Set(['-t', '--tabs', '--initial'])),855  unexpand: (a, d) =>856    readOps(a, d, new Set(['-t', '-a', '--tabs', '--all', '--first-only'])),857  base64: (a, d) =>858    readOps(859      a,860      d,861      new Set(['-d', '-i', '-w', '--decode', '--ignore-garbage', '--wrap']),862    ),863  base32: (a, d) =>864    readOps(865      a,866      d,867      new Set(['-d', '-i', '-w', '--decode', '--ignore-garbage', '--wrap']),868    ),869  tr: (a, d) => readOps(a, d),870 871  // ── Grep / search commands ────────────────────────────────────────────────872 873  grep: (args, cwd) => {874    const hasPatternFlag = args.some(875      (a) =>876        a === '-e' || a === '-f' || a.startsWith('-e') || a.startsWith('-f'),877    );878    const isRecursive = args.some((a) =>879      ['-r', '-R', '--recursive', '--dereference-recursive'].includes(a),880    );881    const flagsWithValue = new Set([882      '-e',883      '-f',884      '-m',885      '-A',886      '-B',887      '-C',888      '--context',889      '--include',890      '--exclude',891      '--exclude-dir',892      '--max-count',893      '--after-context',894      '--before-context',895      '-n',896      '--line-number',897      '--label',898      '-D',899      '--devices',900      '--max-depth',901      '-X',902      '--exclude-from',903    ]);904    const positional = getPositionalArgs(args, flagsWithValue).filter(905      looksLikePath,906    );907    // If -e/-f was used, there is no positional pattern; all positionals are paths.908    // Otherwise, the first positional is the pattern and the rest are paths.909    const filePaths = hasPatternFlag ? positional : positional.slice(1);910    const tool: 'read_file' | 'list_directory' = isRecursive911      ? 'list_directory'912      : 'read_file';913    return filePaths.map((p) => ({914      virtualTool: tool,915      filePath: resolvePath(p, cwd),916    }));917  },918  egrep: (a, d) => (COMMANDS['grep'] as CommandHandler)(a, d),919  fgrep: (a, d) => (COMMANDS['grep'] as CommandHandler)(a, d),920  zgrep: (a, d) => (COMMANDS['grep'] as CommandHandler)(a, d),921  bzgrep: (a, d) => (COMMANDS['grep'] as CommandHandler)(a, d),922 923  rg: (args, cwd) => {924    // ripgrep: recursive by default; first non-flag positional = pattern925    const hasPatternFlag = args.some((a) => a === '-e' || a === '-f');926    const flagsWithValue = new Set([927      '-e',928      '-f',929      '-m',930      '-A',931      '-B',932      '-C',933      '-t',934      '-T',935      '-g',936      '--iglob',937      '--glob',938      '--type',939      '--type-not',940      '--max-count',941      '--max-depth',942      '--context',943      '--after-context',944      '--before-context',945      '-M',946      '--max-columns',947      '--field-match-separator',948    ]);949    const positional = getPositionalArgs(args, flagsWithValue).filter(950      looksLikePath,951    );952    const filePaths = hasPatternFlag ? positional : positional.slice(1);953    return filePaths.map((p) => ({954      virtualTool: 'list_directory' as const,955      filePath: resolvePath(p, cwd),956    }));957  },958 959  ag: (args, cwd) => {960    const hasPatternFlag = args.some((a) => a === '-e');961    const flagsWithValue = new Set([962      '-e',963      '-m',964      '-A',965      '-B',966      '-C',967      '--depth',968      '--file-search-regex',969      '--file-search-regex-i',970      '--ignore',971      '--ignore-dir',972      '-n',973    ]);974    const positional = getPositionalArgs(args, flagsWithValue).filter(975      looksLikePath,976    );977    const filePaths = hasPatternFlag ? positional : positional.slice(1);978    return filePaths.map((p) => ({979      virtualTool: 'list_directory' as const,980      filePath: resolvePath(p, cwd),981    }));982  },983 984  ack: (args, cwd) => {985    const flagsWithValue = new Set([986      '-m',987      '-A',988      '-B',989      '-C',990      '--type',991      '--ignore-dir',992      '--ignore-file',993      '--ignore-directory',994      '-n',995    ]);996    // ack: first positional = pattern, rest = paths997    const positional = getPositionalArgs(args, flagsWithValue).filter(998      looksLikePath,999    );1000    return positional.slice(1).map((p) => ({1001      virtualTool: 'list_directory' as const,1002      filePath: resolvePath(p, cwd),1003    }));1004  },1005 1006  // ── Directory-listing commands ────────────────────────────────────────────1007 1008  ls: (a, d) => listOps(a, d),1009  dir: (a, d) => listOps(a, d),1010  vdir: (a, d) => listOps(a, d),1011  exa: (a, d) =>1012    listOps(1013      a,1014      d,1015      new Set([1016        '-L',1017        '--level',1018        '--sort',1019        '--color',1020        '--colour',1021        '--group',1022        '-I',1023        '--ignore-glob',1024      ]),1025    ),1026  eza: (a, d) =>1027    listOps(1028      a,1029      d,1030      new Set([1031        '-L',1032        '--level',1033        '--sort',1034        '--color',1035        '--colour',1036        '--group',1037        '-I',1038        '--ignore-glob',1039      ]),1040    ),1041  lsd: (a, d) =>1042    listOps(1043      a,1044      d,1045      new Set([1046        '--depth',1047        '--color',1048        '--icon',1049        '--icon-theme',1050        '--date',1051        '--size',1052        '--blocks',1053        '--header',1054        '--classic',1055        '--no-symlink',1056        '--ignore-glob',1057        '-I',1058      ]),1059    ),1060 1061  find: (args, cwd) => {1062    // `find [starting-point...] [expression]`1063    // Starting points come before any expression keyword beginning with `-` or `(`.1064    const expressionKeywords = new Set([1065      '-name',1066      '-iname',1067      '-path',1068      '-ipath',1069      '-regex',1070      '-iregex',1071      '-type',1072      '-maxdepth',1073      '-mindepth',1074      '-newer',1075      '-mtime',1076      '-atime',1077      '-ctime',1078      '-size',1079      '-user',1080      '-group',1081      '-perm',1082      '-links',1083      '-inum',1084      '-exec',1085      '-execdir',1086      '-ok',1087      '-okdir',1088      '-print',1089      '-print0',1090      '-ls',1091      '-delete',1092      '-prune',1093      '-depth',1094      '-empty',1095      '-readable',1096      '-writable',1097      '-executable',1098      '-follow',1099      '-xdev',1100      '-mount',1101      '-true',1102      '-false',1103      '-not',1104      '!',1105      '-a',1106      '-and',1107      '-o',1108      '-or',1109    ]);1110    const startingPoints: string[] = [];1111    for (const arg of args) {1112      if (1113        arg.startsWith('-') ||1114        arg === '(' ||1115        arg === ')' ||1116        expressionKeywords.has(arg)1117      )1118        break;1119      if (looksLikePath(arg)) startingPoints.push(resolvePath(arg, cwd));1120    }1121    if (startingPoints.length === 0) {1122      return [1123        { virtualTool: 'list_directory', filePath: cwd },1124        ...extractFindExecOps(args, cwd),1125      ];1126    }1127    return [1128      ...startingPoints.map((p) => ({1129        virtualTool: 'list_directory' as const,1130        filePath: p,1131      })),1132      ...extractFindExecOps(args, cwd),1133    ];1134  },1135 1136  tree: (args, cwd) =>1137    listOps(1138      args,1139      cwd,1140      new Set([1141        '-L',1142        '-P',1143        '-I',1144        '-o',1145        '-n',1146        '-H',1147        '-T',1148        '--charset',1149        '--filelimit',1150        '--matchdirs',1151        '--dirsfirst',1152        '-J',1153        '-X',1154        '--du',1155        '--si',1156      ]),1157    ),1158 1159  du: (args, cwd) =>1160    listOps(1161      args,1162      cwd,1163      new Set([1164        '-d',1165        '--max-depth',1166        '--threshold',1167        '-t',1168        '--block-size',1169        '-B',1170        '--time-style',1171        '--exclude',1172        '-X',1173        '--time',1174        '--output',1175      ]),1176    ),1177 1178  // ── File-write commands (create or overwrite) ─────────────────────────────1179 1180  touch: (args, cwd) =>1181    getPositionalArgs(1182      args,1183      new Set(['-t', '-r', '--reference', '--date', '-d', '--time']),1184    )1185      .filter(looksLikePath)1186      .map((p) => ({1187        virtualTool: 'write_file' as const,1188        filePath: resolvePath(p, cwd),1189      })),1190 1191  mkdir: (args, cwd) =>1192    getPositionalArgs(args, new Set(['-m', '--mode', '-Z', '--context']))1193      .filter(looksLikePath)1194      .map((p) => ({1195        virtualTool: 'write_file' as const,1196        filePath: resolvePath(p, cwd),1197      })),1198 1199  mkfifo: (args, cwd) =>1200    getPositionalArgs(args, new Set(['-m', '--mode', '-Z']))

Showing the first 1,200 of 2355 lines. Download the file for the rest.

basant307/AI_Governance_Project · CoolFace