Skip to content
Closed
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
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,52 @@

All notable changes to Burnwall.

## [0.9.14] — 2026-06-10

A real-world robustness pass driven by dogfooding: a multi-agent review of
every feature, focused on the failure modes that make a tool freeze, falsely
block, or mislead — the kind that trigger an uninstall.

### Fixed

- **The daily budget now resets at midnight.** A long-running proxy used to
accumulate spend across days and eventually return "budget exceeded" on every
request even though the day's real spend was small. The counter is now
day- and month-aware (restart- and clock-change-proof), and the monthly cap
is actually enforced.
- **Loop detection no longer gets stuck on retries.** A blocked request (and a
client's automatic retry of it, or a retry after a provider outage) no longer
feeds the loop-detection window, so a transient blip can't wedge a session
into a permanent 429 loop. Blocks now carry a `Retry-After`, and the window is
keyed per method/provider/path so unrelated requests don't collide.
- **Fewer false security blocks.** Writing or discussing a file that merely
mentions a sensitive path (e.g. `~/.ssh` in a README) no longer 403s — only
shell-tool arguments get command checks. Windows paths in tool arguments are
no longer mistaken for network mounts, scoped deletes like `rm -rf /tmp/x`
pass, and well-known documentation/example keys are exempt. Blocks now explain
what was caught and how to proceed, and `burnwall report-bug` writes a
sanitized local report for false positives.
- **The proxy no longer hangs on a stalled or unreachable upstream**, and
cancelling a request (Esc) stops the upstream instead of billing the full
response.
- **Accurate cost capture for more tools.** OpenAI's Responses API (used by
Codex) is now parsed instead of silently recording $0, unknown models warn
instead of recording $0, and the cross-tool "today" total no longer
double-counts traffic that went through the proxy.

### Changed

- **A crashed or stopped proxy no longer breaks your terminals.** Shell routing
is liveness-gated: if the proxy isn't running, a new shell talks directly to
the provider (unprotected but working) instead of failing to connect. Every
status surface shows a clear "proxy down" warning when routing points at a
dead port. PowerShell now gets persistent routing like the other shells.
- Plan-aware budgeting: on a flat-rate subscription, the dollar cap is treated
as advisory (tracked and warned, not blocked) unless you opt in.
- Hardening across MCP (prose-safe scanning, clearer approval errors), the audit
chain (lost-key detection), storage (schema versioning), and the daemon
(a real log file, PID identity checks).

## [0.9.13] — 2026-06-09

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "burnwall"
version = "0.9.13"
version = "0.9.14"
edition = "2024"
rust-version = "1.87"
description = "Local proxy for AI coding tools (Claude Code, Codex CLI, Aider): cache-aware cost tracking, path/command security checks, daily budget enforcement. Zero telemetry."
Expand Down Expand Up @@ -204,6 +204,14 @@ path = "tests/unit/observe_test.rs"
name = "audit_cli_test"
path = "tests/integration/audit_cli_test.rs"

[[test]]
name = "audit_test"
path = "tests/unit/audit_test.rs"

[[test]]
name = "torture_test"
path = "tests/integration/torture_test.rs"

