Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Agent Harness

A production-minded end-to-end coding agent in TypeScript, built on the Bun runtime. It pairs a real model loop (OpenRouter tool-calling) with an isolated execution sandbox, hook-based policy enforcement, hard budget ceilings, OTLP telemetry, and GitHub PR publishing — all driven from a live terminal UI.

The agent runs in two modes:

  • Host mode — tools execute in a local repository (fast local iteration).
  • Sandbox mode — the entire session runs inside a disposable E2B VM; the finished work is pushed to a feature branch and opened as a pull request.

Highlights

  • Real agent loop — OpenRouter chat-completions with tool calling, per-turn usage accounting, and automatic history compaction at 150k tokens.
  • Rigorous tool surfaceread_file, edit_file, ripgrep, tree_sitter_symbols, run_shell, git, and full-state TodoWrite plan management. Tool definitions live in one registry consumed by the dispatcher, the model, and MCP.
  • Sandboxed by default — every tool call executes inside a fresh E2B VM, never on the harness host. Results are published as a pull request from an agent/<id> branch.
  • Policy as code — lifecycle hooks (PreToolUse, PostToolUse, SessionStart, SessionEnd, Stop, ...) receive JSON events on stdin and can block calls. Two built-in guards are non-disableable:
    • destructive rm -rf targets outside the worktree;
    • git push that could target the default branch (main/master) — PR branches only.
  • Hard budget ceilings — 4k completion tokens/turn, 200k session tokens, $5/session, 50 turns. No open-ended runs.
  • Defense-in-depth path safety — lexical checks plus realpath and symlink-aware confinement; every tool call passes through PreToolUse/PostToolUse.
  • OTLP telemetry — Langfuse-ready gen_ai.* spans exported over OpenTelemetry.
  • MCP StreamableHTTP server — the same tool surface exposed for other agents and IDEs.
  • Terminal UI — Ink/React: live plan pane, tool-call stream, budget meter, final summary, and PR link.

Tech stack

Area Choice
Runtime Bun (TypeScript, ESM)
Model loop OpenRouter chat/completions with function calling
UI Ink + React
Sandbox E2B (isolated Linux VMs)
Validation Zod, plus hand-rolled plan-state validation
Telemetry OpenTelemetry OTLP/HTTP → Langfuse
Interop MCP StreamableHTTP server
Tests bun test (34 tests across 10 suites), tsc --noEmit

Architecture

src/index.tsx  (CLI entry · Ink terminal UI)
      │
      ├── EventBus ───────────── plan / tool / usage / status / pr events
      ├── HookRunner ─────────── lifecycle hooks + built-in command guards
      └── runSession ─────────── orchestration (session.ts)
            │
            ├── host mode   → createToolSurface()      → local repo
            ├── sandbox     → E2B Sandbox              → createSandboxSurface()  → publishSuccess() → PR
            │
            └── createDispatcher(specs, hooks, events)  (dispatcher.ts)
                  └── runAgentLoop()  (model.ts · OpenRouter)
                        └── registry.ts → read_file · edit_file · ripgrep ·
                                           tree_sitter_symbols · run_shell ·
                                           git · TodoWrite

Flow for every model tool call:

  1. The model loop requests a tool call.
  2. The dispatcher emits PreToolUse; built-in guards (destructive rm -rf, push-to-main) plus any external hook may block it.
  3. The tool runs against the host surface or the sandbox surface.
  4. The dispatcher emits PostToolUse, records usage, and streams the call to the UI.

Getting started

Prerequisites: Bun, an OpenRouter API key, and (for sandbox mode) an E2B API key and a GitHub App installation token.

bun install

Host mode

export OPENROUTER_API_KEY=sk-... AGENT_MODEL=openai/gpt-5
bun run src/index.tsx run ./path/to/repo "Describe the task"

Sandbox mode

Pass a cloneable Git URL instead of a local path:

export OPENROUTER_API_KEY=sk-...
export E2B_API_KEY=...
export AGENT_GITHUB_INSTALLATION_TOKEN=...
bun run src/index.tsx run https://github.com/acme/widgets.git "Describe the task"

Press Ctrl-C to cancel. The terminal UI streams the plan, every tool call, usage, the final summary, and the PR URL on success.


Tool surface

Tool Description
TodoWrite Replaces the complete plan state (full-state only, no patches)
read_file Reads a file inside the repository as UTF-8
edit_file Replaces the first exact occurrence of oldText with newText
ripgrep Line-numbered repository search (falls back to grep -rn)
tree_sitter_symbols Top-level symbol listing via tree-sitter
run_shell Runs a shell command with a timeout (default 30s, max 120s)
git status / diff / commit / push (push refused on main)

