Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
345 changes: 153 additions & 192 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ chutes-tool-types = { path = "crates/common/chutes-tool-types" }
chutes-tracing = { path = "crates/common/chutes-tracing" }
chutes-tty-utils = { path = "crates/codegen/chutes-tty-utils" }
zbus = { version = "5" }
zip = "3"
zip = "9"
zstd = "0.14"

[profile.release]
Expand Down
16 changes: 1 addition & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,13 @@
A Chutes-native terminal coding agent

[![CI](https://github.com/TheStreamCode/chutes-build/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/TheStreamCode/chutes-build/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
[![npm](https://img.shields.io/npm/v/chutes-build?logo=npm&color=cb3837)](https://www.npmjs.com/package/chutes-build)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)

![Chutes Build TUI](assets/chutes/screenshot/chutes-build.png)

Chutes Build is a terminal-based AI coding agent for the [Chutes](https://chutes.ai) ecosystem. It runs as a full-screen TUI (terminal user interface) that understands a codebase, edits files, executes shell commands, searches the web, and manages long-running tasks, interactively, headlessly for scripts and CI, or embedded in editors through the Agent Client Protocol (ACP). It is an independent, unofficial fork of [xai-org/grok-build](https://github.com/xai-org/grok-build) with Chutes-native inference, model routing, media and account endpoints, and it is not affiliated with or endorsed by xAI.

## Contents

- [Install](#install)
- [Quick start](#quick-start)
- [Features](#features)
- [Requirements](#requirements)
- [Configuration](#configuration)
- [Privacy and security](#privacy-and-security)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
- [Support](#support)
- [Contributing](#contributing)
- [License](#license)

## Install

Install the npm launcher, which resolves the native binary for the current platform:
Expand Down
2 changes: 1 addition & 1 deletion crates/codegen/chutes-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pdf_oxide = { workspace = true }
# `workspace = true`: the workspace `zip` pin has default features on, which
# drag aes/bzip2/lzma/zstd into the shipped pager binary. PPTX entries are
# only ever stored or deflated.
zip = { version = "3", default-features = false, features = ["deflate-flate2"] }
zip = { version = "9", default-features = false, features = ["deflate-flate2"] }
quick-xml = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
minijinja = { version = "2", features = ["custom_syntax"] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub(crate) fn extract_pptx_text_from_bytes(bytes: &[u8]) -> Result<String, Strin
let mut slide_numbers: Vec<u32> = archive
.file_names()
.filter_map(|name| {
let name = name.ok()?;
name.strip_prefix("ppt/slides/slide")?
.strip_suffix(".xml")?
.parse()
Expand Down
2 changes: 1 addition & 1 deletion crates/codegen/chutes-update/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ sha2 = { workspace = true }
chutes-tty-utils = { workspace = true }
# MinGit archive extraction. Not `workspace = true`: the workspace pin has
# default features on (aes/bzip2/lzma/zstd); MinGit entries are only deflated.
zip = { version = "3", default-features = false, features = ["deflate-flate2"] }
zip = { version = "9", default-features = false, features = ["deflate-flate2"] }

[target.'cfg(target_os = "macos")'.dependencies]
libc = { workspace = true }
Expand Down
20 changes: 13 additions & 7 deletions crates/codegen/chutes-update/src/windows_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,23 @@ pub(super) fn extract_zip(zip_path: &Path, dest: &Path) -> anyhow::Result<()> {
let mut total: u64 = 0;
for index in 0..archive.len() {
let mut entry = archive.by_index(index).context("read zip entry")?;
let Some(relative) = entry.enclosed_name() else {
anyhow::bail!("archive entry escapes the destination: {}", entry.name());
};
if relative
let entry_name = entry
.name()
.map_err(|e| anyhow::anyhow!("read zip entry name: {e}"))?
.into_owned();
// Check the raw entry name: `enclosed_name` normalizes `.` components away, so it cannot
// enforce the plain-name contract this function promises.
if std::path::Path::new(&entry_name)
.components()
.any(|c| !matches!(c, Component::Normal(_)))
{
anyhow::bail!("archive entry escapes the destination: {}", entry.name());
anyhow::bail!("archive entry escapes the destination: {entry_name}");
}
let Some(relative) = entry.enclosed_name() else {
anyhow::bail!("archive entry escapes the destination: {entry_name}");
};
if entry.is_symlink() {
anyhow::bail!("archive entry is a symlink: {}", entry.name());
anyhow::bail!("archive entry is a symlink: {entry_name}");
}
let out = dest.join(relative);
if entry.is_dir() {
Expand All @@ -174,7 +180,7 @@ pub(super) fn extract_zip(zip_path: &Path, dest: &Path) -> anyhow::Result<()> {
std::fs::File::create(&out).with_context(|| format!("create {}", out.display()))?;
let budget = MAX_MINGIT_EXTRACTED_BYTES - total + 1;
let written = std::io::copy(&mut entry.by_ref().take(budget), &mut out_file)
.with_context(|| format!("extract {}", entry.name()))?;
.with_context(|| format!("extract {entry_name}"))?;
total += written;
if total > MAX_MINGIT_EXTRACTED_BYTES {
anyhow::bail!("archive expands past the {MAX_MINGIT_EXTRACTED_BYTES}-byte cap");
Expand Down
5 changes: 1 addition & 4 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ ignore = [
{ id = "RUSTSEC-2026-0249", reason = "smartstring is unmaintained (repository archived 2026-05-03), not vulnerable — there is no affected API to avoid. It reaches us only as a non-optional dependency of rhai, the workflow scripting engine, and we are already on rhai's latest stable (1.25.1), so no bump closes it and nothing but dropping rhai would. Its own recommended alternatives (compact_str, smol_str) are rhai's migration to make." },
# Arrived with the 1.7.2 release gate (live DB, 2026-10-19). Same rule as
# above: each reason records the reachability check that was run.
{ id = "RUSTSEC-2026-0367", reason = "zip preserves SUID/SGID bits through ZipArchive::extract(). We never call extract(): pptx reads entries in memory by name, the Windows updater copies entries manually behind enclosed_name() with symlinks refused and created files never take archive modes, and the transitive office_oxide copy (zip 8.6) has zero .extract( call sites. Only a major 3→9 migration closes it." },
{ id = "RUSTSEC-2026-0368", reason = "zip extract() follows attacker symlinks. Same reachability as RUSTSEC-2026-0367: no extract() call anywhere in this tree or in transitive office_oxide, and our manual copy path refuses symlinks outright. Only a major 3→9 migration closes it." },
{ id = "RUSTSEC-2026-0369", reason = "zip extract() applies Unix permissions through symlinks. Same reachability as RUSTSEC-2026-0367: no extract() call anywhere in this tree or in transitive office_oxide. Only a major 3→9 migration closes it." },
{ id = "RUSTSEC-2026-0429", reason = "zip extract() sizes a symlink-target buffer from the archive's declared uncompressed size. Same reachability as RUSTSEC-2026-0367: no extract() call anywhere in this tree or in transitive office_oxide; pptx reads entries by name in memory and the Windows updater copies entries manually behind enclosed_name() with symlinks refused. Only a major 3→9 migration closes it." },
{ id = "RUSTSEC-2026-0441", reason = "pin-utils is unmaintained, not vulnerable; it exposes only the pin_mut! and unsafe_pinned! macros and has no affected API to avoid. Transitive through aws-smithy-http in the AWS SDK, already at the latest release that still depends on it, so no bump closes it." },
{ id = "RUSTSEC-2024-0449", reason = "unsafe-libyaml is unmaintained (repository archived 2024-03-25), not vulnerable — there is no affected API to avoid. Transitive through serde_yaml 0.9 (mermaid-to-svg), already at its last release 0.2.11, so no bump closes it; the maintained replacement is serde_yaml's call to make." },
{ id = "RUSTSEC-2026-0370", reason = "zvariant is unmaintained as a standalone crate (merged into zbus 6), not vulnerable. Transitive through zbus 5.14 (ashpd, dark-light theme detection); only a zbus 5→6 migration closes it." },
{ id = "RUSTSEC-2026-0371", reason = "zbus_names is unmaintained as a standalone crate (merged into zbus 6), not vulnerable. Transitive through zbus 5.14 (ashpd, dark-light theme detection); only a zbus 5→6 migration closes it." },
Expand Down
Loading