[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
Expand Down
8 changes: 8 additions & 0 deletions dist-workspace.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ ci = "github"
# npm -> an npm package using the esbuild optionalDependencies layout
# msi -> a native Windows installer
installers = ["shell", "powershell", "homebrew", "npm", "msi"]
# Install to the SAME directory the hand-written README installer (install.ps1
# / install.sh) uses and persists on PATH (L-C3). Without this, cargo-dist
# defaults to $CARGO_HOME/bin, so `burnwall upgrade` (which runs the dist
# installer) wrote the new binary to a *different* dir than the running one —
# leaving the restart pointed at the old path, a second PATH entry, and an
# autostart Run-key aimed at a now-stale exe. One canonical dir removes the
# whole class.
install-path = "~/.burnwall/bin"
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
# Where the Homebrew formula is published (the existing tap repo).
Expand Down
2 changes: 1 addition & 1 deletion editor/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "burnwall",
"displayName": "Burnwall",
"description": "Cost + security for your AI coding agents, at a glance — reads your local Burnwall CLI.",
"version": "0.9.13",
"version": "0.9.14",
"publisher": "intbot",
"license": "FSL-1.1-MIT",
"repository": { "type": "git", "url": "https://github.com/intbot/burnwall" },
Expand Down
30 changes: 27 additions & 3 deletions editor/vscode/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
export interface StatusJson {
total_cost_usd?: number;
combined_total_usd?: number;
proxy_running?: boolean;
env_routing?: string;
blocked_requests?: number;
security_events?: number;
budget?: { daily_limit_usd?: number; spent_today_usd?: number };
Expand Down Expand Up @@ -62,6 +64,9 @@ export interface StatusSummary {
plan: PlanSummary | null;
/** Per-tool coverage; empty when no supported tools are installed. */
coverage: CoverageItem[];
/** True when the env routes to the proxy but the proxy process is not
* running — every request from that environment will fail (U-C1). */
proxyDown: boolean;
}

/** "time until" label for a reset countdown: `45m`, `2h28m`, `2d7h`, `now`. */
Expand Down Expand Up @@ -98,7 +103,9 @@ function planSummary(s: StatusJson): PlanSummary | null {
primaryResetInSecs: primary.reset_in_secs,
secondaryLabel: secondary ? secondary.label : null,
secondaryPct: secondary ? secondary.utilization * 100 : null,
throttled: prov.status !== "allowed",
// Only positively-throttling statuses — Anthropic emits warning-grade
// intermediates (`allowed_warning`) while requests still succeed (U-H4).
throttled: ["throttled", "rejected", "blocked", "rate_limited"].includes(prov.status),
};
if (!best || cand.primaryPct > best.primaryPct) {
best = cand;
Expand All @@ -108,7 +115,10 @@ function planSummary(s: StatusJson): PlanSummary | null {
}

export function summarize(s: StatusJson): StatusSummary {
const costToday = s.combined_total_usd ?? s.total_cost_usd ?? 0;
// Headline figure: the proxied total. `combined_total_usd` is now deduped
// server-side (X4), but proxied spend is the number Burnwall can vouch for;
// the combined figure is detail for the panel, not the bar.
const costToday = s.total_cost_usd ?? s.combined_total_usd ?? 0;

let cacheRead = 0;
let promptTotal = 0;
Expand Down Expand Up @@ -140,12 +150,18 @@ export function summarize(s: StatusJson): StatusSummary {
budgetPercent,
plan: planSummary(s),
coverage,
proxyDown: s.env_routing === "proxied" && s.proxy_running === false,
};
}

/** One-line status-bar label (VS Code `$(icon)` codicons allowed). On a
* subscription, dollars are notional, so the binding limit window leads instead. */
export function statusBarText(s: StatusSummary): string {
// Routed at a dead proxy beats every other message: the user's tools are
// actively failing with connection-refused right now (U-C1).
if (s.proxyDown) {
return "$(error) Burnwall proxy DOWN — run `burnwall start`";
}
const bypassed = s.coverage.filter((c) => c.state === "bypasses");
const bypassPart =
bypassed.length > 0
Expand Down Expand Up @@ -205,14 +221,22 @@ export function tooltip(s: StatusSummary): string {
s.cacheHitRate !== null
? `Cache hit rate: ${Math.round(s.cacheHitRate * 100)}%`
: `Cache hit rate: n/a`;
// On a flat-rate plan the dollar figure is notional (API-equivalent), not a
// bill — label it so a subscriber doesn't read it as money owed.
const costLine = s.plan
? `Cost: $${s.costToday.toFixed(2)} (notional — flat-rate plan)`
: `Cost: $${s.costToday.toFixed(2)}`;
const lines = [
"Burnwall — today",
`Cost: $${s.costToday.toFixed(2)}`,
costLine,
budgetLine,
cacheLine,
`Blocked requests: ${s.blocked}`,
`Security events: ${s.securityEvents}`,
];
if (s.proxyDown) {
lines.splice(1, 0, "⛔ PROXY DOWN — tools routed here will fail to connect. Run `burnwall start`.");
}
if (s.plan) {
const p = s.plan;
lines.push(
Expand Down
43 changes: 41 additions & 2 deletions editor/vscode/test/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ test("summarize computes cost, blocked, cache hit rate, and budget %", () => {
assert.equal(Math.round(s.budgetPercent ?? 0), 35);
});

test("combined_total_usd is preferred over total_cost_usd", () => {
test("the bar headlines the proxied total, not the combined figure (X4/U-H3)", () => {
// The proxied number is what Burnwall can vouch for; combined (proxied +
// unproxied logs) is panel detail, and previously double-counted proxied
// Claude Code into the headline.
const s = summarize({ total_cost_usd: 1, combined_total_usd: 5 });
assert.equal(s.costToday, 5);
assert.equal(s.costToday, 1);
});

test("no tokens -> null cache hit rate; no limit -> null budget %", () => {
Expand Down Expand Up @@ -99,6 +102,42 @@ test("subscription plan: throttled flag surfaces", () => {
assert.ok(statusBarText(s).includes("throttled"));
});

test("warning-grade plan status is NOT throttled (U-H4)", () => {
const s = summarize({
plan: {
providers: [
{
provider: "anthropic",
status: "allowed_warning",
windows: [{ label: "5h", utilization: 0.85, reset_in_secs: 600 }],
},
],
},
});
assert.equal(s.plan?.throttled, false);
assert.ok(!statusBarText(s).includes("throttled"));
});

test("routed at a dead proxy beats all other status (U-C1)", () => {
const s = summarize({
total_cost_usd: 2,
env_routing: "proxied",
proxy_running: false,
});
assert.equal(s.proxyDown, true);
assert.ok(statusBarText(s).includes("DOWN"));
assert.ok(tooltip(s).includes("PROXY DOWN"));
});

test("proxy running while routed is not flagged down", () => {
const s = summarize({
total_cost_usd: 2,
env_routing: "proxied",
proxy_running: true,
});
assert.equal(s.proxyDown, false);
});

test("coverage: a bypassing tool warns in the status bar and tooltip", () => {
const s = summarize({
total_cost_usd: 2,
Expand Down
2 changes: 1 addition & 1 deletion packaging/mcp/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"url": "https://github.com/intbot/burnwall",
"source": "github"
},
"version": "0.9.13",
"version": "0.9.14",
"packages": [
{
"registryType": "oci",
Expand Down
Loading
Loading