Two runtimes, one binary

July 24, 2026 · View on GitHub

TL;DR: the one fak binary plays three roles that "put fak in front of your agent" quietly conflates. fak serve is the gateway runtime — a long-lived server that governs model traffic (routing, cost caps, policy, quarantine, audit). fak serve --native is the agent application runtime — the same binary, but now hosting and running the agent loop itself (sessions, tools, subagents, streaming, resume), kernel-adjudicated. fak guard turns a harness you already run (Claude Code, Codex) into a governed client of a runtime. serve and guard are not two versions of the same thing and not two layers of a stack — they are a runtime and a client.

Concept served: the runtime/client naming spine (epic #3256, workstream A — disambiguation). This is the page the rest of the agent-runtime story hangs on.

Why this page exists

The onboarding line — "point your agent at fak serve" — works right up until someone asks the obvious follow-up: is fak serve an alternative to fak guard, a layer under it, or its server half? Nothing on the front page answers that, so the first real decision — what do I even run? — stalls. Worse, "runtime" is doing double duty: the thing that governs your model calls and the thing that runs your agent loop are different servers, and the word alone doesn't tell them apart. This page names the three roles so the choice becomes mechanical.

The three roles

RoleWhat it isfak surfaceMaturity
Gateway (inference) runtimea long-lived server governing model traffic: model routing, cost/context caps, the default-deny capability floor, result quarantine, the audit journalfak serveshipped — the primary path today
Agent application runtimea long-lived server that hosts and executes the agent loop: sessions, tool dispatch, subagents, streaming, resume — every step kernel-adjudicatedfak serve --native (drives the owned agent.RunArm loop)shipping — the emerging first-class surface (workstream B)
Clientthe harness or app that calls a runtime: Claude Code, Codex, or your own backend over an SDKfak guard turns a harness into a governed client; or repoint one base URL yourselfshipped — witnessed live in front of Claude Code, opencode, Codex

The load-bearing distinction is the first-vs-third row: a gateway runtime proxies the model calls your existing harness makes and adjudicates the tool calls it proposes — your harness still owns the loop. An agent application runtime owns the loop itself: it is the server that decides the next step, dispatches the tool, spawns the subagent, and streams the result, with the kernel on the call path for each one. --native is the flag that flips fak serve from the first to the second — from proxying someone else's turn to running the turn itself.

One binary, drawn out

  CLIENTS                        ONE fak BINARY                      UPSTREAM
  (call a runtime)               (plays two runtime roles)           (tokens)

  ┌──────────────────┐
  │ Claude Code      │           ┌──────────────────────────┐
  │ Codex / Cursor   │──repoint──▶│  fak serve               │
  │ your SDK backend │  base URL │  = GATEWAY runtime        │──▶ Anthropic / OpenAI
  └──────────────────┘           │  routes + governs model  │    Gemini / xAI /
        ▲                        │  traffic; adjudicates     │    Ollama / vLLM /
        │ fak guard              │  each proposed tool call  │    SGLang / llama.cpp
        │ wraps a harness        └──────────────────────────┘
        │ into a governed CLIENT              │
        │                                     │ add --native
        │                                     ▼
        │                        ┌──────────────────────────┐
        └────────────────────────│  fak serve --native      │
                                 │  = AGENT APPLICATION      │
                                 │  runtime: owns the loop   │
                                 │  (agent.RunArm), sessions,│
                                 │  tools, subagents, resume │
                                 └──────────────────────────┘

The same static binary is the gateway runtime, the agent runtime (with --native), and — via fak guard — the wrapper that makes your existing harness a governed client of it. Nothing here is a second process or a separate install.

Which do I run?

Your situationRunWhich role
I already run Claude Code / Codex and want it cheaper + governed, no rewritefak guard -- claudeyour harness becomes a client; guard starts a gateway runtime in-process
I have my own backend/SDK and want to govern its model callsfak serve + repoint the base URLgateway runtime, your code is the client
I want an always-on shared gateway for a fleetfak node installs fak serve as a servicegateway runtime
I want fak to host and run the agent loop, not just proxy my harnessfak serve --nativeagent application runtime
I just want to see the capability floor decide, no modelfak preflight …neither — the offline adjudicator

If you are not sure, you want the client + gateway path (fak guard): it is the shipped default and changes nothing about how your agent is written. Reach for the agent application runtime (--native) when you want fak to own the loop itself.

The analogy you already have

Readers coming from the LLM-infra world already hold this split under other names:

  • An AI gateway (Portkey, LiteLLM, an inference gateway) governs model traffic — that is the gateway runtime, fak serve.
  • A managed agent runtime / the Agent SDK hosts and executes the loop — that is the agent application runtime, fak serve --native.
  • The app that drives them is the client — for fak, a harness wrapped by fak guard, or your own SDK code.

fak's one contribution here is putting both runtimes behind one static Go binary with the same kernel on the call path, so the client-vs-runtime and gateway-vs-agent-runtime choices are flags, not separate stacks. → One binary is the whole surface.

Honest scope

  • The gateway runtime is the mature path. fak serve / fak guard is what is witnessed live today. Most adopters never leave the client-plus-gateway model, and that is the intended default.
  • The agent application runtime is emerging. fak serve --native is real — it drives the owned agent.RunArm loop on the live serve path — but it is the newer surface being packaged into a first-class runtime under workstream B. Treat it as shipping, not settled, and prefer the gateway path unless you specifically want fak to own the loop.
  • "Client" is a role, not a product. The same harness is a client of whichever runtime it points at; fak guard is just the one-command way to make it a governed one.

Enforcement boundary — what a client gets on its own loop

The three roles answer what do I run; the next question an adopter with their own loop asks is what does fak actually enforce on MY loop, and what do I lose by not letting fak guard own my process? The axis that decides it is request-arrival vs turn-end:

The gateway can refuse, throttle, reset, charge, or stop anything that arrives on the wire — trace-keyed, no harness required. It cannot compel a request your loop decided not to send. Everything welded to the end of a turn ("the model stopped — now what?") rides Claude Code lifecycle hooks, and those exist only when fak guard owns the process and installs them into the child's merged --settings.

Per mechanism, which boundary it fires on and whether it transfers to a bare repoint-the-base-URL client:

EnforcementRequest arrival (fak serve, over the wire)Turn end (needs fak guard process ownership)Anchor
Per-turn admission — run-state (pause/drain/stop) + turn/token budget, refused before the model runsyesbeginServedSessionTurn (internal/gateway/session_admit.go), fed by the decideSession hook (cmd/fak/main.go)
Hard spend cap (#3273) — a scope over its versioned budget gets a kernel-side 409 refusal, never a plea to the modelyes — spendBreach refuses the turn, chargeSpend debits post-response, on any served path (serve or guard-as-client)internal/gateway/session_admit.go, internal/gateway/spend_governor.go. Caveat: the governor must be attached via SetSpendGovernor; the fak serve CLI wiring is still open (#4859), so today attachment is programmatic, not a flag
Pace shaping — per-request min-gap delay and a max-tokens cap that can only lower what the client asked foryesservedSessionTurn.minGapMs, maxTokensFor (internal/gateway/session_admit.go)
Budget / coherence session resets — the human-like continue on token/context drain; a latched hard reset on the next admitted turnyesisBudgetResetReason, armCoherenceReset / consumeCoherenceReset (internal/gateway/session_admit.go)
Operator control + steer — the operator POSTs drain/stop or budget/pace changes; a steer lands at the next request boundaryyessteerSession (cmd/fak/main.go) enqueues on the a2achan bus
Confirm-token / reversibility gate — an irreversible action needs a fresh operator token, checked per requestyesReversibilityConfirmed / reversibilityGateVerdict (internal/adjudicator/decide.go)
Deny-all auto-continue — resume the agent past a turn the floor refused entirely (the deny-all false-stop fix)yes — Stop hookcmd/fak/guard_stophook.go, installed in cmd/fak/guard.go
Task-handoff gate — a clean Stop must carry a fak.task-handoff.v1 JSON (enforce for headless/fleet, auto-off attended-interactive)yes — Stop hookcmd/fak/guard_handoff_mode.go
Operator-directed gate — catches a headless turn ending by asking an absent human (#4951)yes — Stop hookguardOperatorDirectedEffectiveMode (cmd/fak/guard.go)
PreCompact — intercept the harness's context compactionyes — PreCompact hookinstallGuardPreCompactHook (cmd/fak/guard.go)
Supervised restart loop — on context-budget exhaustion, relaunch the child under the continuation trace with a carryover seedyes — guard relaunches the childnewGuardBudgetRestarter, --restart-on-budget (cmd/fak/guard.go)

Two honest footnotes, so no row outruns the code:

  • The residual weld is thinner than it looks. Even the turn-end give-up decision is mostly gateway-side already: the counter it keys on — consecutive deny-all turns proposing the identical refused action — lives in the gateway (denyAllSameSnapshot, internal/gateway/metrics_observe.go). Only the actuation (actually ending or continuing the harness's turn) is harness-bound; exposing that seam to non-guard clients is #5161.
  • The per-session cache-break gate is inert today. The counter, cause vocabulary, and Prometheus surface exist (internal/metrics/cache_break.go), but the #2915 prefix-mutation detector that would produce live events never landed — do not read it as a working gateway signal.

So the practical answer to "what do I lose by not running fak guard": nothing in the left column — admission, budgets, spend, pace, resets, steer, and the reversibility gate all bind a bare base-URL client, because they fire when the request arrives. What you lose is the right column: everything that reacts to your loop stopping — auto-continue past a false stop, the handoff and operator-directed gates, PreCompact, and the supervised restart — because the model-API wire never tells the gateway your turn ended. This table is the shape future seam findings should land in: one row per mechanism, one boundary, one anchor. (Driving tickets: #5152, #5153, #5161; captured by #5162.)

Where to go deeper