Working on Blinker
Blinker is a sandbox (blinker) and a coding agent built on it
(blinker-agent), whose whole reason to exist is safety. Every change
should leave them at least as safe, small and simple as they were.
Principles
- Security is paramount. Follow every best practice for safe Go:
- Check every error. Never discard one with
_unless the comment says why that is safe. Failures fail closed: deny, refuse, or stop. - Validate everything that crosses a trust boundary: model output and tool arguments, policy and config files, command output, server responses. The model is the adversary in the threat model: it may be confused or prompt-injected.
- The kernel sandbox is what enforces the policy. In-process checks decide what to ask the user and which sandbox mode to use; they are never the only thing standing between the model and a file. File tools do their I/O inside the sandbox too.
- Never interpolate untrusted text into shell commands, sandbox profiles or regular expressions without validating or escaping it. Pass arguments as argv, not through a shell.
- Strip terminal control sequences from anything shown to the user that came from the model, a file or a command, so it cannot fake a prompt. Approval prompts show the full script or diff, never a truncated one.
- Secrets (API keys) stay in blinker's process. They never reach a sandboxed command's environment, a log, or the model.
- Run
go vet ./...andgo test ./...before calling work done. The sandbox tests run against the realbwrap/sandbox-exec.
- Check every error. Never discard one with
- Keep dependencies minimal. Use the standard library. Add a
dependency only when doing without it is clearly worse, and say why in
the commit message. The current direct dependencies are Bubble Tea,
Bubbles, Lip Gloss,
x/ansi, andx/vtwith Ultraviolet (the TUI, and the terminal emulator for the sandbox command's box, all from Charm),unisegandgo-runewidth(text widths, as Bubbles measures them),golang.org/x/term, andgolang.org/x/net(its HTML parser, for Fetch). - Keep the codebase small. Don't overengineer: no speculative abstractions, options nobody asked for, or layers that exist "in case". Every line is attack surface and something a reviewer must read. Describe files with globs, not regular expressions. Regular expressions are fine for simple, specific pattern matching and extraction, but never in security-critical code (policy matching, sanitizing, parsing what decides access), where bugs hide in them: write that out plainly.
- Ask the user through the TUI. When information is required (a
setting, an approval, a choice), prompt for it in the TUI. Don't add
command-line flags or environment variables for it, and don't read from
stdin outside the TUI. The sandbox command,
blinker, asks only what its policy'shost askrules say to, in its box, or on the terminal when the command's output is redirected; it takes its policy as-p,--preview, and no other options unless asked for. - Use XDG directories. Configuration goes under
$XDG_CONFIG_HOME/blinker(default~/.config/blinker), state such as history under$XDG_STATE_HOME/blinker(default~/.local/state/blinker), on macOS too. Never write into the project except through the agent's tools. - File formats: history is stored as XML, settings as JSON
(
config.jsonfor the sandbox,agent.jsonfor the agent), and the access policies (policies/<name>.policy, the agent'sagent-safeandagent-unsafeamong them,policies/commands/<command>.policy, and each project'spolicies/projects/<project>/{safe,unsafe}.policy) as text with one rule per line, described in the README. Blinker edits policies line by line, so their comments survive. Tools are bash scripts with a header (tools/<Name>.sh); the built-in ones are ininternal/tooldef/tools/. The policiesmake installinstalls are inpolicies/.
Layout
cmd/blinker: the sandbox command, which runs a program under a policy, in a box of its own when run from a terminal (box.go).cmd/blinker-agent: the agent's entry point and subcommands.internal/config: the JSON settings files and XDG paths.internal/llm: the OpenAI-compatible chat client (streaming, tool calls).internal/policy: parsing and evaluating access policies.internal/tooldef: reading tool files, and the built-in tools' definitions.internal/sandbox: running commands under bubblewrap (Linux) or Seatbelt (macOS), each with its own network proxy, and previewing them (Linux:preview.go,apply.go). Everything that differs by OS is behind theplatforminterface (platform.go), implemented inbwrap.goandseatbelt.go; the few Linux-only system calls are insys_linux.go.internal/proxy: the allowlisting HTTP/CONNECT proxy.internal/pty: opening pseudo-terminals.internal/tools: the agent's tools: Read, Write, Patch, Safe_Bash, Unsafe_Bash, the built-in Commit and Fetch, and running the user's tools.internal/diff: line diffs and applying unified diffs.internal/diffview: showing unified diffs on a terminal, as delta does, for the agent's prompts andblinker --preview.internal/htmlmd: turning web pages into Markdown, for Fetch.internal/agent: the conversation loop.internal/history: saved conversations (XML).internal/tui: the Bubble Tea interface, including setup.