Releases: cortexkit/aft
v0.19.6
Highlights
Bash permissions
bash:
"*": deny
"terraform fmt *": allow
"terraform validate *": allow
"terraform plan *": allow
"terraform apply *": ask…where the agent was still running non-whitelisted commands. Investigation found four independent bypasses, all fixed in this release:
echoskipped permission asks. The Rust scanner exemptedechofrom the ask list via a special case OpenCode does not have. Removed.- Zero-
command-node inputs ran without asks. Pure redirects (> /tmp/x), variable assignments (FOO=bar), arithmetic (((i++))), test forms ([[ -f foo ]]),readonly,declare, subshells, and grammar-load failures all parsed cleanly but produced nocommandAST node — so the per-node loop iterated zero times and the ask list stayed empty. Now fail-closed: any non-empty input that produces zero asks emits a wildcard"*"bash ask. - Plugin-side
runAsknever executed.@opencode-ai/plugin@1.14.xreturnsEffect<void>fromctx.ask(), and AFT's bundledeffect@3.xrunner could not run an Effect 4 instance — every ask rejected with"Not a valid effect"before permission evaluation ran. AFT now uses the sameeffectruntime as the host SDK (peer dependency), so allow/deny decisions actually propagate. - OpenCode SDK peers bumped to
^1.14.39so deny rules are evaluated against the current permission model.
$SHELL respect on Windows
Mirrors OpenCode's resolution algorithm exactly (packages/opencode/src/shell/shell.ts):
$SHELLenv varpwsh.exepowershell.exe- Git-for-Windows
bash.exeauto-detected next togitonPATH cmd.exe/$COMSPEC
WindowsShell::Posix(PathBuf) is invoked as <shell> -c <command> and ships with the same wrapper / exit-marker mechanics as the PowerShell and cmd variants for both foreground and background bash.
Background bash on Windows now also propagates correctly through the new POSIX path: wrapper script written as .sh, invoked as <bash> <wrapper>, exit code captured via the standard trap + atomic-rename pattern.
ls rewrite preserves intent
Two related fall-through cases in crates/aft/src/bash_rewrite/rules.rs:
ls -l README.mdpreviously rewrote toread README.md, dumping file contents instead of showing size, mtime, permissions, owner. Now falls through to real bash so-lusers see realls -loutput.ls README.md(no flags) andread README.mdare also not equivalent:ls FILEechoes the filename,read FILEdumps contents. Stat the path; fall through when it's a regular file.ls NONEXISTENTfalls through too — bash'sNo such file or directorywording is what agents already know.
ls ., ls DIR, and ls (cwd) continue to rewrite to read for directory listings, where the semantics match.
Other fixes
- Windows background bash picks up the new
WindowsShell::Posixvariant correctly (the v0.19.5 enum refactor missed thebash_backgroundregistry).
Full Changelog:
v0.19.5...v0.19.6
v0.19.5
Highlights
This release is mostly a stability + product-quality patch on top of v0.19.4. Three notable user-visible changes:
- Solidity is now a first-class language for
aft_outline,aft_zoom, andast_grep_search/ast_grep_replace. ast_grep_*now returns actionable hints when an agent's pattern produces zero matches because of common mistakes (regex alternation in AST patterns, Rust match-arm pipes, trailing-colon Python defs, etc.) instead of silently looking like a clean no-op.aft doctor --fixcan now consent-gate ONNX Runtime cleanup so semantic search can recover from a corrupted or incompatible local install without manual surgery;--clearcan also offer to drop stale cachedaftbinary versions.
The release also fixes a correctness bug that made self-hosted semantic-embedding backends unusable, modernizes the install/update story, and brings the release CI in line with the PR-time CI used to gate every change.
New language support
- Solidity (
tree-sitter-solidity) — outline contracts, libraries, interfaces, modifiers, constructors, events, errors, structs, enums, and state variables;aft_zoomworks on any of those;ast_grep_searchandast_grep_replaceuse$(notµ) for meta-variables because$is a valid Solidity identifier character. Callgraph, imports, formatter, and checker integrations are intentionally not part of this drop.
AST-grep hints on zero matches
Both ast_grep_search and ast_grep_replace now ship server-side hint metadata when:
- a pattern fails to compile (
invalid_pattern); or - a pattern compiles but produces zero matches.
The hint engine recognizes patterns like foo|bar (regex-style alternation in an AST pattern), => { ... } | _ => { ... } (Rust match-arm pipes), trailing-colon Python def shapes, anonymous $$$ in rewrites, and several other common AST-vs-text mistakes. Both OpenCode and Pi render the same shared hint, so failures are now self-explaining instead of looking like silent no-ops.
Semantic search backends
Self-hosted embedding endpoints work again. The previous SSRF guard rejected 127.0.0.1 and localhost, which broke the default Ollama configuration. The configure-time validator now allows loopback for openai_compatible and ollama backends; the corresponding README docs were also added for backend selection, fingerprinting (changing backend / model / base_url rebuilds the index), and the trust boundary between user and project config.
Doctor improvements
aft doctor (the unified @cortexkit/aft CLI) gained two interactive remediation paths:
aft doctor --fixnow offers a guided ONNX Runtime cleanup. If a systemlibonnxruntimeis too old to satisfy AFT'sfastembedbuild, the bridge already skips it and falls through to AFT's managed download — but if the managed copy itself is corrupted, this command clears only AFT-managed ONNX paths (never system libraries) and lets the next startup re-download cleanly.aft doctor --clearcan additionally drop stale cached AFT binary versions from~/.cache/aft/bin/while keeping the active version.
The CLI also now tolerates stdout noise from the aft child process (per #29): one-shot bridge requests buffer non-JSON or malformed lines as diagnostics and only fail when too few valid protocol responses arrive, surfacing binary path, exit code, stderr tail, and sample noise lines instead of throwing a raw SyntaxError.
Install and update modernization
User-facing install flows are now npx-only — npx @cortexkit/aft setup, npx @cortexkit/aft doctor, npx @cortexkit/aft doctor lsp <file>. README, CLI help text, and the Rust ONNX error messages all use the new wording.
The auto-update path also moved off bun install. The OpenCode plugin's auto-updater now spawns npm install --no-audit --no-fund --no-progress because OpenCode populates ~/.cache/opencode/packages/<plugin>/ with package-lock.json, not bun.lock — so the previous code path either no-op'd (no bun.lock to touch) or generated a parallel bun.lock that drifted from npm's view of dependencies. Bun is still fully supported as a runtime; the package shebang stays #!/usr/bin/env node.
Release CI now gates on E2E
Tag pushes were previously skipping the full E2E matrix; only the basic test job gated build/publish. An e2e regression on main could ship to production through a tag push without ever running the e2e suite. Two changes:
_e2e-suite.ymlis a single reusable workflow defininge2e-linux(18 Docker scenarios) ande2e-windows(27 native scenarios).tests.ymlandrelease.ymlboth call it — single source of truth, no drift between PR-time and release-time E2E.- All five platform-binary build jobs and
publish-cratesnow depend on[test, e2e]. An E2E regression blocks the entire publish chain (npm packages, platform binaries, GitHub release, and crates.io).
The release workflow also caught up with tests.yml on its own image (ubuntu-22.04), Cargo cache (Swatinem/rust-cache@v2), and test invocation (cargo test --workspace). Release-time CI is now a faithful preview of PR-time CI on the same code, instead of a much-slower divergent run.
Quality and reliability
format::tests::resolve_tool_caches_*is no longer flaky on Linux runners. Both tests mutate the global tool-resolution cache; they now serialize through a tests-module-scopeMutexso one test'sclear_tool_cache()cannot wipe an entry the other test had just written.callgraph::tests::callgraph_watcher_{add,remove}_callerare no longer flaky on macOS. Watcher events have up to ~1s coalescing latency under load; both tests now poll for the watcher to catch up with a generous deadline instead of asserting after a fixed 500 ms sleep.- Remaining Clippy debt was cleared so
cargo clippy --workspace --all-targets -- -D warningsis now part of the green gate. scripts/release.shnow refreshesbun.lockautomatically as part of the version bump. Earlier releases could leavebun.lockout of sync with the syncedpackage.jsonversions; CI runs with--frozen-lockfilewould then fail at install. The release script also keepsCargo.locksynced.- Pre-existing Windows ARM64 Cargo job in
tests.ymlwas scoped to lib tests only and marked non-blocking until the older Windows integration tests get broader JSON-and-path-escaping cleanup. Linux unit tests, Linux Docker E2E, and Windows native E2E remain the authoritative green gates.
Closes
- #29 —
aft-clicrashed on stdout noise from the bridge child.
v0.19.4
Highlights
Background bash on Windows works for the first time. Foreground bash gets robust shell fallback. Watchdog correctness improvements apply to both Unix and Windows.
What's new
Issue #27 fix — foreground bash shell fallback (Windows)
Windows users on stripped SKUs (IoT LTSC, Server Core), restricted PATH inheritance, ASR/AppLocker policies blocking PowerShell — all now get bash via runtime shell selection. AFT walks pwsh.exe → powershell.exe → cmd.exe and retries with the next candidate when a spawn fails with NotFound. cmd.exe is always added as the floor; it lives in a Windows search-path location that ASR/AppLocker policies generally cannot remove.
Foreground bash also stops inheriting stdin from the bridge's NDJSON pipe — the original issue #26 deadlock root cause that survived earlier fixes only on a few SKUs.
Background bash on Windows (new feature)
Detached background bash now works on Windows with full Unix architectural parity:
- File-based wrappers (
.ps1/.batper task) eliminate cross-shell command-line quoting issues that dropped wrapper output silently. cmd.exeis preferred over PowerShell for bg-bash because PowerShell's detached-handle inheritance dropped wrapper stdout/stderr on Windows 11. cmd is reliable and!ERRORLEVEL!correctly captures the user command's real exit code.- PID-based replay survives the AFT process exiting; tasks resume cleanly on next bridge spawn.
- Watchdog detects the wrapper crashing without writing an exit marker — the task moves to
Failedwithin ~250ms instead of staying stuckRunninguntil the 24h timeout.
Watchdog correctness (cross-platform)
Tasks whose wrapper crashes — child segfault, OOM, signal — now terminate within one watchdog tick (~250 ms) and surface as Failed with exit_code=null. Previously they stayed stuck Running until the 24-hour task timeout.
storage_dir fallback fix (Windows)
The HOME env var is not set on Windows by default. AFT's storage_dir fallback chain (storage_dir | HOME | ".") was degrading to "." on Windows when no explicit storage_dir was configured, producing relative-path bg-bash exit-marker writes that failed under move /Y. The fallback now also checks USERPROFILE. Same pattern fixed in three call sites: bash_background, search_index, and semantic_index.
Test coverage
- New Windows e2e harness scenarios verify bg-bash on real Windows 11 ARM64 (via Parallels): cmd-wrapper exit-code recording, PATH-forced cmd fallback, runtime shell retry. 27/27 passing on the live VM.
- Cross-platform unit tests for the retry-loop logic (
try_spawn_with_fallback) and watchdog Failed-marking (reap_child). - Linux Docker E2E now includes a longer scenario-1 timeout for cold-cache QEMU runs.
CI
tests.ymlnow runs Cargo + Bun + Linux Docker E2E + Windows native E2E on every PR. Switched frombun --filtertobun run --cwd <path>for workspace builds (matches the working release.yml syntax). Made integration test helpers cross-platform socargo test --workspacepasses on Windows runners.
Verified scope
- ✅ Foreground bash with shell fallback (live Win11 ARM64)
- ✅ Background bash via cmd.exe wrapper (5 new e2e scenarios)
- ✅
!ERRORLEVEL!exit-code capture under cmd /V:ON - ✅ Watchdog Failed-marking on dead-PID-no-marker
- ✅ Linux x64 Docker E2E (18/18)
- ✅ Windows cross-compile clean
- ✅ 531 lib + 491 integration Rust tests on macOS
Not yet verified
- pwsh.exe (PowerShell 7+) end-to-end — not installed on the test VM. Code path is exercised by unit tests through
try_spawn_with_fallbackmocks. - Other Windows SKUs (Server Core, IoT LTSC) — theoretical based on shell-fallback architecture.
- Windows ARM64 native build —
ring/clangbuild deps still fail; use the cross-compiled x64 binary under WoW64 emulation.
v0.19.3
v0.19.3
A focused stability release closing four real production issues observed on a live developer machine: orphaned LSP processes, wasted bridge spawns when the host launches from $HOME, and a TUI status display that conflated cross-project disk usage with the current project.
Fixes
No more orphaned LSP children on shutdown
When aft received SIGTERM (plugin restart, e2e cleanup, OpenCode quit), the signal handler called process::exit directly without killing spawned LSP children. Direct children (typescript-language-server, biome lsp-proxy, etc.) and Node-shim grandchildren got orphaned to PID 1.
$HOME no longer accepted as a project root
When OpenCode Desktop or Pi launches the plugin from the user's home directory and a session has no stored project directory, the resolver was handing the plugin $HOME as the project root. Configuring against $HOME walks the entire user home tree (often hundreds of thousands of files), hits the 30s configure timeout, gets killed, and silently retries on every reload — wasting one full bridge spawn per host launch.
Two-layer guard:
- The eager-configure flow in both plugins now detects
$HOMEand skips with a clear log message:"Eager configure skipped: cwd=$HOME is the user home directory." BridgePool.getBridge()itself throws a typedHomeProjectRootErrorif it ever receives$HOME(defense-in-depth).
Subdirectories of $HOME remain valid project roots and pass through unchanged.
TUI sidebar shows the current project's disk size, not the cross-project total
Status disk.trigram_disk_bytes and disk.semantic_disk_bytes were summing recursively across the entire <storage>/index/ and <storage>/semantic/ directories — every project the user had ever opened. A 4.8 MB project could appear to use 16+ GB because a sibling project's cache was huge.
Both caches are partitioned per project by project_cache_key(project_root). Status now resolves the current project's key and sizes only that project's slice. The new disk.project_cache_key field also exposes the key so hosts can correlate disk numbers with on-disk cache directories.
Diagnostics
Bridge Spawning binary: ... and Bridge killed after timeout log lines now carry the triggering session ID, making it much easier to correlate plugin logs against agent traces. Lines that legitimately have no session attribution (eager configure, watcher events, startup) stay unprefixed — that's the correct semantics.
Coverage
This release adds 5 new tests:
- 3 LSP-child-registry tests covering
trackon spawn,untrackon graceful shutdown, anduntrackonDrop(regression guard for the orphan leak). - 10
$HOMEguard tests covering the helper function, the typed throw, the cached-entry-bypass case, and the subdirectory passthrough. - 2 status-disk-scope tests proving project A's status doesn't include project B's cache size, and that an empty project reports zero rather than the cross-project total.
All existing tests continue to pass: 522 Rust unit + 490 integration + 32 aft-bridge + 41 CLI + 380 Pi + 678 OpenCode = 2,143 tests, 0 fails.
Recommended cleanup
If you've been running OpenCode Desktop or Pi from $HOME on previous versions, your <storage>/index/ directory may contain a large $HOME-rooted index that is now permanently abandoned. To reclaim disk space:
# Inspect AFT cache sizes
du -sh ~/.local/share/opencode/storage/plugin/aft/index/*
# A 16 GB+ entry that doesn't match a project you actually use is the
# orphaned $HOME index — safe to delete.
rm -rf ~/.local/share/opencode/storage/plugin/aft/index/<hash>v0.19.2
What's new in v0.19.2
🐛 Bug fixes
Foreground bash hang on Windows (issue #26)
AFT's foreground bash was inheriting stdin from the long-running bridge process — and the bridge's stdin is the JSON-RPC protocol pipe from OpenCode. Any child process that tried to read from stdin (PowerShell Read-Host, git/npm credential prompts, package-manager confirmations, etc.) would block forever waiting for input that never came, manifesting as the 65s bridge transport timeout reported in the issue.
Background bash had Stdio::null() since day one — foreground bash didn't. That asymmetry produced the bug.
Fix in crates/aft/src/commands/bash.rs:
- Detach foreground bash's stdin with
Stdio::null()(matches background bash and OpenCode's native bash) - Pass
-NonInteractiveto PowerShell on Windows so prompts fail fast instead of hanging
Closes #26.
Windows ARM64 support (Prism emulation)
Three coordinated changes in @cortexkit/aft-bridge so the AFT plugin works on Windows ARM64 via Microsoft Prism (the built-in x64 emulator):
platform.ts: mapwin32-arm64 → win32-x64so the resolver and downloader pick the x64 binary that runs cleanly under Prism. We don't ship a native ARM64 build.downloader.ts: route through the sharedplatformKey()helper so cache-miss downloads also pick up the new mapping.onnx-runtime.ts: align ONNX Runtime arch with the AFT binary's arch (not Node'sprocess.arch). Node correctly reports arm64 but our x64 aft.exe under Prism panics ondlopenof native ARM64 ONNX. The map keeps ONNX in lockstep with the resolved binary.
Verified live on Windows 11 ARM64: aft.exe loads x64 ONNX Runtime, builds the semantic index, and serves all 16 tools.
🧪 Tests
New Windows E2E harness
Real-bridge end-to-end test for Windows (tests/windows-e2e/). Runs OpenCode + a mock LLM inside a Parallels VM against the just-built aft.exe and plugin dist, exercising the full plugin → bridge → child-process flow that the Linux Docker harness can't cover. Includes a dedicated Scenario 2b regression test for issue #26 — invokes Read-Host and asserts bash returns within its own timeout (no bridge transport hang).
22/22 PASS on Windows 11 ARM64.
Linux Docker E2E fixture fix
The Docker E2E mock-server was using the pre-v0.18 aft_outline({ directory: ... }) API; v0.18.x renamed it to target. The first scripted turn was silently failing, the bridge never spawned, and the harness's lenient exit-code checks masked it. Updated to use target — Docker E2E now goes from 16/17 → 18/18 PASS.
v0.19.1
Highlights
This release is a focused stability and ergonomics pass. Two reliability fixes deserve special attention if you're using experimental.bash.background or semantic_search:
- Background bash completion reminders no longer drop after the first wake. The
wakeFiredThisIdlegate suppressed every completion after the first one in any idle window — empirically about 1 in 5 reminders. Subsequent completions now each fire their own reminder, debounced through the existing coalescer. - Semantic Index no longer reports "failed" when ONNX downloads successfully. A startup race could spawn the eager-warm bridge before ONNX Runtime download finished, leaving that bridge without
ORT_DYLIB_PATH. Eager-warm now waits up to 60s for ONNX resolution before spawning, so semantic search is "ready" on the same plugin start instead of needing a second restart.
Changes
Bridge package (@cortexkit/aft-bridge)
- ONNX Runtime download now runs in the background instead of blocking plugin load. Plugin tools register immediately; semantic search lights up once the runtime resolves.
aft_zoomnow returns plain-text formatted output across both OpenCode and Pi via sharedformatZoomText. No more JSON-escaped newlines and quotes in agent-visible blobs.- URL fetch under Node v24 now correctly handles dual
lookupcallback shape withopts.all: true(Piaft_outline(url:)was failing withERR_INVALID_IP_ADDRESS). Bumped toundiciv8 with surfaced underlying fetch causes. - New
BridgePool.setConfigureOverride()API lets the plugin layer patch configure overrides on already-running bridges.
Plugin behavior
bashis now only hoisted when at least one ofexperimental.bash.rewrite,experimental.bash.compress, orexperimental.bash.backgroundis enabled. With all three off, the host's native bash tool is used unmodified. This avoids surprising users who have AFT installed but haven't opted in to bash hoisting.- Background-bash anti-polling guidance: tool descriptions and reminder text now describe completion delivery without prescriptive "end your turn" copy.
bash_statuspolling responses now repeat the same reminder rather than nudging the agent into a polling loop. - Bash rewrite footer is now terser:
Prefer 'read' tool over bash.instead of the previous verbose explanation. - Watcher invalidation now ignores read-only metadata events (
AccessTime,Permissions,Ownership,Extended). Read-only tools like Biome lint no longer invalidate trigram and symbol caches.
Logging
- Plugin log no longer renders LSP errors with four nested
[aft]tags.slog_*!macros and 14 source files were scrubbed of inlined prefixes; env_logger handles the outer tag exclusively. - LSP spawn failures are now cached per
(server_kind, workspace_root). One bad workspace root no longer logs an error on every edit — failed spawns log once per session.
Repo dogfooding
- Root
tsconfig.jsonplusbun-typesandtypescriptdevDeps soscripts/**/*.tsget full LSP coverage when working on the repo itself. (No user-facing impact.)
Polish
- Discord badge and section-nav link in README pointing at the cortexkit Discord (
https://discord.gg/DSa65w8wuf).
Known issues
- Issue #26 (Windows bash transport timeout at 65s) is not addressed in this release. Windows e2e harness work is still in progress; a fix will ship once we have real Windows reproduction in CI.
Full Changelog: v0.19.0...v0.19.1
v0.19.0
First release after v0.18.4, with substantial improvements across plugin reliability, performance, and tool surface.
Critical fixes
- Background-bash idle wakes preserved provider prefix cache — synthetic prompts now reuse the last assistant's
{providerID, modelID, variant}so cached prefixes survive across turn-end notifications, ignored messages, and configure-warning deliveries.
Performance
- Incremental semantic refresh — only re-embed files whose mtime+size changed instead of rebuilding the entire embedding set on every restart. On real OpenCode sessions, this collapses cold-restart cost from minutes to milliseconds.
- Symbol cache disk persistence — per-project symbol tables now persist under
<storage_dir>/symbols/<project_key>/symbols.bin. ~91% cache hit rate on second restart instead of full re-parse every spawn. - Async configure warnings —
configurereturns immediately. File-walk + language detection + missing-binary detection happen on a background thread and stream back asconfigure_warningspush frames. - Home-directory bridge hang fix — FSEvents watcher attach moved off the configure foreground. Launching OpenCode from
$HOMEno longer triggers a 30s configure timeout that wedges the bridge in a respawn loop. - Bash default timeout 30s — foreground bash now defaults to 30s with a workflow hint that tells the agent to use
bash({ background: true })for longer commands.
Pi: URL support for aft_outline and aft_zoom
Pi now mirrors OpenCode's URL-fetching behavior for remote docs.
aft_outline—targetacceptshttp:///https://URLs. Auto-detected by URL prefix.aft_zoom— new optionalurlparameter (mutually exclusive withfilePath). Multi-symbol zoom routes through the cached fetched copy.- Same DNS-pinned, SSRF-guarded fetcher OpenCode already uses. Cached under
<storageDir>/url_cache/with a 1-day TTL.
// Pi
aft_outline({ target: "https://platform.claude.com/docs/en/build-with-claude/prompt-caching.md" })
aft_zoom({ url: "https://...", symbol: "1-hour cache duration" })Tool surface refinements
aft_outlineunifiedtarget— file path, directory path, URL, or array of paths in one parameter (auto-detected). Replaces the previous mutually-exclusivefilePath/files/directoryshape.aft_deletetakesfiles: string[]— pass{ files: ["a.ts", "b.ts"] }for any number of files including one. Returns honest partial-success reporting when some deletes fail.[cmpaft]compressed-output marker — replaces the verbose(compressed by aft)suffix on bash output compression. Saves tokens on every compressed result the agent sees.- Pi background-bash completion via
steer— completion now reaches the agent between its tool batch and the next LLM call instead of waiting for full turn completion. - Workflow hints in system prompt — AFT plugins now inject token-efficient workflow guidance (
aft_outline→aft_zoomchains, URL fetching, long-running command patterns) into the agent's system prompt. Conditional on the actual configured tool surface.
Bug fixes (audit batch)
configure_warningssession routing — async push frames now carrysession_idend-to-end so multi-session OpenCode delivers warnings to the correct client.- Semantic refresh data preservation — transient embed-backend errors no longer drop existing cache entries or update file mtimes. Old embeddings stay until the next successful extraction.
- Prewarm generation guard — background prewarm threads use generation-guarded
set_project_root/load_from_diskso a stale thread can't repopulate the symbol cache after reconfigure. - Semantic addition detection — refresh now handles all cases (added/changed/deleted) and returns a
RefreshSummary. Previously, newly added files weren't picked up between restarts. aft_outlinedirectory honest signaling —walk_truncated,complete, andskipped_filesfields are preserved end-to-end.apply_patchdiff size gate — raised from 100 KB bytes to 5000 lines so large source files (e.g. 114 KB / 3084 lines) get proper diff rendering in the UI.
Architecture
@cortexkit/aft-bridgeshared package — bridge transport, pool, downloader, resolver, platform mapping, ONNX runtime, and URL fetcher all moved into one shared package. Both@cortexkit/aft-opencodeand@cortexkit/aft-piconsume from it. Single source of truth for transport behavior.- URL fetching consolidated —
fetchUrlToTempFile,cleanupUrlCache, and_isPrivateIpv4are now exported from@cortexkit/aft-bridgeso OpenCode and Pi share one DNS-pinning and SSRF-guarding implementation.
Install
bunx --bun @cortexkit/aft setupIf you've already installed:
bunx --bun @cortexkit/aft doctor— the doctor tool will detect the new version and prompt to clear caches if needed.
Upgrading
OpenCode and Pi plugins both pin to @cortexkit/aft-bridge@0.19.0 exactly. Existing installations pull in the new bridge automatically on next package update; no config changes required.
Full changelog: v0.18.4...v0.19.0
v0.18.4
v0.18.4
Security & correctness audit (29 fixes)
A full-codebase council audit found and fixed 29 issues across Rust and both plugins.
P0 — Security
- DNS-pinning SSRF: URL fetch now pins resolved IPs before following redirects (#1)
- Bash permission-scan parse failure no longer bypasses the permission gate (#2)
- Shared stdout
BufWriterfor background bash frames prevents interleaved output (#4) - Dangerous env vars (
LD_PRELOAD,DYLD_INSERT_LIBRARIES, etc.) blocked from bash rewrites (#5) - Private IPs rejected in semantic search
base_urlat configure time (#25)
P1 — Correctness
- LSP
didOpennotification now uses the validated path afterdelete_file(#6) aft_outlinedirectory mode skips symlinked directories to prevent loops (#7)- Search index cache files now include a CRC32 integrity check (#8)
- Background bash tasks detach cleanly on SIGTERM/SIGINT so completions survive bridge restart (#9)
- Ambiguous
move_symbolnow fails with a clear error instead of silently picking one (#10) appendContent(edit append mode) runs syntax validation after write (#11)- Active bridge lookup scoped by project root, not global (#13)
- OpenCode pool canonicalizes keys with
realpathSyncto avoid duplicate bridges (#14)
P2 — Robustness
- Background bash temp files include task ID for easier debugging (#12)
- Commands report
complete: true/falsehonestly per the tri-state protocol (#16) - Transaction rollback failures use a distinct
rollback_failederror code (#17) greppasses leading-dash patterns torgvia--to prevent flag injection (#18)- Bash rewrite regex patterns capped at 10 KB to prevent ReDoS (#19)
- Glob
edit_matchcheckpoints include the request ID for uniqueness (#20) - Bash permission paths canonicalized before lookup (#22)
- Large file ranged reads no longer rejected — clamps to file length (#23)
- Batched
aft_zoomsurfaces partial failures per symbol (#24) - Background task IDs use OS entropy (
getrandom) — format is nowbgb-<8hex>(#26) - File watcher filters by exact path component, not substring (#28)
bash_statusnow includesstderr_pathfor inspecting background task stderr (#29)- Pi bash hoisting decoupled from read hoisting (#30)
- Pi config migration preserves block comments (#31)
- RPC client retries on stale port file (#32)
Workflow hints — system prompt injection
Both OpenCode and Pi now inject a short ## Prefer AFT tools for token efficiency block into the agent system prompt. Sections are conditional on the registered tool surface:
- Web/URL access —
aft_outline({ url })→aft_zoom({ url, symbol })instead of fetching whole pages - Code exploration —
grep/aft_search→aft_outline→aft_zoominstead of chains ofreadcalls - Relationship questions —
aft_navigate(callers,impact,trace_to,trace_data) instead of grep + read chains (shown only attool_surface: "all") - Long-running commands —
bash({ background: true })+bash_status(shown only whenexperimental.bash.backgroundis enabled)
Always-on, no config toggle needed. Irrelevant sections are omitted automatically.
Bug fixes
- Windows build:
signal_hookgated behindcfg(unix)— Windows x64 binary now builds correctly - Test files missing
/// <reference path="../bun-test.d.ts" />directive fixed
v0.18.3
What's Changed
Bug Fixes
- Background bash status —
timed_outtasks now display correctly; the plugin was checking for"timeout"while Rust serializes as"timed_out" - Pi bash wake messages —
sendUserMessagenow passes{ deliverAs: "followUp" }to prevent "Agent is already processing" rejections mid-turn - HTML
aft_zoomsection content — heading symbols now return the full section body down to the next sibling heading, not just the heading line itself - Pi bash renderer —
renderResultnow shows last 25 lines of output, matching Pi's built-in bash behavior handle_appendsyntax validation —appendContentnow calls real syntax validation instead of hardcodingsyntax_valid: true- Glob
edit_matchformatting — glob edits now use the fullauto_formatpipeline; the separatewrite_format_onlypath is removed formatter_timeout_secsconfig — the timeout override now flows correctly from both plugin config loaders into Rustconfigure- Pi formatter result fields — hoisted mutation results now expose
formattedandformatSkippedReasonto agents formatter_excluded_pathskip reason — paths excluded by scoped formatter config are now distinguished from generic formatter errors
Logging
- Rust session-id logging — all Rust command-side log lines now include
[ses_xxx]for correlation; background threads capture session_id at spawn time - Plugin per-request session-id — OpenCode and Pi plugin tool dispatch logs carry session IDs via
sessionLog/Warn/Error() - Test log isolation —
bun testnow writes toaft-plugin-test.loginstead of the liveaft-plugin.logto prevent test noise in production logs - RPC call/result noise removed — sidebar and
/aft-statuspolling no longer floods the plugin log
Audit fixes (P0/P1)
- Stdout write race — watchdog thread and main loop now share a properly serialized output path
delete_fileLSP path — uses validated canonical path for LSP notification instead of raw user inputmove_symbolambiguous result — returnssuccess: falsefor ambiguous symbol matches instead of misreporting successatomic_writetemp name — includes task ID to prevent concurrent write collisions- OpenCode pool
realpathSync—normalizeKey()now resolves symlinks like the Pi pool, fixing duplicate bridge entries on macOS/var→/private/var - Random slug collision — background task IDs now use 64-bit entropy instead of 32-bit
v0.18.2
Bug fixes
TUI plugin failed to load on npm installs (sidebar + /aft-status missing)
The published v0.18.0 and v0.18.1 npm tarballs were missing src/logger.ts,
which is a transitive dependency of src/shared/rpc-client.ts and several
other shared modules used by the TUI plugin. When OpenCode loaded the TUI
plugin from the npm package, the import chain failed with module-not-found,
silently disabling the entire TUI plugin entry — both the AFT sidebar slot
and the /aft-status command disappeared.
Local dev installs (file:// or workspace path) were unaffected because
src/logger.ts was always present on disk regardless of the package files
manifest. The bug existed since the sidebar was first added in v0.18.0.
Fix: src/logger.ts is now included in the published files array so
all transitive imports resolve when the package is installed from npm.