Bring Your Own Agent — a local-first coding agent harness.
Point it at any model (OpenAI-compatible, Anthropic, Ollama, llama.cpp, LM Studio, a custom base URL), pick a prompt tier sized to that model, and let it run shell + file tools inside a bubblewrap sandbox whose policy you control.
No cloud service. No build step. No required runtime dependencies — the CLI uses Node built-ins
only; the optional TUI uses @opentui/core, which ships beside the release binary.
v0.2.2 — usable, and honest about it. The core, the provider layer, session persistence, the tool set, the sandbox, Obsidian-backed memory and the terminal UI are implemented and tested. The CLI and the TUI are the frontends that ship today. Web and Electron adapters are planned but not in this release; neither are MCP, subagents, or web fetch/search.
- Node.js >= 26 (the code runs directly through native TypeScript type-stripping; there is no build step and no transpiler).
- Linux with
bubblewrapfor sandboxing (apt install bubblewrap,pacman -S bubblewrap, …). On a machine without it, byoa still runs but commands are unwrapped — andstrict(the default) refuses to run instead of silently degrading.
Use a release artifact — not the source tree. Node does not type-strip files inside node_modules,
so a tarball of the TypeScript source is not a runnable install.
Standalone (no npm install; needs Node >= 26):
mkdir -p ~/.local/lib/byoa ~/.local/bin
tar -xzf byoa-0.2.2-linux-x64.tar.gz -C ~/.local/lib/byoa
ln -sf ~/.local/lib/byoa/byoa ~/.local/bin/byoa
byoa --versionKeep the extracted directory together: the launcher resolves @opentui/core from the
node_modules beside it. Copying the byoa file out on its own still runs the plain CLI, but
--tui will fall back.
npm-installable tarball:
npm install -g ./byoa-0.2.2-npm.tgz
byoa --versionBoth are attached to the releases page. npm run pack:release rebuilds them
from a checkout.
# no API key needed: the fake provider echoes and exercises the whole pipeline
byoa --provider fake -p "hello"
# a real model
byoa --provider deepseek --model deepseek-chat -p "explain this repo"
byoa --provider openai --model gpt-4o-mini
# local models (LM Studio / Ollama / llama.cpp all speak the OpenAI protocol)
byoa --provider lmstudio --model google/gemma-4-e4b
byoa --provider ollama --model qwen3-coder
# interactive
byoa
byoa --tuiInside the REPL: /help, /tier, /model [provider/]model, /cancel, /exit.
The TUI (--tui) is a calm, single-column layout: header, transcript, input, and one stats line
(tokens in/out, tok/s, elapsed, tool calls, memory hits, model/tier/sandbox). It is opt-in and
falls back to the plain CLI when stdout is not a TTY, TERM=dumb, or NO_COLOR is set.
TUI keys: Enter send · Ctrl+C cancel (or clear) · Esc cancel · Ctrl+T reasoning ·
Ctrl+O tools · PgUp/PgDn scroll · /help · /exit. Approvals: a allow, d/Esc deny.
Sessions are persisted and resumable:
byoa sessions
byoa --resume <id-or-title-substring>Configured in ~/.config/byoa/config.json (created on demand; run byoa providers to see what is
resolved). Keys are read from environment variables — openai uses OPENAI_API_KEY, anthropic
uses ANTHROPIC_API_KEY, and so on. A literal key in the config file is accepted but warns on
every run; prefer apiKeyEnv.
Any OpenAI-compatible endpoint works, including a custom base URL and headers.
| Tier | For | History budget | Output cap |
|---|---|---|---|
lite |
small/local models | 6k tokens | 2k |
normal |
cloud models (default) | 40k tokens | 8k |
large |
precision, cost is not the concern | 120k tokens | 16k |
The tier is picked from the model by default (--tier overrides). It scales the persona, the
context budget, the per-tool-result ceiling and the output cap as one decision.
The sandbox is a policy object first and a bwrap invocation second, so it can be inspected,
printed and tightened. Defaults:
- Filesystem is a read-allowlist, not a denylist. Only the roots you list (
/usr,/etc, …) plus the workspace are ever mounted.$HOME,/runand everything else simply do not exist inside the namespace. There is no--ro-bind / /. - The workspace is the one read-write mount. Because of that,
$HOME, its parent directory, and/are refused as workspaces unless you pass--allow-home; point it at a project directory instead. - Network is denied by default (
--unshare-all).--net allow-allshares the host network. - Environment is cleared and rebuilt from an allowlist, so API keys do not cross the boundary.
- No privilege escalation (
--unshare-allimplies no-new-privs;sudo/mountfail). - Limits are applied with
ulimitinside the spawned shell (bwrap 0.13 has no--rlimit-*).
Honest limitations, because a boundary you don't describe is a boundary you don't have:
- The command deny-list is a speed bump for destructive commands, not a security boundary. It parses shell words and checks command positions; obfuscation can defeat it. The namespace is the boundary.
--no-sandboxruns unsandboxed, with your environment and your filesystem. It warns loudly, and it is not the default.strict(default) refuses to run at all when bubblewrap is missing.--net allow-allexposes the host network, including loopback services.
Long-term memory is plain markdown in an Obsidian vault, indexed locally with SQLite FTS5 — no embeddings, no server, no database to babysit.
- Notes live in
<vault>/memory/byoa/with YAML frontmatter (tags,created,source) and a[[Inbox]]footer. - Namespaced: byoa can only read and write its own subfolder, so it never reads notes written by another agent or by you. A generic reader with an empty namespace sees the whole vault.
- The model gets two tools:
memory_recall(query, limit?, tag?)andmemory_remember(text, title, tag). - Recalled notes are budgeted per tier and injected as read-only context; lite tier skips auto-recall entirely.
- Writes are gated: empty text, unusable titles, or text that looks like it contains credentials are rejected with a reason.
memory.backend: "sqlite" switches to a plain SQLite store at ~/.local/share/byoa/memory.db
(no markdown files), with the same namespace behaviour.
~/.config/byoa/config.json (or --config), ~/.local/share/byoa/byoa.db for sessions (or
--db / BYOA_DB), and BYOA_VAULT for the Obsidian vault path.
npm test # tsc --noEmit, then the full suite
npm run test:only # suite only
npm run smoke # type-stripping import checkThe suite includes real bubblewrap isolation tests — they skip only when a working sandbox is
genuinely unavailable, and the probe actually creates a namespace rather than checking
bwrap --version.
MIT. See LICENSE.
AI-generated with OpenCode + DeepSeek V4.1 Flash. Human review, testing and direction by the repository owner.