HKUDS/Vibe-Trading / Chapter 4

Programming /

E03_Bounded_Autonomy

# Bounded Autonomy: Five Elements That Scale From $100 to $100M > A broker API is not a tool. It is a custodian. Wiring an LLM into one is not tool use — it is delegated authority — and the pattern that makes it safe is not a smarter prompt, it is a five-element architecture that scales by changing only the limits. ## Key Takeaways - The bug moves downward. A model that respects a system prompt is not the same as a model that respects a mandate; the difference is enforced by a structural pre-trade gate, not by reasoning. - Bounded autonomy is five elements, in this order: mandate, kill switch, fail-closed pre-trade gate, audit ledger, persistent runner with auto-expiry. Same five, from $100 paper accounts to $100M live books — only the limits change. - The structural per-broker paper/live guard is the most under-discussed element. Brokers without a runtime discriminator (Longbridge, Dhan, Shoonya) are capped at paper + read-only by architecture, not by configuration. - The same shape shows up one layer up, inside the research workflow. Mandate ≅ research-goal claim. Kill switch ≅ graceful cancel. Pre-trade gate ≅ signal-engine pre-flight. Audit ledger ≅ run cards. Auto-expiry ≅ hypothesis registry invalidation. --- Here is a sentence that, once you read it, you cannot unread: *a broker API is not a tool.* In the Vibe-Trading connector package under `agent/src/trading/connectors/`, ten brokers are wired into the agent loop: `ibkr`, `robinhood`, `tiger`, `longbridge`, `alpaca`, `okx`, `binance`, `futu`, `dhan`, `shoonya`. Each exposes something that looks like a tool — a function call, a JSON schema, a tool registry entry. Each is registered through MCP. Each is reachable from the same `trading_place_order` interface that the agent loop dispatches to. The shape is identical to the eighty-five other tools in the system. The *meaning* is not. When an LLM agent calls `get_market_data("NVDA", days=30)`, the worst case is a hallucinated price that the user notices on the next prompt. When the same agent calls `trading_place_order("NVDA", qty=10000, side="buy")`, the worst case is a real market order that fills at the wrong price, against the wrong account, at the wrong time, in an amount the user did not authorize. The function-call surface is identical. The blast radius is not. Treating the second call as the first is the default in most agent frameworks I have read. It is the bug Vibe-Trading set out to fix. The fix has a name. **Bounded autonomy.** Five elements, in a specific order, each assumed by the next. The order is the architecture. Pull requests 2026-05-29, 2026-06-02, 2026-06-05, 2026-06-07, and 2026-06-09 layer them in. Once you see the pattern, you see it everywhere: in the swarm worker MCP trust boundary, in the signal-engine pre-flight, in the research-goal lifecycle, in the run-card hash chain. Bounded autonomy is the dominant motif in the codebase. It is the answer to "how does an LLM earn the right to talk to something that matters?" ## The five elements, named The five elements are not a checklist. They are a stack, and the stack has a direction. Skip any element and the structure is unsafe. Reorder them and the structure does not compose. **1. Mandate.** The user commits, before the agent runs, to a set of limits: symbol universe, order size, exposure cap, leverage cap, daily-trade cap. The mandate is not a prompt. It is a value the runtime reads before every order is dispatched. The user can change the mandate at runtime; the agent cannot. In Vibe-Trading, the mandate is exposed as a connector profile (paper vs live × broker) and is the same object across CLI, REST, and MCP — the connector-first architecture that landed in the May 31 PR. **2. Filesystem kill switch.** An instant halt. A file on disk. No graceful shutdown, no in-flight cancellation ceremony, no "let me finish this order first." When the file exists, the agent stops placing orders. When the file is removed, the agent resumes (subject to the mandate's auto-expiry). The kill switch is local, instant, and trivially auditable. It is the single most important element, because it is the one the user reaches for in an incident. The fact that it is a file — not an API call, not a websocket, not a CLI flag — is a deliberate architectural choice. A file works when the network is down. A file works when the API is rate-limiting. A file works when the user is panicking. **3. Fail-closed pre-trade gate.** Every order, before it leaves the connector, is validated against the mandate. Symbol in the universe? Order size within cap? Exposure within cap? Leverage within cap? Daily count under cap? If any check fails, the order is rejected. The gate is *fail-closed*: an exception in the gate means no order, not "let the order through and log the failure later." The gate is the same code path regardless of which broker the connector wraps. The gate is structural — it cannot be bypassed by a clever prompt, because the gate runs after the LLM returns an order and before the broker receives it. **4. Audit ledger.** Every order, every gate check, every mandate change, every kill-switch flip is logged immutably. The ledger is append-only, written with `flush + fsync`, and queryable for forensic analysis. The audit trail is what makes the system defensible to a compliance team. It is also what makes the system debuggable when something goes wrong — and something always goes wrong, because that is what production means. Sessions write `flush + fsync` each append (PR #147, 2026-05-30) so that AI responses survive mid-write crashes. The agent does not just produce answers. It produces answers you can re-trace. **5. Persistent runner with auto-expiry.** A long-lived autonomous loop that wakes on a schedule, checks the mandate, and places orders within the mandate. The runner has an *auto-expiry* on the mandate: after a configurable duration (default: end of trading day, or one week for research workflows), the mandate expires and the runner halts. Auto-expiry is the answer to "what if I forget to turn it off?" The answer is: you do not have to remember. The system forgets on your behalf. ```mermaid flowchart TB M["1. Mandate<br/>symbol universe · order size · exposure · leverage · daily cap"] K["2. Filesystem kill switch<br/>instant halt · no graceful ceremony"] G["3. Fail-closed pre-trade gate<br/>validates every order against mandate"] A["4. Audit ledger<br/>flush+fsync · append-only · queryable"] R["5. Persistent runner with auto-expiry<br/>scheduled wake · mandate expires"] M --> G K --> G G --> A R --> M R -.expires.-> Halt["Halt"] ``` The arrows in that diagram are intentional. The mandate *flows into* the gate. The kill switch *also flows into* the gate — either can veto an order. The gate *flows into* the ledger; every accepted order is recorded. The runner *reads* the mandate and *expires into* halt. The shape is a directed graph, and the

