Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ac9b22c
fix(install): reject archive with path traversal before extraction (#…
ousamabenyounes Apr 17, 2026
6ebde6d
fix(git): resolve status completeness conflicts
em0t May 9, 2026
e827184
Merge pull request #1368 from ousamabenyounes/fix/issue-1250
aeppling May 13, 2026
3ba1634
Merge pull request #991 from em0t/fix/git-completeness
aeppling May 14, 2026
b70b0fe
fix(docker): forward --tail flag in compose logs
pszymkowiak May 14, 2026
5f1d8b0
Merge pull request #1885 from rtk-ai/fix/compose-logs-tail-flag-v2
pszymkowiak May 14, 2026
62fc0e0
fix(filters): address adversarial test-suite findings on aggressive f…
aeppling May 12, 2026
16803a6
chore(filters): remove filter-level annotations and restore compose l…
aeppling May 15, 2026
f6b28c2
fix(filters): add test for aggressive filter batch fix
aeppling May 15, 2026
be51783
fix(git): stream push output via FilterMode::Streaming (#963)
ousamabenyounes May 14, 2026
46fe7c4
feat(hints): add tail hints for tee & hints + address reviews
aeppling May 16, 2026
4eefe2f
fix: re-add env python as noisy dir
aeppling May 16, 2026
3571d52
fix: '...' ascii to unicode, remove some comments
aeppling May 16, 2026
b8172e5
fix(kubectl): compact get pods and services aliases
pagarsky May 5, 2026
2dd0ec9
Merge pull request #1720 from pagarsky/fix/kubectl-get-alias
aeppling May 17, 2026
d6c5647
Merge pull request #1531 from ousamabenyounes/fix/issue-963
aeppling May 17, 2026
f21b864
fix(filters): split docker ps/-a paths, cap ruff violations at 50
aeppling May 18, 2026
90c285c
Merge pull request #1895 from rtk-ai/fix/aggressive-filters-batch
aeppling May 19, 2026
548e4dd
fix(tee): safe truncation caps and compose-ps tee content fix
aeppling May 16, 2026
4960630
fix(rust): multi-line blocks used with tail hint
aeppling May 17, 2026
4acdcf2
docs(cmds): truncations hints and recovery guidelines
aeppling May 17, 2026
15a0d2e
Merge pull request #1928 from rtk-ai/fix/tee-hint-trunc-quality
aeppling May 19, 2026
2241428
test(hooks/init): add red test for copilot-instructions preservation
YOMXXX May 20, 2026
d108165
fix(hooks/init): preserve user content in copilot-instructions.md
YOMXXX May 20, 2026
544a5f4
test(hooks/init): cover idempotency, stale-block, dry-run, fresh, mal…
YOMXXX May 20, 2026
194a1b4
refactor(hooks/init): extract run_copilot_at for parallel-safe tests
YOMXXX May 20, 2026
f2864eb
docs(readme): fix license references to match LICENSE (Apache-2.0)
tylerezimmerman May 20, 2026
b6054a5
refacto(truncations): Set global CAPS for truncation
aeppling May 20, 2026
6a57821
Merge pull request #1997 from tylerezimmerman/fix/readme-license-apac…
aeppling May 20, 2026
35a540c
refactor(hooks/init): share write_rtk_block, unify malformed handling
YOMXXX May 20, 2026
a04aa7e
Merge pull request #1976 from YOMXXX/fix/copilot-instructions-preserve
aeppling May 20, 2026
9c80934
Merge branch 'develop' into fix/tee-hint-trunc-quality
aeppling May 21, 2026
d5a1731
fix(truncate): global caps reduce (avoid underflow and 0 results)
aeppling May 21, 2026
1e56235
Merge pull request #1998 from rtk-ai/fix/tee-hint-trunc-quality
aeppling May 21, 2026
7d31049
Merge pull request #2020 from rtk-ai/master
aeppling May 21, 2026
7753e48
fix(git): drop -uall from compact status so output never exceeds raw
aeppling May 22, 2026
06476d1
Merge pull request #2035 from rtk-ai/fix/git-status-uall
aeppling May 22, 2026
6e6efd4
docs(git): sync status README with --porcelain -b (drop -uall)
aeppling May 22, 2026
8564ddc
Merge pull request #2040 from rtk-ai/fix/review-1879-git-status-doc-test
aeppling May 22, 2026
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
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Don't invent new output formats. Don't add RTK-specific headers or markers in th

If a filter fails, fall back to raw output. RTK should never prevent a command from executing or producing output. Better to pass through unfiltered than to error out. Same for hooks: exit 0 on all error paths so the agent's command runs unmodified.

Every filter needs a fallback path. Every hook must handle malformed input gracefully.
Every filter needs a fallback path. Every hook must handle malformed input gracefully. Truncation follows the same rule: capping output at N items is only acceptable if accompanied by a hint that lets the agent recover the hidden data.

### Zero Overhead

Expand Down Expand Up @@ -262,6 +262,7 @@ cargo fmt --all --check && cargo clippy --all-targets && cargo test
- [ ] Unit tests added/updated for changed code
- [ ] Snapshot tests reviewed (`cargo insta review`)
- [ ] Token savings >=60% verified
- [ ] Any truncated list has a recovery hint (`force_tee_tail_hint` or `force_tee_hint`) and uses a `CAP_*` from `src/core/truncate.rs`
- [ ] Edge cases covered
- [ ] `cargo fmt --all --check && cargo clippy --all-targets && cargo test` passes
- [ ] Manual test: run `rtk <cmd>` and inspect output
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<p align="center">
<a href="https://github.com/rtk-ai/rtk/actions"><img src="https://github.com/rtk-ai/rtk/workflows/Security%20Check/badge.svg" alt="CI"></a>
<a href="https://github.com/rtk-ai/rtk/releases"><img src="https://img.shields.io/github/v/release/rtk-ai/rtk" alt="Release"></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
<a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License: Apache 2.0"></a>
<a href="https://discord.gg/RySmvNF5kF"><img src="https://img.shields.io/discord/1470188214710046894?label=Discord&logo=discord" alt="Discord"></a>
<a href="https://formulae.brew.sh/formula/rtk"><img src="https://img.shields.io/homebrew/v/rtk" alt="Homebrew"></a>
</p>
Expand Down Expand Up @@ -483,7 +483,7 @@ Join the community on [Discord](https://discord.gg/RySmvNF5kF).

## License

MIT License - see [LICENSE](LICENSE) for details.
Apache License 2.0 - see [LICENSE](LICENSE) for details.

## Disclaimer

Expand Down
7 changes: 7 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ install() {
error "Failed to download binary"
fi

# Verify archive contents before extraction (CWE-22 path traversal).
# Reject any entry with an absolute path or a ".." component.
info "Verifying archive..."
if tar -tzf "$ARCHIVE" | grep -qE '^/|(^|/)\.\.(/|$)'; then
error "Archive contains unsafe paths (absolute or directory traversal) — refusing to extract"
fi

info "Extracting..."
tar -xzf "$ARCHIVE" -C "$TEMP_DIR"

Expand Down
98 changes: 98 additions & 0 deletions scripts/test-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env sh
# Tests for install.sh path traversal check (issue #1250, CWE-22).
#
# Verifies:
# 1. Safe archives (single binary, "./prefix", subdirs) are accepted.
# 2. Archives with absolute paths are rejected pre-extraction.
# 3. Archives with ".." components are rejected pre-extraction.
# 4. The check is still present in install.sh (regression guard).

set -eu

REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
INSTALL_SH="$REPO_ROOT/install.sh"

if [ ! -f "$INSTALL_SH" ]; then
echo "FAIL: install.sh not found at $INSTALL_SH"
exit 1
fi

if ! command -v python3 >/dev/null 2>&1; then
echo "SKIP: python3 not available — crafted tarball tests require python3"
exit 0
fi

TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

# The check replicated from install.sh (keep in sync with install.sh).
# Returns 0 when archive is safe, 1 when unsafe.
check_archive() {
if tar -tzf "$1" | grep -qE '^/|(^|/)\.\.(/|$)'; then
return 1
fi
return 0
}

# --- Build safe archive using standard tar ---
mkdir -p "$TMPDIR/safe_src"
printf '#!/bin/sh\necho rtk\n' > "$TMPDIR/safe_src/rtk"
(cd "$TMPDIR/safe_src" && tar -czf "$TMPDIR/safe.tgz" rtk)

# --- Build crafted malicious archives with python ---
python3 - "$TMPDIR" <<'PY'
import sys, tarfile, io

base = sys.argv[1]


def make(name, entry):
with tarfile.open(f"{base}/{name}", "w:gz") as t:
info = tarfile.TarInfo(name=entry)
data = b"pwned"
info.size = len(data)
t.addfile(info, io.BytesIO(data))


make("traversal.tgz", "../etc/evil")
make("absolute.tgz", "/tmp/evil_abs")
make("middle.tgz", "rtk/../../../etc/evil")
make("end_dotdot.tgz", "rtk/..")
PY

FAIL=0
pass() { printf ' PASS: %s\n' "$1"; }
fail() { printf ' FAIL: %s\n' "$1"; FAIL=1; }

echo "==> Functional checks"

if check_archive "$TMPDIR/safe.tgz"; then
pass "safe archive accepted"
else
fail "safe archive rejected (false positive)"
fi

for bad in traversal absolute middle end_dotdot; do
if check_archive "$TMPDIR/$bad.tgz"; then
fail "$bad archive accepted (should be rejected)"
else
pass "$bad archive rejected"
fi
done

echo "==> Regression guard"

if grep -qF 'tar -tzf' "$INSTALL_SH" && grep -qF '\.\.' "$INSTALL_SH"; then
pass "install.sh still contains the path-traversal check"
else
fail "install.sh is missing the path-traversal check — was it removed?"
fi

echo ""
if [ "$FAIL" -eq 0 ]; then
echo "All install.sh path traversal tests passed"
exit 0
else
echo "Some tests failed"
exit 1
fi
16 changes: 16 additions & 0 deletions src/cmds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,21 @@ When filtering fails, fall back to raw output and warn on stderr. Never block th

Modules that parse structured output (JSON, NDJSON, state machines) must call `tee::tee_and_hint()` so users can recover full output on failure.

### Internal Truncation Recovery

When a filter caps a list at N items (e.g. `take(20)`), the remaining items must be accessible via a tee hint. **Never show `"… +N more"` without a recovery path** — the agent has no way to retrieve the hidden content.

**Choosing the right hint:**

| Content type | Function | Condition |
|---|---|---|
| Flat list — one item = one line in the tee | `force_tee_tail_hint(content, slug, MAX + 1)` | PR lists, error lines, file paths — anything where each item is a single-line string |
| Multi-line blocks | `force_tee_hint(content, slug)` | Test failures, build error blocks — items that span multiple lines so a line offset is meaningless |

**Cap values come from `src/core/truncate.rs`.** Pick the `CAP_*` matching your data class (`CAP_ERRORS`, `CAP_WARNINGS`, `CAP_LIST`, `CAP_INVENTORY`) and bind it to a local `const MAX_XXX: usize = CAP_Y;`. Derive `take(MAX_XXX)`, `> MAX_XXX`, and the offset `MAX_XXX + 1` from the local. These CAPs will later become the configuration surface for per-filter cap tuning (user-overridable via config) — keep all truncation values routed through them so that hook lands as a single switch rather than a codebase-wide hunt. A filter that genuinely needs to deviate uses **`truncate::reduced(CAP_Y, n)`** (e.g. `reduced(CAP_WARNINGS, 5)`) so it still tracks the global when reconfigured — never a bare literal, never `cap - n` (underflows once caps are runtime-configurable), and never `*`/`/` (those scale unboundedly). `reduced` falls back to the full cap if the reduction would empty the list. Each deviation needs a one-line comment stating why; if there's no real reason, just use the plain CAP. See `src/core/README.md` ("Truncation Caps") for the full rationale.

**The tee content must match what `tail` produces.** For `force_tee_tail_hint`, build the tee from the same formatted values shown in the output — not raw/intermediate data. If the filter reformats items before displaying them, pre-build a `Vec<String>` of formatted lines and use it for both the display loop and the tee.

### Stderr Handling

Modules must capture stderr and include it in the raw string passed to `timer.track()`, so token savings reflect total output.
Expand All @@ -278,6 +293,7 @@ Adding a new filter or command requires changes in multiple places. For TOML-vs-
- Use `RunOptions::default()` when filtering combined text output
- Add `.tee("label")` when the filter parses structured output (enables raw output recovery on failure)
- **Exit codes**: handled automatically by `run_filtered()` — just return its result
- **Truncation**: if the filter caps any list at N items, emit `force_tee_tail_hint` (flat lists) or `force_tee_hint` (multi-line blocks) so the agent can recover hidden items — see [Internal Truncation Recovery](#internal-truncation-recovery). Use a named constant for the cap; derive the offset from it (`MAX_XXX + 1`)
2. **Register module**:
- Ecosystem `mod.rs` files use `automod::dir!()` — any `.rs` file in the directory becomes a public module automatically. No manual `pub mod` needed, but be aware: WIP or helper files will also be exposed. Only commit command-ready modules.
- Add variant to `Commands` enum in `main.rs` with `#[arg(trailing_var_arg = true, allow_hyphen_values = true)]`
Expand Down
21 changes: 11 additions & 10 deletions src/cmds/cloud/aws_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use crate::core::tee::force_tee_hint;
use crate::core::tracking;
use crate::core::truncate::{CAP_INVENTORY, CAP_LIST};
use crate::core::utils::{
exit_code_from_output, exit_code_from_status, human_bytes, join_with_overflow,
resolved_command, shorten_arn, truncate_iso_date,
Expand All @@ -15,7 +16,7 @@ use lazy_static::lazy_static;
use regex::Regex;
use serde_json::Value;

const MAX_ITEMS: usize = 20;
const MAX_ITEMS: usize = CAP_LIST;
const JSON_COMPRESS_DEPTH: usize = 4;

/// Result of a filter function: filtered text + whether items were truncated.
Expand Down Expand Up @@ -494,7 +495,7 @@ fn filter_s3_ls(output: &str) -> FilterResult {

if total > limit {
let text = format!(
"{}\n... +{} more items",
"{}\n +{} more items",
lines[..limit].join("\n"),
total - limit
);
Expand Down Expand Up @@ -553,7 +554,7 @@ fn filter_ec2_instances(json_str: &str) -> Option<FilterResult> {
}

if truncated {
result.push_str(&format!(" ... +{} more\n", total - MAX_ITEMS));
result.push_str(&format!(" +{} more\n", total - MAX_ITEMS));
}

let text = result.trim_end().to_string();
Expand Down Expand Up @@ -700,7 +701,7 @@ fn filter_cfn_describe_stacks(json_str: &str) -> Option<FilterResult> {

// --- P0 filters: CloudWatch Logs, CloudFormation Events, Lambda ---

const MAX_LOG_EVENTS: usize = 50;
const MAX_LOG_EVENTS: usize = CAP_INVENTORY;

/// Convert days since Unix epoch to (year, month, day). Civil calendar, UTC.
fn days_to_ymd(days: i64) -> (i64, i64, i64) {
Expand Down Expand Up @@ -759,7 +760,7 @@ fn filter_logs_events(json_str: &str) -> Option<FilterResult> {
}

if truncated {
lines.push(format!("... +{} more events", total - MAX_LOG_EVENTS));
lines.push(format!(" +{} more events", total - MAX_LOG_EVENTS));
}

let text = lines.join("\n");
Expand Down Expand Up @@ -1132,7 +1133,7 @@ fn filter_dynamodb_items(json_str: &str) -> Option<FilterResult> {
}

if truncated {
lines.push(format!("... +{} more items", total - MAX_ITEMS));
lines.push(format!(" +{} more items", total - MAX_ITEMS));
}

let text = lines.join("\n");
Expand Down Expand Up @@ -1426,7 +1427,7 @@ fn filter_logs_query_results(json_str: &str) -> Option<FilterResult> {
}

if truncated {
lines.push(format!("... +{} more rows", total - MAX_ITEMS));
lines.push(format!(" +{} more rows", total - MAX_ITEMS));
}

let text = lines.join("\n");
Expand Down Expand Up @@ -1616,7 +1617,7 @@ mod tests {
}
let input = lines.join("\n");
let result = filter_s3_ls(&input);
assert!(result.text.contains("... +20 more items"));
assert!(result.text.contains(" +20 more items"));
assert!(result.truncated);
}

Expand Down Expand Up @@ -1852,7 +1853,7 @@ mod tests {
}
let json = format!(r#"{{"DBInstances": [{}]}}"#, dbs.join(","));
let result = filter_rds_instances(&json).unwrap();
assert!(result.text.contains("... +5 more instances"));
assert!(result.text.contains(" +5 more instances"));
assert!(result.truncated);
}

Expand Down Expand Up @@ -1893,7 +1894,7 @@ mod tests {
}
let json = format!(r#"{{"events": [{}]}}"#, events.join(","));
let result = filter_logs_events(&json).unwrap();
assert!(result.text.contains("... +10 more events"));
assert!(result.text.contains(" +10 more events"));
assert!(result.truncated);
}

Expand Down
Loading
Loading