Commits


ui: unveil ~/.fugu/skills before the startup skills read skills_load() ran in main() before any unveil, with the whole filesystem visible and symlinks followed -- so a symlink planted at ~/.fugu/skills/<name>/SKILL.md was an arbitrary-file read primitive: its first bytes were slurped into a model-visible skill body. Call the (previously dead) skills_unveil() first so the load is pinned to ~/.fugu/skills; unveil checks the resolved target, so an out-of-tree symlink is refused (EACCES) and the skill is silently skipped. In-tree skills are unaffected. The skills dir then joins the front-end's locked unveil set, read-only -- the user's own dir, no runtime code reads it. Behaviour change: skills symlinked OUT of ~/.fugu/skills no longer load (deliberate; that is the read primitive). regress/skills gains test_skills_unveil_confinement: a forked child (the unveil is process-wide and irreversible) plants /real plus /evil whose SKILL.md symlinks to a file outside the skills dir, then asserts skills_unveil + skills_load loads /real but never follows /evil. Verified it fails without the unveil (revert experiment). Found by an adversarial confinement audit of the curses UI worker after its runtime pledge was widened for the conversation drawer.


ui: conversation drawer to browse and switch sessions Add a side drawer to the curses UI (o in vi normal mode) listing past conversations newest-first with a preview and relative time. Tab moves focus between the drawer and the conversation; j/k + arrows select; Enter switches. A "[+ New conversation]" row starts a fresh session. Switching is a pure UI + journal operation -- net is stateless per turn (the UI resends the full projection), so the next turn carries the new history with no net/parent changes. Switching mid-turn soft-abandons the in-flight turn: the old journal gets a turn_failed marker, the UI drops the abandoned turn's late output (handle_msg gates on an `abandoning` flag, still answering M_TOOL_REQUEST so net can drain), and busy stays set until it finishes so a send in the new conversation waits. The drawer needs runtime file access (list/read/create journals), so the curses worker keeps rpath/wpath/cpath after startup instead of narrowing to stdio+tty; unveil still confines it to ~/.fugu/sessions. - journal_list(): scandir the sessions dir, newest-first, skipping empty files. - ui_curses.c: drawer state/render/input, the soft-abandon switch, the o/Tab rebinding (o no longer enters insert in normal mode). - regress/tui: seed two sessions, open the drawer, switch, assert the transcript becomes the other session's content. - CONTEXT.md: session / conversation / drawer / turn / projection glossary.


add clean-check pipeline: make check = clean-room build + full regress Wipes all build artifacts, does a fresh DEFAULT build, runs every regress suite from scratch, exits non-zero on any failure. This is the canonical verification step. Incremental builds bit us twice: a stale in-tree binary, and an FUGU_DEBUG build left in src/ that dies under pledge and hangs chat/compact/tui. Wired as the make check target, shipped in the distfile, and documented in AGENTS.md (Testing).


regress: cut test timeouts 60/90s -> 15s, document binary-flavor gotcha The whole suite completes in ~1s, so a 90s alarm backstop is absurdly long -- a genuine hang now fails in 15s (10s for the chat/compact stub self-alarm), still a 15x margin over normal runtime. Applied across all eight suites that arm an alarm (chat, compact, exec, fetch, imsg, net, sandbox, tui). Also adds .scratch/regress-debug-binary-gotcha documenting why chat/compact/tui hang/fail when src/ was last built FUGU_DEBUG=1 (they exec the in-tree binaries, which die under pledge), plus the stale top-level vs obj/ binary trap.


fix build warnings: drop dead resolve_secret, declare forkpty in tui test parent.c: resolve_secret was kept as a back-compat wrapper during the api-hardening sweep but every caller moved to secret_load -- remove it (-Wunused-function). tui_test.c: it included <util.h> for forkpty(3), but the build is -I.../src/common where fugu has its own util.h, which shadowed the system header; declare forkpty directly instead.


regress: tear down child processes on timeout instead of orphaning chat_test and compact_test fork a stub TLS server + the full fugu, then set alarm(90). on_alarm only _exit(97)d, orphaning both children -- the stub has no timeout of its own and blocks forever in accept()/tls_read(), so every stalled run leaked processes that ran until killed by hand. Track the child pids and SIGKILL them from on_alarm before exiting; also give the stub child its own alarm(60) + default SIGALRM so it self-terminates if orphaned by any other path. Verified: a forced stall (FUGU_BIN=/bin/cat) now leaves no live orphans.


ui: drop speaker labels, left-align message bodies prefix_of returns empty for BK_USER and BK_ASSIST, so user and assistant messages render with no "you"/"fugu" prefix and their bodies start at column 0. They stay distinguishable by weight (user bold, assistant normal) and the existing blank line between blocks; tool:/error: labels are kept. regress/tui now asserts the labels are absent instead of present.


fetch: http_allow wildcard for allow-all-minus-block posture acl_host_match treats a bare "*" token as matching any host, so http_allow "*" plus an http_block list gives an allow-all-minus-blocklist policy for the http_request tool without new config keys, parser, or imsg changes. The SSRF guard is unchanged (private/loopback/link-local still refused), so even wide open this brokered path stays far safer than allow_subprocess_net -- removing the incentive to reach for unguarded subprocess networking. Documents "*" in fugu.conf.5 with the exfiltration caveat (default stays default-deny; "*" is an explicit operator choice). Adds three regress/fetch cases: wildcard allows an un-listed host, block wins over the wildcard, and an unrelated block does not over-match. Full regress 710 -> 713, 0 failures. Issue: .scratch/http-allow-wildcard/


Import fugu: OpenBSD-native privsep AI coding agent Five pledge/unveil-sandboxed binaries (fugu parent + fugu-{ui,net,exec,fetch}), base system + vendored jsmn.h only. Multi-provider net worker (Anthropic + OpenAI-compatible wire protocols), vi-modal curses TUI with markdown rendering, runtime /model picker, SSRF-guarded fetch worker, sandboxed exec tools, and a user/model-invocable skills subsystem. Includes the regress suites, man pages, ADRs, and the .scratch issue tracker.