Chapter 4 of 5 13m Article Audio Learning path

Bounded Autonomy: Five Elements That Scale From $100 to $100M

A broker API is not a tool. It is a custodian. Wiring an LLM into one is not tool use — it is delegated authority — and the pattern that makes it safe is not a smarter prompt, it is a five-element architecture that scales by changing only the limits.

Key Takeaways

  • The bug moves downward. A model that respects a system prompt is not the same as a model that respects a mandate; the difference is enforced by a structural pre-trade gate, not by reasoning.
  • Bounded autonomy is five elements, in this order: mandate, kill switch, fail-closed pre-trade gate, audit ledger, persistent runner with auto-expiry. Same five, from $100 paper accounts to $100M live books — only the limits change.
  • The structural per-broker paper/live guard is the most under-discussed element. Brokers without a runtime discriminator (Longbridge, Dhan, Shoonya) are capped at paper + read-only by architecture, not by configuration.
  • The same shape shows up one layer up, inside the research workflow. Mandate ≅ research-goal claim. Kill switch ≅ graceful cancel. Pre-trade gate ≅ signal-engine pre-flight. Audit ledger ≅ run cards. Auto-expiry ≅ hypothesis registry invalidation.

---

Here is a sentence that, once you read it, you cannot unread: *a broker API is not a tool.*

In the Vibe-Trading connector package under agent/src/trading/connectors/, ten brokers are wired into the agent loop: ibkr, robinhood, tiger, longbridge, alpaca, okx, binance, futu, dhan, shoonya. Each exposes something that looks like a tool — a function call, a JSON schema, a tool registry entry. Each is registered through MCP. Each is reachable from the same trading_place_order interface that the agent loop dispatches to. The shape is identical to the eighty-five other tools in the system. The *meaning* is not.

When an LLM agent calls get_market_data("NVDA", days=30), the worst case is a hallucinated price that the user notices on the next prompt. When the same agent calls trading_place_order("NVDA", qty=10000, side="buy"), the worst case is a real market order that fills at the wrong price, against the wrong account, at the wrong time, in an amount the user did not authorize. The function-call surface is identical. The blast radius is not. Treating the second call as the first is the default in most agent frameworks I have read. It is the bug Vibe-Trading set out to fix.

The fix has a name. Bounded autonomy. Five elements, in a specific order, each assumed by the next. The order is the architecture. Pull requests 2026-05-29, 2026-06-02, 2026-06-05, 2026-06-07, and 2026-06-09 layer them in. Once you see the pattern, you see it everywhere: in the swarm worker MCP trust boundary, in the signal-engine pre-flight, in the research-goal lifecycle, in the run-card hash chain. Bounded autonomy is the dominant motif in the codebase. It is the answer to "how does an LLM earn the right to talk to something that matters?"

The five elements, named

The five elements are not a checklist. They are a stack, and the stack has a direction. Skip any element and the structure is unsafe. Reorder them and the structure does not compose.

1. Mandate. The user commits, before the agent runs, to a set of limits: symbol universe, order size, exposure cap, leverage cap, daily-trade cap. The mandate is not a prompt. It is a value the runtime reads before every order is dispatched. The user can change the mandate at runtime; the agent cannot. In Vibe-Trading, the mandate is exposed as a connector profile (paper vs live × broker) and is the same object across CLI, REST, and MCP — the connector-first architecture that landed in the May 31 PR.

2. Filesystem kill switch. An instant halt. A file on disk. No graceful shutdown, no in-flight cancellation ceremony, no "let me finish this order first." When the file exists, the agent stops placing orders. When the file is removed, the agent resumes (subject to the mandate's auto-expiry). The kill switch is local, instant, and trivially auditable. It is the single most important element, because it is the one the user reaches for in an incident. The fact that it is a file — not an API call, not a websocket, not a CLI flag — is a deliberate architectural choice. A file works when the network is down. A file works when the API is rate-limiting. A file works when the user is panicking.

3. Fail-closed pre-trade gate. Every order, before it leaves the connector, is validated against the mandate. Symbol in the universe? Order size within cap? Exposure within cap? Leverage within cap? Daily count under cap? If any check fails, the order is rejected. The gate is *fail-closed*: an exception in the gate means no order, not "let the order through and log the failure later." The gate is the same code path regardless of which broker the connector wraps. The gate is structural — it cannot be bypassed by a clever prompt, because the gate runs after the LLM returns an order and before the broker receives it.

4. Audit ledger. Every order, every gate check, every mandate change, every kill-switch flip is logged immutably. The ledger is append-only, written with flush + fsync, and queryable for forensic analysis. The audit trail is what makes the system defensible to a compliance team. It is also what makes the system debuggable when something goes wrong — and something always goes wrong, because that is what production means. Sessions write flush + fsync each append (PR #147, 2026-05-30) so that AI responses survive mid-write crashes. The agent does not just produce answers. It produces answers you can re-trace.

5. Persistent runner with auto-expiry. A long-lived autonomous loop that wakes on a schedule, checks the mandate, and places orders within the mandate. The runner has an *auto-expiry* on the mandate: after a configurable duration (default: end of trading day, or one week for research workflows), the mandate expires and the runner halts. Auto-expiry is the answer to "what if I forget to turn it off?" The answer is: you do not have to remember. The system forgets on your behalf.

flowchart TB
    M["1. Mandate<br/>symbol universe · order size · exposure · leverage · daily cap"]
    K["2. Filesystem kill switch<br/>instant halt · no graceful ceremony"]
    G["3. Fail-closed pre-trade gate<br/>validates every order against mandate"]
    A["4. Audit ledger<br/>flush+fsync · append-only · queryable"]
    R["5. Persistent runner with auto-expiry<br/>scheduled wake · mandate expires"]

    M --> G
    K --> G
    G --> A
    R --> M
    R -.expires.-> Halt["Halt"]

The arrows in that diagram are intentional. The mandate *flows into* the gate. The kill switch *also flows into* the gate — either can veto an order. The gate *flows into* the ledger; every accepted order is recorded. The runner *reads* the mandate and *expires into* halt. The shape is a directed graph, and the direction is the architecture.

The paper/live discriminator

Here is the part that most frameworks get wrong. A broker without a structural paper/live discriminator is a broker that cannot safely take live orders. The discriminator has to live inside the broker's API surface, not in the user's configuration. If a careless user (or a careless agent) can flip paper → live with a config change, the architecture has not done its job.

Vibe-Trading distinguishes brokers by their discriminator. IBKR has TWS / IB Gateway demo accounts with a separate port — the discriminator is the host. Robinhood has OAuth + the demo flag — the discriminator is the token scope. Tiger has a paper endpoint — the discriminator is the URL. OKX, Binance, and Futu each have a testnet environment. Those five brokers get full bounded-autonomy support, including order placement. They are the brokers the agent can place live orders through.

Longbridge, Dhan, and Shoonya do not have a runtime discriminator. Pull request #181, merged 2026-06-05, caps them at paper + read-only. No order placement. No live trading. The architecture says: "this broker cannot prove which environment it is talking to, so we will not let the agent talk to it in a way that could be live." That is the structural guard. It is not configurable. It is not a prompt. It is a property of the connector. The agent cannot promote Longbridge to live trading by reasoning harder. It cannot, because the connector does not expose order placement to live.

This is the deepest cut in the bounded-autonomy pattern. Most agents treat "support for broker X" as a binary. Vibe-Trading treats it as a spectrum: live, paper + read-only, or not supported. The spectrum is a property of the connector, declared in code, enforced at the type level.

Imagine you are the engineer who has to defend this decision to your compliance team. The compliance team does not want a system prompt. They do not want a "be careful" instruction in the agent's CLAUDE.md. They want a structural guarantee: *the agent cannot place a live order through Longbridge because the connector does not expose order placement to live.* That sentence, delivered in a meeting, ends the conversation. The architecture is the policy. The code is the audit.

The connector-first architecture

Pull request 2026-05-31 introduced the *connector-first* pattern. Before it, the trading layer was a tangle of per-broker helper functions scattered across the agent loop, the CLI, the REST API, and the MCP server. Each surface had its own way of selecting a broker. Each surface had its own way of asking "is this paper or live?" Each surface had its own way of forgetting that selection between requests.

The connector-first pattern centralizes the selection. A trading_connections tool lists the selectable profiles. A trading_select_connection tool picks one. The selected profile is held in a single state object, shared across CLI, REST, and MCP. Every subsequent call — trading_account, trading_positions, trading_orders, trading_place_order — operates against the selected profile. The selection is explicit. The selection is shared. The selection cannot drift between surfaces.

This is the same architectural claim as the loader registry in chapter one. Centralize the chokepoint. Make every consumer go through it. The chokepoint enforces the contract. Centralizing connector selection eliminated a class of bugs identical to the swarm-grounding bugs in chapter one: divergent state across surfaces, leading to "the CLI said paper but the MCP said live" inconsistency.

The trust boundary did not exist before the connector-first pattern. It existed *implicitly*, scattered across the codebase, with each surface enforcing its own partial version of it. The PR made the boundary explicit. Now the compliance team can ask "what connector is selected?" and the answer is a single field in a single state object, with a single transition log.

The same shape, one layer up

I want to flag a pattern that I did not appreciate until I read the codebase three times. The bounded-autonomy five-element pattern shows up again inside the research workflow, in slightly different vocabulary, and the recurrence is not an accident.

| Broker layer | Research workflow | |---|---| | Mandate | Research-goal claim + acceptance criteria | | Filesystem kill switch | Graceful cancel (first Ctrl+C, force on second within 2s) | | Fail-closed pre-trade gate | Signal-engine pre-flight (interface validation before instantiation) | | Audit ledger | Run cards (run_card.json + run_card.md with config_hash + strategy_hash) | | Persistent runner with auto-expiry | Scheduled research executor with hypothesis-registry invalidation |

The patterns are isomorphic. The broker mandate is what the user commits to before the agent acts. The research-goal claim is what the user commits to before the agent investigates. The broker kill switch is the instant halt; the graceful cancel is the same shape at a different time scale. The pre-trade gate rejects orders that violate the mandate; the signal-engine pre-flight rejects engines that fail interface validation. The audit ledger records orders; the run card records research. The persistent runner expires the mandate; the hypothesis registry expires claims that no longer hold.

Why does the same pattern show up twice? Because the trust boundary does not end at the broker. The same logic applies to anything the agent can do that has consequences: a research conclusion that goes into a report, a backtest that informs a position, a hypothesis that drives a strategy. The agent is not just executing trades. It is *producing artifacts* that will be used downstream. Each artifact is a delegation of authority, and each delegation requires the same five-element architecture to be safe.

I came into this codebase assuming the bounded-autonomy pattern was a security feature bolted on top of the trading layer. It is not. It is a general pattern for any agent that has consequences. The codebase applies it consistently. Once you see it, you cannot unsee it.

The narrower claim

I am arguing that for any LLM agent that touches a consequential system — broker, production database, customer-facing action, research artifact — the safe pattern is five elements: mandate, kill switch, pre-trade gate, audit ledger, persistent runner with auto-expiry. The pattern is portable. The pattern scales from $100 to $100M by changing only the limits, not the architecture.

I am not arguing that all five elements are equally important. The kill switch is the one you reach for first. The pre-trade gate is the one that prevents the most incidents. The audit ledger is the one that makes the system defensible after an incident. The mandate is the one that lets the user sleep. The persistent runner with auto-expiry is the one that prevents "the agent ran all weekend and lost money." They are all necessary. They are not all equally visible.

I am also not arguing that this pattern is unique to Vibe-Trading. Other systems (Stripe's financial operations, Bloomberg's trader desktop, large-bank internal agent frameworks) have pieces of it. What is unusual is the consistent application across both the broker layer and the research workflow, with the same vocabulary, with the same enforcement style, with the same level of detail. The pattern is a habit, not a feature.

What this looks like in production

The bounded-autonomy pattern is enforced in agent/src/trading/ through:

  • A mandate file in ~/.vibe-trading/mandate.json (or equivalent profile), read before every order.
  • A kill-switch file at ~/.vibe-trading/KILL_SWITCH (or env var equivalent), checked before every order.
  • A pre-trade gate in the connector wrapper, fail-closed on any exception.
  • An audit ledger in ~/.vibe-trading/audit/, append-only, flush + fsync per write.
  • A persistent runner in agent/src/live/, with auto-expiry on the mandate.

The pattern is enforced structurally, not by prompt. The agent loop can be jailbroken, the system prompt can be overridden, the user can paste in adversarial context — and the order still has to clear the pre-trade gate. The mandate is the mandate. The gate is the gate. The architecture is the policy.

The same is true of the research-workflow layer. The signal-engine pre-flight (PR #149, 2026-05-30) validates LLM-generated engines against an interface contract before instantiation. The run card records every research artifact with config_hash + strategy_hash, making every run reproducible. The hypothesis registry (PR 2026-05-16) tracks claims with invalidation dates. The pattern is the same. The vocabulary is the same. The architecture is the policy.

The bug moves downward. The broker layer's bugs lived in the absence of a structural paper/live discriminator. The research workflow's bugs lived in the absence of an artifact trail. The fix is at the bottom — in the architecture, in the gate, in the ledger, in the kill switch. The five elements are not retrofits. They are load-bearing. Remove any one and the structure is unsafe; remove any two and it is unimplementable. The five elements were not added to make the system safe. They were specified to make the system possible.

The trust boundary does not end at the broker. The same shape shows up inside the research workflow, and that is the next layer.

---

References:

  • source/vibe-trading/agent/src/trading/connectors/ — ten broker connectors with structural paper/live guards
  • PR 2026-05-29 — Robinhood first canonical live MCP connector behind OAuth + mandate + kill switch
  • PR 2026-06-02 — six more connectors (Tiger, Alpaca, OKX, Binance, Futu, Longbridge)
  • PR 2026-06-05 — Dhan + Shoonya + structural per-broker paper/live rule (PR #181)
  • PR 2026-05-31 — connector-first architecture: shared profile across CLI/REST/MCP
  • PR #149 (2026-05-30) — signal-engine pre-flight interface validation
  • PR #147 (2026-05-30) — session flush + fsync for crash-safe audit trail
  • PR 2026-05-12 — run cards (run_card.json + run_card.md) for reproducible research artifacts
  • PR 2026-05-16 — hypothesis registry with invalidation policy

---

The trust boundary does not end at the broker. The same shape shows up inside the research workflow, and it is the operating surface — not a guard around the operating surface, but the surface itself. That is the next chapter.