All outputs are truncated to 4,000 characters. In sandbox mode every tool above executes inside the VM via src/sandbox.ts.


Security model

  • Path confinementcreateToolSurface resolves paths lexically against the repo root, then walks up to the nearest existing ancestor and compares realpaths, so a symlink inside the repo cannot reach outside. Absolute and .. traversal paths are rejected. (Host mode only; sandbox mode is fully isolated.)
  • Sandbox by default — sandbox mode requires a cloneable Git URL; local host paths are rejected. The worktree is created with git worktree add on an agent/<id> branch inside the VM.
  • Non-disableable guards (in src/hooks.ts, applied in PreToolUse):
    • destructiveCommandGuard — blocks rm -rf targets outside the worktree (~ and .. included).
    • pushToMainGuard — blocks git push targeting main/master, --all/--mirror, and ambiguous bare/remote-only pushes.
  • External policy hooks — a PreToolUse handler can block any call by printing {"block":true,"reason":"..."}. Hook failures are non-fatal.
  • Budget ceilings — tokens, dollars, and turns are hard limits enforced by the loop; exhaustion fails the session.

Hooks

Hooks fire as executable commands that receive a JSON event line on stdin. Set them with environment variables:

export AGENT_HOOK_SESSION_END='./scripts/session-end.sh'
export AGENT_HOOK_PRE_TOOL_USE='./scripts/policy.sh'
bun run src/index.tsx run ./repo "Task"

Supported events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Notification, SessionEnd, Stop.

PreToolUse handlers may return {"block":true,"reason":"..."} on stdout to reject a call; the built-in guards run in addition to any external handler. Stop writes a session trace to <repo>/.agent/traces/<session>.json (host mode) or .agent-traces/<session>.json (sandbox mode).


Plan state

TodoWrite manages the task plan as full-state JSON at .agent/state.json (host mode) or .agent/state.json inside the sandbox worktree. Each turn must replace the entire state; incremental patches are rejected. Todos carry pending / in_progress / done status and a free-form notes array. Persistence is atomic (temp file + rename) and validated before write.


Budgets and the model loop

The loop (src/model.ts) enforces:

  • 4,000 completion tokens per turn
  • 200,000 total session tokens
  • 150,000-token compaction trigger (older turns are summarized by the model once)
  • $5.00 per session
  • 50 turns

Exceeding any ceiling fails the session. Model usage is exported as OTLP gen_ai.* spans when Langfuse credentials are present (LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_OTLP_ENDPOINT or LANGFUSE_BASE_URL).


Success publishing

On success in sandbox mode, publishSuccess (src/success.ts) commits and pushes the agent/<id> branch, then opens a pull request using a GitHub App installation token (AGENT_GITHUB_INSTALLATION_TOKEN; repository contents + pull-request write permissions). The PR body contains the final plan and a bounded trace bundle.


MCP server

The same file/shell tool surface is exposed as a StreamableHTTP MCP server:

bun run mcp -- ./repo

Listens at http://127.0.0.1:3001/mcp (override with MCP_PORT).


Project layout

src/
  index.tsx       CLI entry point + Ink terminal UI bootstrap
  app.tsx         terminal UI (plan, tool stream, budget meter)
  session.ts      session orchestration: surface selection, dispatch, publishing
  model.ts        agent loop, budgets, history compaction (OpenRouter)
  registry.ts     tool definitions shared by dispatcher, model, and MCP
  tools.ts        host tool surface + path-confinement + output bounding
  sandbox.ts      E2B sandbox: VM lifecycle, worktree, sandbox tool surface
  hooks.ts        HookRunner, lifecycle events, command guards
  dispatcher.ts   tool routing, PreToolUse/PostToolUse, plan store
  plan.ts         TodoWrite tool, plan validation + atomic persistence
  events.ts       EventBus (buffer + replay for the UI)
  success.ts      PR publishing via GitHub App installation token
  telemetry.ts    OpenTelemetry OTLP export + gen_ai spans
  mcp-server.ts   StreamableHTTP MCP server for the tool surface
  *.test.ts       unit + integration tests (bun test)

Testing

bun run check   # tsc --noEmit
bun test        # 34 tests across 10 suites

Coverage includes path-escape attacks (host + sandbox), symlink escape, the destructive-command and push-to-main guards, end-to-end env-configured hooks, plan validation/persistence, dispatcher routing, and sandbox surface behavior.


License

MIT © 2026 itsmiladlotfi

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages