CoolFace
Modelpublic

Felipe97/llama-cpp-compiled

sourceHugging Faceupdated 2d agoView on Hugging Face
0likes1.1kdownloads
server-tools.cpp2173 linesDownload Raw Back to server
1#include "server-tools.h"2 3#include "subproc.h"4#include "base64.hpp"5 6#include <filesystem>7#include <fstream>8#include <regex>9#include <thread>10#include <chrono>11#include <atomic>12#include <cstring>13#include <cctype>14#include <cstdint>15#include <cstdlib>16#include <algorithm>17#include <iterator>18#include <unordered_set>19#include <tuple>20#include <functional>21#include <memory>22#include <mutex>23 24#if defined(_WIN32)25#   ifndef NOMINMAX26#       define NOMINMAX27#   endif28#   include <windows.h>29#   include <fcntl.h>30#   include <io.h>31#else32#   include <cerrno>33#   include <unistd.h>34#endif35 36namespace fs = std::filesystem;37 38//39// internal helpers40//41 42// a child process writes in the OEM code page, so accented output would reach43// the JSON layer as invalid bytes. run() spawns without a console, so the44// console code page never applies45static std::string console_output_to_utf8(const std::string & text) {46#if defined(_WIN32)47    // a chunk can end mid sequence, so the incomplete tail is dropped first48    if (text.empty() || is_valid_utf8(text.substr(0, validate_utf8(text)))) {49        // never decode twice a child that already emits UTF-850        return text;51    }52 53    const UINT cp = GetOEMCP();54 55    // fail rather than emit replacement characters when the code page is wrong56    const int wide_len = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, text.data(), (int) text.size(), nullptr, 0);57    if (wide_len <= 0) {58        return text;59    }60    std::wstring wide(wide_len, L'\0');61    MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, text.data(), (int) text.size(), wide.data(), wide_len);62 63    const int utf8_len = WideCharToMultiByte(CP_UTF8, 0, wide.data(), wide_len, nullptr, 0, nullptr, nullptr);64    if (utf8_len <= 0) {65        return text;66    }67    std::string utf8(utf8_len, '\0');68    WideCharToMultiByte(CP_UTF8, 0, wide.data(), wide_len, utf8.data(), utf8_len, nullptr, nullptr);69    return utf8;70#else71    return text;72#endif73}74 75json server_tool::to_json() const {76    return {77        {"display_name", display_name},78        {"tool", name},79        {"type", type()},80        {"permissions", json{81            {"write", permission_write}82        }},83        {"uses_cwd", uses_cwd},84        {"definition", get_definition()},85    };86}87 88static constexpr size_t SERVER_TOOL_GIT_LS_FILES_MAX_OUTPUT = 8 * 1024 * 1024; // 8 MB89// budget for one listing call, shared by the git and walker paths90static constexpr int SERVER_TOOL_LIST_ENTRIES_TIMEOUT = 15; // seconds91 92// entry kinds a directory listing may return93enum class list_kind {94    files, // regular files only95    dirs,  // directories only96    all,   // both97};98 99// a narrow path uses the active code page on Windows, so every crossing between100// a std::string (always UTF-8 here) and fs::path is converted explicitly101static fs::path path_from_utf8(const std::string & s) {102    return fs::u8path(s);103}104 105// '/' separators on every platform: Windows accepts them, the web UI needs them106static std::string path_to_utf8(const fs::path & p) {107    const auto s = p.generic_u8string();108    return std::string(s.begin(), s.end());109}110 111// home directory, read once at first use (getenv is not thread safe against setenv)112static const std::string & home_dir() {113    static const std::string home = [] {114#ifdef _WIN32115        // the narrow getenv would return the profile path in the active code page116        const wchar_t * w = _wgetenv(L"HOME");117        if (w == nullptr) w = _wgetenv(L"USERPROFILE");118        return w ? path_to_utf8(fs::path(w)) : std::string();119#else120        const char * h = getenv("HOME");121        return h ? std::string(h) : std::string();122#endif123    }();124    return home;125}126 127static std::string expand_home(const std::string & path) {128    if (path.empty() || path[0] != '~') return path;129    if (path.size() > 1 && path[1] != '/' && path[1] != '\\') return path;130    const std::string & home = home_dir();131    if (home.empty()) return path;132    return home + path.substr(1);133}134 135// depth of a '/'-separated relative path: "a/b/c" is 3136static int entry_depth(const std::string & rel) {137    return 1 + (int) std::count(rel.begin(), rel.end(), '/');138}139 140// directories that a listing reports but never descends into: they can be enormous141// lowercase only, the local walker case-folds a name before the lookup142static const char * const SERVER_TOOL_JUNK_DIR_NAMES[] = {143    ".git", ".svn", ".hg", "node_modules", "__pycache__",144    ".venv", "venv", "dist", "build", "target", ".cache", ".idea", ".vscode",145};146 147class tools_io {148public:149    struct exec_result {150        std::string output;151        int  exit_code = -1;152        bool timed_out = false;153    };154 155    virtual ~tools_io() = default;156 157    virtual bool is_directory(const std::string & path) const = 0;158    virtual bool is_regular_file(const std::string & path) const = 0;159    virtual bool file_size(const std::string & path, uintmax_t & out_size) const = 0;160    virtual bool read_file(const std::string & path, std::string & out) const = 0;161    virtual bool write_file(const std::string & path, const std::string & content) const = 0;162    // resolve `path` against the IO's working directory; absolute paths are returned unchanged163    virtual std::string resolve(const std::string & path) const = 0;164    struct list_entry {165        std::string rel; // '/'-separated, relative to `base`166        bool is_dir = false;167    };168    struct list_result {169        std::vector<list_entry> entries;170        std::string err;        // set when `base` is not a directory171        bool truncated = false; // set when the walk could not see everything172    };173    // entries relative to `base`, which must already be resolved (absolute)174    // max_depth == 0 means unlimited, 1 means direct children of `base` only175    virtual list_result list_entries(const std::string & base, int max_depth, list_kind kind) const = 0;176    // on_chunk, if set, is called with each chunk of output as it is read (before truncation cuts in);177    // returning false terminates the process early (e.g. the client disconnected)178    virtual exec_result run(179            const std::vector<std::string> & args,180            size_t max_output,181            int timeout_secs,182            const std::function<bool(const std::string &)> & on_chunk = nullptr) const = 0;183};184 185// shared subprocess execution helper, used by both the local and the isolate-backed tools_io implementations.186// combine_stderr=false when the raw stdout bytes must not be tainted by stderr, e.g. reading file contents.187static tools_io::exec_result run_subprocess(188        const std::vector<std::string> & args,189        size_t max_output,190        int timeout_secs,191        const std::function<bool(const std::string &)> & on_chunk,192        bool combine_stderr,193        const std::string & cwd = "",194        const std::string * stdin_data = nullptr) {195    tools_io::exec_result res;196 197    common_subproc proc;198 199    int options = subprocess_option_no_window200                | subprocess_option_inherit_environment201                | subprocess_option_search_user_path;202    if (combine_stderr) {203        options |= subprocess_option_combined_stdout_stderr;204    }205 206    if (!proc.create(args, options, {}, cwd.empty() ? nullptr : cwd.c_str())) {207        res.output = "failed to spawn process";208        return res;209    }210 211    std::atomic<bool> done{false};212    std::atomic<bool> timed_out{false};213 214    std::thread timeout_thread([&]() {215        auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_secs);216        while (!done.load()) {217            if (std::chrono::steady_clock::now() >= deadline) {218                timed_out.store(true);219                proc.terminate();220                return;221            }222            std::this_thread::sleep_for(std::chrono::milliseconds(100));223        }224    });225 226    // write stdin before reading stdout, the child drains stdin as it goes227    // always close stdin, a transport client waits forever if its stdin pipe stays open228    if (FILE * in = proc.stdin_file()) {229        if (stdin_data != nullptr && !stdin_data->empty()) {230#if defined(_WIN32)231            // pipe fds default to CRT text mode: binary keeps the bytes untranslated232            _setmode(_fileno(in), _O_BINARY);233#endif234            // a short write is not an error by itself, the exit code below decides235            fwrite(stdin_data->data(), 1, stdin_data->size(), in);236        }237        fflush(in);238    }239    proc.close_stdin();240 241    FILE * f = proc.stdout_file();242    std::string output;243    bool truncated = false;244    if (f) {245#if defined(_WIN32)246        // pipe fds default to CRT text mode: binary keeps the bytes untranslated247        _setmode(_fileno(f), _O_BINARY);248#endif249        // read raw bytes, not lines: the output can hold NUL and must arrive as soon as it is ready250        // keep draining past the size cap, else the child blocks on a full pipe251        char buf[4096];252        for (;;) {253#if defined(_WIN32)254            const int n = _read(_fileno(f), buf, (unsigned) sizeof(buf));255#else256            ssize_t n = read(fileno(f), buf, sizeof(buf));257            while (n < 0 && errno == EINTR) {258                n = read(fileno(f), buf, sizeof(buf));259            }260#endif261            if (n <= 0) {262                break;263            }264            if (truncated) {265                continue;266            }267            const size_t len = (size_t) n;268            if (output.size() + len <= max_output) {269                output.append(buf, len);270                if (on_chunk && !on_chunk(console_output_to_utf8(std::string(buf, len)))) {271                    proc.terminate();272                    break;273                }274            } else {275                size_t remaining = max_output - output.size();276                output.append(buf, remaining);277                if (on_chunk && remaining > 0) on_chunk(console_output_to_utf8(std::string(buf, remaining)));278                truncated = true;279            }280        }281    }282 283    done.store(true);284    if (timeout_thread.joinable()) {285        timeout_thread.join();286    }287 288    res.exit_code = proc.join();289 290    res.output    = console_output_to_utf8(output);291    res.timed_out = timed_out.load();292    if (truncated) {293        res.output += "\n[output truncated]";294    }295    return res;296}297 298class tools_io_basic : public tools_io {299public:300    // cwd, if non-empty, is used to resolve relative paths and as the working directory for run()301    explicit tools_io_basic(std::string cwd = "") : cwd(std::move(cwd)) {}302 303    // expands a leading `~`, then resolves `path` against `cwd` (or the server304    // working directory when `cwd` is unset); the result is always absolute305    std::string resolve(const std::string & path) const override {306        const std::string p = expand_home(path);307 308        fs::path full = path_from_utf8(p);309        if (!full.is_absolute()) {310            if (cwd.empty()) {311                std::error_code ec;312                const fs::path cur = fs::current_path(ec);313                if (ec) return p;314                full = cur / full;315            } else {316                full = path_from_utf8(cwd) / full;317            }318        }319 320        // drop "." and ".." so they never reach git or the client321        full = full.lexically_normal();322        // a trailing ".." normalizes to a path that ends with a separator323        if (!full.has_filename() && full != full.root_path()) {324            full = full.parent_path();325        }326        return path_to_utf8(full);327    }328 329    bool is_directory(const std::string & path) const override {330        std::error_code ec;331        return fs::is_directory(path_from_utf8(resolve(path)), ec) && !ec;332    }333 334    bool is_regular_file(const std::string & path) const override {335        std::error_code ec;336        return fs::is_regular_file(path_from_utf8(resolve(path)), ec) && !ec;337    }338 339    bool file_size(const std::string & path, uintmax_t & out_size) const override {340        std::error_code ec;341        out_size = fs::file_size(path_from_utf8(resolve(path)), ec);342        return !ec;343    }344 345    bool read_file(const std::string & path, std::string & out) const override {346        std::ifstream f(path_from_utf8(resolve(path)), std::ios::binary);347        if (!f) return false;348        std::ostringstream ss;349        ss << f.rdbuf();350        out = ss.str();351        return true;352    }353 354    bool write_file(const std::string & path, const std::string & content) const override {355        std::error_code ec;356        fs::path fpath = path_from_utf8(resolve(path));357        if (fpath.has_parent_path()) {358            fs::create_directories(fpath.parent_path(), ec);359            if (ec) return false;360        }361        std::ofstream f(fpath, std::ios::binary);362        if (!f) return false;363        f << content;364        return (bool) f;365    }366 367    list_result list_entries(const std::string & base, int max_depth, list_kind kind) const override {368        list_result out;369 370        std::error_code ec;371        if (!fs::is_directory(base, ec) || ec) {372            out.err = "path does not exist or is not a directory";373            return out;374        }375 376        const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(SERVER_TOOL_LIST_ENTRIES_TIMEOUT);377 378        // git ls-files cannot list directories; use the walker when they are requested379        if (kind == list_kind::files) {380            auto res = run(381                {"git", "-C", base, "ls-files", "--cached", "--others", "--exclude-standard"},382                SERVER_TOOL_GIT_LS_FILES_MAX_OUTPUT, SERVER_TOOL_LIST_ENTRIES_TIMEOUT);383 384            if (res.exit_code == 0 && !res.timed_out) {385                std::istringstream iss(res.output);386                std::string line;387                while (std::getline(iss, line)) {388                    if (!line.empty() && line.back() == '\r') line.pop_back();389                    if (line.empty()) continue;390                    std::replace(line.begin(), line.end(), '\\', '/');391                    if (max_depth > 0 && entry_depth(line) > max_depth) continue;392                    if (is_regular_file(path_to_utf8(path_from_utf8(base) / path_from_utf8(line)))) {393                        out.entries.push_back({line, false});394                    }395                }396                return out;397            }398        }399 400        out.entries = list_entries_fallback(base, max_depth, kind, deadline, out.truncated);401        return out;402    }403 404    exec_result run(405            const std::vector<std::string> & args,406            size_t max_output,407            int timeout_secs,408            const std::function<bool(const std::string &)> & on_chunk = nullptr) const override {409        return run_subprocess(args, max_output, timeout_secs, on_chunk, /*combine_stderr=*/true, cwd);410    }411 412private:413    std::string cwd;414 415    // a link can point back to an ancestor and loop forever, so it is never walked416    static bool is_link(const fs::directory_entry & entry) {417        std::error_code ec;418        if (entry.is_symlink(ec) || ec) {419            return true;420        }421#if defined(_WIN32)422        // a junction looks like a plain directory to std::filesystem, so read the reparse tag423        WIN32_FIND_DATAW data;424        const HANDLE h = FindFirstFileW(entry.path().c_str(), &data);425        if (h == INVALID_HANDLE_VALUE) {426            return false;427        }428        FindClose(h);429        if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) {430            return false;431        }432        // other reparse points (cloud placeholder, dedup stub) are real directories433        return data.dwReserved0 == IO_REPARSE_TAG_SYMLINK || data.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT;434#else435        return false;436#endif437    }438 439    // NTFS is case insensitive, so Build and build are the same directory440    static std::string get_effective_name(const std::string & fname) {441#if defined(_WIN32)442        std::string lowered = fname;443        std::transform(lowered.begin(), lowered.end(), lowered.begin(),444                       [](unsigned char c) { return (char) std::tolower(c); });445        return lowered;446#else447        return fname;448#endif449    }450 451    static const std::unordered_set<std::string> & junk_dir_names() {452        static const std::unordered_set<std::string> names(453            std::begin(SERVER_TOOL_JUNK_DIR_NAMES), std::end(SERVER_TOOL_JUNK_DIR_NAMES));454        return names;455    }456 457    std::vector<list_entry> list_entries_fallback(const std::string & base, int max_depth, list_kind kind,458                                                  std::chrono::steady_clock::time_point deadline, bool & truncated) const {459        std::vector<list_entry> result;460 461        std::vector<std::tuple<fs::path, fs::path, int>> stack;462        stack.emplace_back(path_from_utf8(base), fs::path(), 0);463 464        while (!stack.empty()) {465            if (std::chrono::steady_clock::now() >= deadline) {466                truncated = true;467                return result;468            }469 470            auto [dir, rel_dir, depth] = std::move(stack.back());471            stack.pop_back();472 473            std::error_code ec;474            // step the iterator by hand: the throwing increment escapes on a directory that goes away475            fs::directory_iterator it(dir, fs::directory_options::skip_permission_denied, ec);476            // permission errors are skipped above, so this is a subtree the caller never sees477            if (ec) {478                truncated = true;479                continue;480            }481            for (const fs::directory_iterator end; it != end; it.increment(ec)) {482                if (ec) {483                    truncated = true;484                    break;485                }486                if (std::chrono::steady_clock::now() >= deadline) {487                    truncated = true;488                    return result;489                }490                const fs::directory_entry & entry = *it;491                const fs::path fname = entry.path().filename();492                std::error_code tec;493                const bool is_dir = entry.is_directory(tec);494                if (tec) continue;495                if (is_dir) {496                    if (kind == list_kind::dirs || kind == list_kind::all) {497                        result.push_back({path_to_utf8(rel_dir / fname), true});498                    }499                    // junk directories stay selectable but are never walked: they can be enormous500                    if (junk_dir_names().count(get_effective_name(path_to_utf8(fname))) > 0) continue;501                    if (!is_link(entry) && (max_depth == 0 || depth + 1 < max_depth)) {502                        stack.emplace_back(entry.path(), rel_dir / fname, depth + 1);503                    }504                } else if (entry.is_regular_file(tec)) {505                    if (kind == list_kind::files || kind == list_kind::all) {506                        result.push_back({path_to_utf8(rel_dir / fname), false});507                    }508                }509            }510        }511 512        return result;513    }514};515 516// timeout for auxiliary isolate calls (stat/mkdir/ls helpers); exec_shell_command uses its own517// caller-controlled timeout instead, enforced separately in run()518static constexpr int SERVER_TOOL_ISOLATE_EXEC_TIMEOUT = 15; // seconds519static constexpr size_t SERVER_TOOL_ISOLATE_READ_FILE_MAX_SIZE = 64 * 1024 * 1024; // 64 MB520 521// runs every tools_io operation as a command inside an isolate: a container, a remote host, ...522// the isolate is created, mounted, and torn down externally by the caller523// it must provide a POSIX environment: sh, cat, wc, mkdir, dirname, find, timeout524class tools_io_isolate : public tools_io {525public:526    // cwd, if non-empty, is used to resolve relative paths and as the working directory for run()527    explicit tools_io_isolate(std::string cwd = "") : cwd(std::move(cwd)) {}528 529    // resolves `path` against `cwd` if `path` is relative and `cwd` is set; otherwise returns `path` unchanged.530    // isolate paths are always POSIX-style ('/'), regardless of host OS.531    std::string resolve(const std::string & path) const override {532        if (cwd.empty() || (!path.empty() && path[0] == '/')) {533            return path;534        }535        return cwd + "/" + path;536    }537 538    bool is_directory(const std::string & path) const override {539        return shell_test("-d", resolve(path));540    }541 542    bool is_regular_file(const std::string & path) const override {543        return shell_test("-f", resolve(path));544    }545 546    bool file_size(const std::string & path, uintmax_t & out_size) const override {547        auto res = exec({"sh", "-c", "wc -c < \"$1\"", "_", resolve(path)}, 64, true);548        if (res.exit_code != 0 || res.timed_out) return false;549        try {550            size_t pos;551            out_size = (uintmax_t) std::stoull(res.output, &pos);552        } catch (...) {553            return false;554        }555        return true;556    }557 558    bool read_file(const std::string & path, std::string & out) const override {559        // combine_stderr=false: stderr must not be spliced into raw file bytes560        auto res = exec({"cat", "--", resolve(path)}, SERVER_TOOL_ISOLATE_READ_FILE_MAX_SIZE, false);561        if (res.exit_code != 0 || res.timed_out) return false;562        out = res.output;563        return true;564    }565 566    bool write_file(const std::string & path, const std::string & content) const override {567        // the content travels on stdin: no argv for the far side to re-parse, no temp file on the host568        auto res = run_subprocess(569            build_argv({"sh", "-c", "mkdir -p \"$(dirname \"$1\")\" && cat > \"$1\"", "_", resolve(path)},570                       /*needs_stdin=*/true),571            4096, SERVER_TOOL_ISOLATE_EXEC_TIMEOUT, nullptr, true, "", &content);572        return res.exit_code == 0 && !res.timed_out;573    }574 575    list_result list_entries(const std::string & base, int max_depth, list_kind kind) const override {576        list_result out;577 578        const std::string abs_base = resolve(base);579        if (!is_directory(base)) {580            out.err = "path does not exist or is not a directory";581            return out;582        }583 584        // git ls-files cannot list directories; use the walker when they are requested585        if (kind == list_kind::files) {586            auto res = exec(587                {"sh", "-c", "cd \"$1\" && git ls-files --cached --others --exclude-standard", "_", abs_base},588                SERVER_TOOL_GIT_LS_FILES_MAX_OUTPUT, true);589 590            if (res.exit_code == 0 && !res.timed_out) {591                for (const auto & rel : split_lines(res.output, /*strip_dot_slash=*/false)) {592                    if (max_depth > 0 && entry_depth(rel) > max_depth) continue;593                    out.entries.push_back({rel, false});594                }595                return out;596            }597        }598 599        if (kind == list_kind::dirs || kind == list_kind::all) {600            for (auto & rel : find_entries(abs_base, max_depth, /*dirs=*/true, out.truncated)) {601                out.entries.push_back({std::move(rel), true});602            }603        }604        if (kind == list_kind::files || kind == list_kind::all) {605            for (auto & rel : find_entries(abs_base, max_depth, /*dirs=*/false, out.truncated)) {606                out.entries.push_back({std::move(rel), false});607            }608        }609 610        return out;611    }612 613    // wraps the command with an in-isolate `timeout`, since killing the host-side client614    // does not kill the process tree running inside the isolate615    exec_result run(616            const std::vector<std::string> & args,617            size_t max_output,618            int timeout_secs,619            const std::function<bool(const std::string &)> & on_chunk = nullptr) const override {620        std::vector<std::string> inner = {"timeout", std::to_string(timeout_secs) + "s"};621        inner.insert(inner.end(), args.begin(), args.end());622        // small buffer over timeout_secs so the in-isolate `timeout` has a chance to exit cleanly623        // before the host-side supervisory timeout forcibly kills the client624        return run_subprocess(625            build_argv(with_cwd(inner), /*needs_stdin=*/true),626            max_output, timeout_secs + 5, on_chunk, true);627    }628 629protected:630    // wrap `inner` (a complete POSIX argv) into the host-side argv that runs it in the isolate631    // a transport that re-parses its args in a remote shell (ssh) must join `inner` with shell_quote_join()632    virtual std::vector<std::string> build_argv(const std::vector<std::string> & inner, bool needs_stdin) const = 0;633 634    // quote `argv` into a single string that a POSIX shell re-parses into exactly `argv`635    static std::string shell_quote_join(const std::vector<std::string> & argv) {636        std::string out;637        for (const auto & arg : argv) {638            if (!out.empty()) out += ' ';639            out += '\'';640            for (const char c : arg) {641                // a single quote cannot be escaped inside single quotes: close, escape, reopen642                if (c == '\'') out += "'\\''";643                else           out += c;644            }645            out += '\'';646        }647        return out;648    }649 650private:651    std::string cwd;652 653    // set the working directory in the command itself, no `-w` equivalent exists on every transport654    // auxiliary calls do not need this, they use the absolute paths from resolve()655    std::vector<std::string> with_cwd(const std::vector<std::string> & inner) const {656        if (cwd.empty()) {657            return inner;658        }659        // 127 is what a shell reports for a command it could not run660        std::vector<std::string> out = {"sh", "-c", "cd \"$1\" || exit 127; shift; exec \"$@\"", "_", cwd};661        out.insert(out.end(), inner.begin(), inner.end());662        return out;663    }664 665    exec_result exec(const std::vector<std::string> & inner, size_t max_output, bool combine_stderr) const {666        return run_subprocess(667            build_argv(inner, /*needs_stdin=*/false),668            max_output, SERVER_TOOL_ISOLATE_EXEC_TIMEOUT, nullptr, combine_stderr);669    }670 671    bool shell_run(const std::vector<std::string> & inner) const {672        auto res = exec(inner, 4096, true);673        return res.exit_code == 0 && !res.timed_out;674    }675 676    bool shell_test(const char * flag, const std::string & path) const {677        return shell_run({"sh", "-c", std::string("[ ") + flag + " \"$1\" ]", "_", path});678    }679 680    static std::vector<std::string> split_lines(const std::string & text, bool strip_dot_slash) {681        std::vector<std::string> result;682        std::istringstream iss(text);683        std::string line;684        while (std::getline(iss, line)) {685            if (!line.empty() && line.back() == '\r') line.pop_back();686            if (line.empty()) continue;687            if (strip_dot_slash && line.rfind("./", 0) == 0) line = line.substr(2);688            std::replace(line.begin(), line.end(), '\\', '/');689            result.push_back(line);690        }691        return result;692    }693 694    // one `find` pass in the isolate. junk directories stay selectable but are never descended into,695    // and -mindepth/-maxdepth keep a busybox image working as well as a GNU one696    std::vector<std::string> find_entries(const std::string & abs_base, int max_depth, bool dirs, bool & truncated) const {697        std::string prune_expr;698        for (const char * n : SERVER_TOOL_JUNK_DIR_NAMES) {699            if (!prune_expr.empty()) prune_expr += " -o ";700            prune_expr += std::string("-name ") + n;701        }702 703        std::string cmd = "cd \"$1\" && find . -mindepth 1";704        if (max_depth > 0) {705            cmd += " -maxdepth " + std::to_string(max_depth);706        }707        cmd += " \\( " + prune_expr + " \\) -prune";708        cmd += dirs ? " -print -o -type d -print" : " -o -type f -print";709 710        auto res = exec({"sh", "-c", cmd, "_", abs_base}, SERVER_TOOL_GIT_LS_FILES_MAX_OUTPUT, true);711        truncated = truncated || res.timed_out;712        return split_lines(res.output, /*strip_dot_slash=*/true);713    }714};715 716// an already-running container, driven through `<engine> exec`717// docker and podman take the same verbs and the same argument order, so one class drives both718class tools_io_container : public tools_io_isolate {719public:720    tools_io_container(std::string bin, std::string container_id, std::string cwd = "")721        : tools_io_isolate(std::move(cwd)), bin(std::move(bin)), container_id(std::move(container_id)) {}722 723protected:724    std::vector<std::string> build_argv(const std::vector<std::string> & inner, bool needs_stdin) const override {725        std::vector<std::string> argv = {bin, "exec"};726        if (needs_stdin) {727            argv.push_back("-i");728        }729        argv.push_back(container_id);730        argv.insert(argv.end(), inner.begin(), inner.end());731        return argv;732    }733 734private:735    std::string bin;736    std::string container_id;737};738 739// a remote host reached over ssh740// this is remoting, not isolation: the tools can do anything the target account can do741class tools_io_ssh : public tools_io_isolate {742public:743    tools_io_ssh(std::string target, std::string cwd = "")744        : tools_io_isolate(std::move(cwd)), target(std::move(target)) {}745 746    // the target can come from a client header, and ssh reads options from its argv747    // a target starting with '-' would become one, e.g. -oProxyCommand=<anything> runs on the host748    static bool is_valid_target(const std::string & target) {749        if (target.empty() || target[0] == '-') {750            return false;751        }752        return std::all_of(target.begin(), target.end(), [](unsigned char c) {753            return std::isalnum(c) || c == '.' || c == '-' || c == '_' || c == '@';754        });755    }756 757protected:758    std::vector<std::string> build_argv(const std::vector<std::string> & inner, bool needs_stdin) const override {759        // the remote shell re-parses the command line, so `inner` travels as one quoted word760        std::vector<std::string> argv = ssh_argv();761        if (!needs_stdin) {762            argv.push_back("-n");763        }764        argv.push_back(target);765        argv.push_back(shell_quote_join(inner));766        return argv;767    }768 769private:770    std::string target;771 772    // there is no console here, so a prompt would hang the tool call773    // key-based auth only, and the admin must trust the host key beforehand774    static std::vector<std::string> ssh_argv() {775        return {776            "ssh",777            "-o", "BatchMode=yes",778            "-o", "PasswordAuthentication=no",779            "-o", "KbdInteractiveAuthentication=no",780            "-o", "StrictHostKeyChecking=yes",781        };782    }783};784 785// "<engine>:<image>" spawns a container and owns it, "<engine>-container:<id>" attaches to one786struct container_runtime_spec {787    std::string bin;788    std::string arg; // image name when spawning, container id when attaching789    bool attach = false;790 791    static bool parse(const std::string & spec, container_runtime_spec & out) {792        // docker and podman take the same verbs, hence a single implementation793        static const char * engines[] = {"docker", "podman"};794        for (const char * bin : engines) {795            const std::string attach_prefix = std::string(bin) + "-container:";796            if (spec.rfind(attach_prefix, 0) == 0) {797                out = {bin, spec.substr(attach_prefix.size()), true};798                return true;799            }800            const std::string spawn_prefix = std::string(bin) + ":";801            if (spec.rfind(spawn_prefix, 0) == 0) {802                out = {bin, spec.substr(spawn_prefix.size()), false};803                return true;804            }805        }806        return false;807    }808 809    // same risk as the ssh target: an id starting with '-' would become an engine option,810    // e.g. --privileged811    static bool is_valid_id(const std::string & id) {812        if (id.empty() || !std::isalnum((unsigned char) id[0])) {813            return false;814        }815        return std::all_of(id.begin(), id.end(), [](unsigned char c) {816            return std::isalnum(c) || c == '.' || c == '-' || c == '_';817        });818    }819};820 821static std::unique_ptr<tools_io> make_tools_io(const json & params) {822    std::string cwd     = json_value(params, "cwd", std::string());823    std::string runtime = json_value(params, "runtime", std::string());824    if (runtime.empty()) {825        // an empty runtime runs the tools on the host826        return std::make_unique<tools_io_basic>(cwd);827    }828    container_runtime_spec container;829    if (container_runtime_spec::parse(runtime, container)) {830        // spawning belongs to the runtime that owns the container, a tool call only attaches831        if (!container.attach) {832            throw std::runtime_error("tool runtime must name a running container: " + runtime);833        }834        if (!container_runtime_spec::is_valid_id(container.arg)) {835            throw std::runtime_error("invalid container id: " + container.arg);836        }837        return std::make_unique<tools_io_container>(container.bin, container.arg, cwd);838    }839    const std::string ssh_prefix = "ssh:";840    if (runtime.rfind(ssh_prefix, 0) == 0) {841        std::string target = runtime.substr(ssh_prefix.size());842        if (!tools_io_ssh::is_valid_target(target)) {843            throw std::runtime_error("invalid ssh target: " + target);844        }845        return std::make_unique<tools_io_ssh>(target, cwd);846    }847    // do not fall back to the host, the caller asked for an isolate848    throw std::runtime_error("unknown tool runtime: " + runtime);849}850 851// no '/' in pattern -> match basename at any depth; else match full relative path852static bool path_glob_match(const std::string & pattern, const std::string & rel_path) {853    if (pattern.find('/') == std::string::npos) {854        return glob_match(pattern, path_to_utf8(path_from_utf8(rel_path).filename()));855    }856    if (pattern == "**" || pattern.rfind("**/", 0) == 0 || pattern.rfind('/', 0) == 0) {857        return glob_match(pattern, rel_path);858    }859    return glob_match("**/" + pattern, rel_path);860}861 862//863// read_file: read a file with optional line range and line-number prefix864//865 866static constexpr size_t SERVER_TOOL_READ_FILE_MAX_SIZE = 16 * 1024; // 16 KB867static constexpr size_t SERVER_TOOL_READ_FILE_MAX_SIZE_BASE64 = 32 * 1024 * 1024; // 32 MB868 869struct server_tool_read_file : server_tool {870    server_tool_read_file() {871        name = "read_file";872        display_name = "Read file";873        uses_cwd = true;874        permission_write = false;875    }876 877    json get_definition() const override {878        return {879            {"type", "function"},880            {"function", {881                {"name", name},882                {"description", "Read the contents of a file. Optionally specify a 1-based line range. "883                                "If append_loc is true, each line is prefixed with its line number (e.g. \"1\u2192...\")."},884                {"parameters", {885                    {"type", "object"},886                    {"properties", {887                        {"path",       {{"type", "string"},  {"description", "Path to the file"}}},888                        {"start_line", {{"type", "integer"}, {"description", "First line to read, 1-based (default: 1)"}}},889                        {"end_line",   {{"type", "integer"}, {"description", "Last line to read, 1-based inclusive (default: end of file)"}}},890                        {"append_loc", {{"type", "boolean"}, {"description", "Prefix each line with its line number"}}},891                    }},892                    {"required", json::array({"path"})},893                }},894            }},895        };896    }897 898    json invoke(json params, server_tool::stream *) const override {899        std::string path  = params.at("path").get<std::string>();900        int  start_line   = json_value(params, "start_line", 1);901        int  end_line     = json_value(params, "end_line",  -1); // -1 = no limit902        bool append_loc   = json_value(params, "append_loc", false);903        // comes from the x-resp-type header, the model cannot ask for it904        bool as_base64    = json_value(params, "resp_type", std::string()) == "base64";905 906        auto io = make_tools_io(params);907 908        uintmax_t file_size = 0;909        if (!io->file_size(path, file_size)) {910            return {{"error", "cannot stat file: " + path}};911        }912 913        if (as_base64) {914            if (file_size > SERVER_TOOL_READ_FILE_MAX_SIZE_BASE64) {915                return {{"error", string_format(916                    "file too large (%zu bytes, max %zu)",917                    (size_t)file_size, SERVER_TOOL_READ_FILE_MAX_SIZE_BASE64)}};918            }919            std::string content;920            if (!io->read_file(path, content)) {921                return {{"error", "failed to open file: " + path}};922            }923            return {924                {"base64",     base64::encode(content.data(), content.size())},925                {"size_bytes", (size_t) content.size()},926            };927        }928 929        if (file_size > SERVER_TOOL_READ_FILE_MAX_SIZE && end_line == -1) {930            return {{"error", string_format(931                "file too large (%zu bytes, max %zu). Use start_line/end_line to read a portion.",932                (size_t)file_size, SERVER_TOOL_READ_FILE_MAX_SIZE)}};933        }934 935        std::string content;936        if (!io->read_file(path, content)) {937            return {{"error", "failed to open file: " + path}};938        }939 940        std::istringstream f(content);941        std::string result;942        std::string line;943        int lineno = 0;944 945        while (std::getline(f, line)) {946            lineno++;947            if (lineno < start_line) continue;948            if (end_line != -1 && lineno > end_line) break;949 950            std::string out_line;951            if (append_loc) {952                out_line = std::to_string(lineno) + "\u2192" + line + "\n";953            } else {954                out_line = line + "\n";955            }956 957            if (result.size() + out_line.size() > SERVER_TOOL_READ_FILE_MAX_SIZE) {958                result += "[output truncated]";959                break;960            }961            result += out_line;962        }963 964        return {{"plain_text_response", result}};965    }966};967 968//969// file_glob_search: find files matching a glob pattern under a base directory970//971 972static constexpr int SERVER_TOOL_FILE_SEARCH_MAX_RESULTS = 100;973static constexpr const char * SERVER_TOOL_FILE_SEARCH_TYPE_FILE = "file";974static constexpr const char * SERVER_TOOL_FILE_SEARCH_TYPE_DIR  = "dir";975static constexpr const char * SERVER_TOOL_FILE_SEARCH_TYPE_ALL  = "all";976 977struct server_tool_file_glob_search : server_tool {978    server_tool_file_glob_search() {979        name = "file_glob_search";980        display_name = "File search";981        uses_cwd = true;982        permission_write = false;983    }984 985    json get_definition() const override {986        return {987            {"type", "function"},988            {"function", {989                {"name", name},990                {"description",991                    "Recursively search for files matching a glob pattern under a directory. "992                    "Automatically skips files ignored by .gitignore (when the directory is inside a git repo) "993                    "and common junk directories (.git, node_modules, build, dist, etc.) otherwise. "994                    "A pattern with no '/' (e.g. \"*.cpp\") matches the file's basename at any depth. "995                    "A pattern containing '/' matches the full relative path; unless already anchored with "996                    "\"**/\" or a leading '/', it is automatically prefixed with \"**/\". "997                    "Use type=\"dir\" or \"all\" to also list directories; directory entries are suffixed with '/' in the output. "998                    "Note: directory listings do not apply .gitignore filtering."},999                {"parameters", {1000                    {"type", "object"},1001                    {"properties", {1002                        {"path",      {{"type", "string"},  {"description", "Base directory to search in"}}},1003                        {"include",   {{"type", "string"},  {"description", "Glob pattern for files to include (e.g. \"*.cpp\" or \"src/**/*.cpp\"). Default: **"}}},1004                        {"exclude",   {{"type", "string"},  {"description", "Glob pattern for files to exclude"}}},1005                        {"type",      {{"type", "string"},  {"description", "Entry type to return: \"file\" (default), \"dir\" or \"all\""}}},1006                        {"max_depth", {{"type", "integer"}, {"description", "Maximum depth to descend into subdirectories (default: 0 = unlimited; 1 = direct children only)"}}},1007                        {"limit",     {{"type", "integer"}, {"description", string_format("Maximum number of results to return, capped at %d (default %d)", SERVER_TOOL_FILE_SEARCH_MAX_RESULTS, SERVER_TOOL_FILE_SEARCH_MAX_RESULTS)}}},1008                    }},1009                    {"required", json::array({"path"})},1010                }},1011            }},1012        };1013    }1014 1015    json invoke(json params, server_tool::stream *) const override {1016        auto io = make_tools_io(params);1017 1018        const std::string path = params.at("path").get<std::string>();1019 1020        std::string base      = io->resolve(path);1021        std::string include   = json_value(params, "include", std::string("**"));1022        std::string exclude   = json_value(params, "exclude", std::string(""));1023        std::string type      = json_value(params, "type",    std::string("file"));1024        int         max_depth = std::max(0, json_value(params, "max_depth", 0));1025        const int   limit_req = json_value(params, "limit", SERVER_TOOL_FILE_SEARCH_MAX_RESULTS);1026        if (limit_req < 1) {1027            return {{"error", "invalid limit: " + std::to_string(limit_req) + " (expected 1 or more)"}};1028        }1029        const int   limit     = std::min(limit_req, SERVER_TOOL_FILE_SEARCH_MAX_RESULTS);1030 1031        list_kind kind;1032        if (type == SERVER_TOOL_FILE_SEARCH_TYPE_FILE) {1033            kind = list_kind::files;1034        } else if (type == SERVER_TOOL_FILE_SEARCH_TYPE_DIR) {1035            kind = list_kind::dirs;1036        } else if (type == SERVER_TOOL_FILE_SEARCH_TYPE_ALL) {1037            kind = list_kind::all;1038        } else {1039            return {{"error", "invalid type: " + type + " (expected \"file\", \"dir\" or \"all\")"}};1040        }1041 1042        const auto listing = io->list_entries(base, max_depth, kind);1043        if (!listing.err.empty()) {1044            return {{"error", listing.err + ": " + path}};1045        }1046 1047        std::vector<tools_io::list_entry> matches;1048        for (const auto & entry : listing.entries) {1049            if (!path_glob_match(include, entry.rel)) continue;1050            if (!exclude.empty() && path_glob_match(exclude, entry.rel)) continue;1051            matches.push_back(entry);1052        }1053 1054        size_t total = matches.size();1055        size_t shown = std::min(total, (size_t) limit);1056 1057        std::ostringstream output_text;1058        json entries_json = json::array();1059        for (size_t i = 0; i < shown; i++) {1060            output_text << matches[i].rel << (matches[i].is_dir ? "/" : "") << "\n";1061            entries_json.push_back({1062                {"path", matches[i].rel},1063                {"type", matches[i].is_dir ? "dir" : "file"},1064            });1065        }1066 1067        output_text << "\n---\nTotal matches: " << total << "\n";1068        if (total > shown) {1069            output_text << string_format(1070                "[%zu results limit reached (%zu total matches). Refine the glob pattern to narrow the search.]\n",1071                shown, total);1072        }1073        if (listing.truncated) {1074            output_text << "[results truncated: time budget or unreadable directory]\n";1075        }1076 1077        // `base` is always absolute (resolve falls back to the server cwd), so1078        // API clients (e.g. the web UI picker) can join the relative entries1079        // into absolute paths. `plain_text_response` is what the model sees;1080        // `entries` is the same data as structured JSON for the UI picker,1081        // which reads `entries`/`base` instead of re-parsing the text.1082        return {{"plain_text_response", output_text.str()}, {"entries", entries_json}, {"base", base}};1083    }1084};1085 1086//1087// grep_search: search for a regex pattern in files1088//1089 1090static constexpr size_t SERVER_TOOL_GREP_SEARCH_MAX_RESULTS = 100;1091 1092struct server_tool_grep_search : server_tool {1093    server_tool_grep_search() {1094        name = "grep_search";1095        display_name = "Grep search";1096        uses_cwd = true;1097        permission_write = false;1098    }1099 1100    json get_definition() const override {1101        return {1102            {"type", "function"},1103            {"function", {1104                {"name", name},1105                {"description",1106                    "Search for a pattern in files under a path. Returns matching lines with file paths "1107                    "(and, unless searching a single file, paths relative to the given directory). "1108                    "Automatically skips files ignored by .gitignore (when the directory is inside a git repo) "1109                    "and common junk directories (.git, node_modules, build, dist, etc.) otherwise. "1110                    "include/exclude: a pattern with no '/' matches the basename at any depth; a pattern "1111                    "containing '/' matches the full relative path (auto-anchored with \"**/\" unless already anchored)."},1112                {"parameters", {1113                    {"type", "object"},1114                    {"properties", {1115                        {"path",                {{"type", "string"},  {"description", "File or directory to search in"}}},1116                        {"pattern",             {{"type", "string"},  {"description", "Pattern to search for (regular expression unless literal is true)"}}},1117                        {"include",             {{"type", "string"},  {"description", "Glob pattern to filter files (default: **)"}}},1118                        {"exclude",             {{"type", "string"},  {"description", "Glob pattern to exclude files"}}},1119                        {"return_line_numbers", {{"type", "boolean"}, {"description", "If true, include line numbers in results"}}},1120                        {"literal",             {{"type", "boolean"}, {"description", "Treat pattern as a literal string instead of a regular expression (default: false)"}}},1121                        {"ignore_case",         {{"type", "boolean"}, {"description", "Case-insensitive search (default: false)"}}},1122                        {"context_lines",       {{"type", "integer"}, {"description", "Number of lines of context to show before and after each match (default: 0)"}}},1123                    }},1124                    {"required", json::array({"path", "pattern"})},1125                }},1126            }},1127        };1128    }1129 1130    json invoke(json params, server_tool::stream *) const override {1131        std::string path        = params.at("path").get<std::string>();1132        std::string pat_str     = params.at("pattern").get<std::string>();1133        std::string include     = json_value(params, "include", std::string("**"));1134        std::string exclude     = json_value(params, "exclude", std::string(""));1135        bool        show_lineno = json_value(params, "return_line_numbers", false);1136        bool        literal     = json_value(params, "literal", false);1137        bool        ignore_case = json_value(params, "ignore_case", false);1138        int         ctx_lines   = std::max(0, json_value(params, "context_lines", 0));1139 1140        std::string pattern_src = pat_str;1141        if (literal) {1142            static const std::string specials = "\\^$.|?*+()[]{}";1143            std::string escaped;1144            escaped.reserve(pat_str.size() * 2);1145            for (char c : pat_str) {1146                if (specials.find(c) != std::string::npos) escaped += '\\';1147                escaped += c;1148            }1149            pattern_src = escaped;1150        }1151 1152        std::regex pattern;1153        try {1154            auto flags = std::regex::ECMAScript;1155            if (ignore_case) flags |= std::regex::icase;1156            pattern = std::regex(pattern_src, flags);1157        } catch (const std::regex_error & e) {1158            return {{"error", std::string("invalid regex: ") + e.what()}};1159        }1160 1161        auto io = make_tools_io(params);1162 1163        // collect (absolute_path, display_path) pairs to search1164        std::vector<std::pair<std::string, std::string>> files;1165 1166        const std::string abs_path = io->resolve(path);1167        if (io->is_regular_file(abs_path)) {1168            files.emplace_back(abs_path, path);1169        } else if (io->is_directory(abs_path)) {1170            const auto listing = io->list_entries(abs_path, 0, list_kind::files);1171            if (!listing.err.empty()) {1172                return {{"error", listing.err + ": " + path}};1173            }1174            for (const auto & entry : listing.entries) {1175                if (!path_glob_match(include, entry.rel)) continue;1176                if (!exclude.empty() && path_glob_match(exclude, entry.rel)) continue;1177                files.emplace_back(path_to_utf8(path_from_utf8(abs_path) / path_from_utf8(entry.rel)), entry.rel);1178            }1179        } else {1180            return {{"error", "path does not exist: " + path}};1181        }1182 1183        std::ostringstream output_text;1184        size_t total = 0;1185        bool limit_reached = false;1186        bool show_num = show_lineno || ctx_lines > 0;1187 1188        for (const auto & file_entry : files) {1189            if (limit_reached) break;1190            const std::string & fpath        = file_entry.first;1191            const std::string & display_path = file_entry.second;1192 1193            std::string content;1194            if (!io->read_file(fpath, content)) continue;1195            std::vector<std::string> lines;1196            {1197                std::istringstream f(content);1198                std::string line;1199                while (std::getline(f, line)) lines.push_back(line);1200            }

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