• 1.2 589d337057

    1.2 - Sandbox update
    ci/woodpecker/tag/release Pipeline was successful
    ci/woodpecker/push/tests Pipeline was successful
    Stable

    qpismont released this 2026-09-20 17:07:14 +02:00 | 0 commits to main since this release

    Summary

    1.2 turns Herald from a bot that sent the raw diff to a model into one that reviews pull requests inside an ephemeral Dev Container. Each review now clones the PR head, builds and starts the repository's devcontainer.json, and lets the model explore the code with read-only tools — the code is no longer shipped in the prompt. To support this, the project was split into a Cargo workspace (herald-server + a new devcontainer-rs crate), the third-party OpenRouter SDK was replaced by an in-tree client with tool calling, and Herald can now run in a container with nothing mounted but the container daemon socket. Review comments are anchored on changed lines and tagged with a severity.

    Important changes

    • Sandboxed reviews (new core flow). Every review runs inside a disposable Dev Container built from the pull request's .devcontainer/devcontainer.json. The model explores the repository with read-only tools (ls, file_size, read_file, grep, find) instead of receiving the code. A repository without a .devcontainer/devcontainer.json can no longer be reviewed.
    • New devcontainer-rs crate. Full devcontainer lifecycle (parse, build, up, hooks, exec, teardown) driven through the container daemon's Docker-compatible API via Bollard. No docker/podman binary is required inside the image, and both Docker and Podman sockets work.
    • Workspace restructure and binary rename. The single herald package became a workspace of crates/herald-server and crates/devcontainer-rs; the binary and package are now herald-server (Containerfile, CI and scripts updated accordingly).
    • In-tree OpenRouter client. openrouter-rs was dropped for a local client implementing non-streaming POST /chat/completions with tool calling, reasoning enabled, and cost reporting. It removes a duplicate reqwest version from the tree.
    • Containerized deployment fixed. The clone and the build context are streamed to the daemon over the socket instead of being bind-mounted from a host path, so Herald works in a container with only -v …/podman.sock:/var/run/docker.sock — no shared workspace directory.
    • BOT_NAME removed. The bot's name is now the login of the Gitea user owning GITEA_TOKEN, resolved at startup (GET /api/v1/user).
    • New environment variables. SANDBOX_MAX_ITERATIONS (default 8) and DOCKER_HOST (default unix:///var/run/docker.sock). BOT_NAME was removed; .env.example and the README table updated.
    • Pull request data comes from the API, not the webhook. The diff is fetched from /api/v1/repos/{owner}/{repo}/pulls/{n}.diff (the webhook diff_url is session-authenticated and does not serve private repositories), and the changed-file list is fetched and paginated for exact paths and statuses.
    • Review comments are anchored on changed lines. Each comment is placed on an added line (new_position) or a removed line (old_position); the side is resolved from the diff, and any comment that does not match a changed line is dropped (with a warning).
    • Severity on every comment. Comments are tagged bug, security, performance or maintainability, shown at the start of the posted comment and broken down in the review summary.

    Other changes

    • Generated lockfiles excluded from the changes handed to the model (Cargo.lock, package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock(b), composer.lock, Gemfile.lock, poetry.lock, uv.lock, Pipfile.lock, go.sum, packages.lock.json, flake.lock, pubspec.lock).
    • Prompts rewritten for the tool-based flow: exhaustive reporting, "read before you assert", explicit added/removed semantics (removed lines are not in the working directory), anchoring rules, and a summary that also lists what the PR does well.
    • Sandbox isolation hardened. Each review gets its own image tag, container and network; the network is cut after postCreateCommand/postStartCommand; runArgs from the untrusted PR are parsed but never applied; ${localEnv:…} references are never read from Herald's environment (secret-exfiltration guard); every container command is bounded by a timeout and all resources are removed on completion or failure.
    • Diff size capped at 1 MiB, with an explicit error above it.
    • Graceful shutdown deadline of 60 s, so a stuck sandbox cannot keep the process alive indefinitely.
    • Startup warning when the container daemon is unreachable (every review would fail).
    • Dependency and toolchain updates: Rust 1.98, reqwest 0.12 → 0.13, sentry 0.48 → 0.49 (new client options API), tower-http 0.6 → 0.7.
    • CI strengthened: cargo fmt --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, workspace tests, and a Containerfile build.
    • Containerfile reworked: multi-stage build, minimal runtime packages (git, ca-certificates, libssl3t64, libzstd1, zlib1g), and the binary as PID 1 to receive SIGTERM.
    • Dev container updated: rust:1.98-trixie with clippy/rustfmt, plus --userns=keep-id and label=disable; .zed/settings.json added, .vscode/settings.json and .dockerignore removed.
    • README: new "Sandboxed reviews" section documenting the flow, the socket, the runArgs policy and the isolation guarantees.
    Downloads