Skip to content

Architecture

The Candle Harness is one binary, candle-harness, that runs as several processes. One of them, the core, holds every agent key and is the only one that trades. Everything else attaches to it.

main view (candle-harness) ──┐
popped views ────────────┤
engine turn ─────────────┤── socket ──▶ CORE ──▶ candle mcp, one per agent key ──▶ Candle
(through candle-harness mcp)│ ├──▶ your candle CLI ──────────────────▶ Candle
prompt pane launcher ────┘ (a ticket) ├──▶ Hacc
└──▶ state/: keys, journal, socket
engine turn ──▶ your model provider, through the harness's own sign-in
prompt pane ──▶ your candle CLI, as you: owner steps, passphrases, funds out

Paths below are relative to the Candle Harness’s home, ~/.config/candle/harness/ (or CANDLE_HARNESS_HOME). The home and state/ are mode 0700.

The core (candle-harness core headless, or started by the first view). One per Candle Harness home, held by a lock (state/core.lock). Two homes on one account run two cores, each with its own limits and its own record. The core:

  • holds each agent’s Candle key, and runs Candle’s MCP server (candle mcp) once for each, with that key;
  • turns what an agent proposes into cards, values them fresh, checks them against limits, and runs them once approved, or at once where a rule below says so;
  • keeps the journal, the source of truth for what happened, rebuilt on start before the socket opens;
  • reads in the background for the panes, inside each key’s request budget;
  • serves the attach socket.

A core a view started for itself stays up while a view is attached, a turn runs, a trade is in flight or being read back, a card is open, a trigger is on, a rules bot enters or holds a position its exits watch, or a code bot is on, and exits a minute after none of those hold. candle-harness core runs until it’s stopped. candle-harness stop asks a running core to stop; one that can’t be asked is signalled by the pid in its lock, only once that pid is checked to be a core. candle-harness service install runs candle-harness core --service at login, as a LaunchAgent on macOS or a systemd user service on Linux, carrying PATH, CANDLE_HARNESS_HOME, LANG and any proxy and certificate variables, never a key; a core already running isn’t a failure there. On macOS, keep-awake runs caffeinate -i -w <core pid> while a trigger or bot is on.

The core also runs the user’s triggers (triggers.toml): it watches Candle’s market read for levels, Hacc’s stream for the tape and personal signals, its own journal for fills and cards, and the clock for schedules, and fires each one through the same paths as everything else (below). With no view attached, it shows the computer’s own notification itself (osascript on macOS, notify-send on a Linux desktop), never on a machine with no screen to show it on.

It runs the user’s rules bots too (bots.toml): each is a Hacc strategy definition, entered on Hacc’s full tape (a stream of its own, quality=all, apart from the curated one views and triggers read) and exited on Candle’s market read, through the same paths (below).

And it supervises the user’s code bots (bots.toml, kind = "code"): each approved bot’s command, from the approved version, run with no shell in its folder, in a process group of its own, with the view’s allowlisted environment (no CANDLE_* variable, no key, no inherited descriptor) plus the socket’s path and a fresh bot token. It’s restarted after a crash with a back-off, given up after five crashes in ten minutes, ended with its group when the core stops, and ended by the next core if a killed core left it running (only once its pid, start time and command line match; where the group’s leader has gone, only when a member carries the bot’s name and this core’s socket in its environment, else it’s left alone and journalled). Its output has control sequences removed and is redacted before it’s kept (state/bot-logs/, 0600, the last 5 MB); a private key printed over several lines is left out whole. Each start journals a hash of the bot’s own files (dependency folders are skipped; the lockfile stands for them), of its lockfile, and of the command’s program when that’s a file outside the folder, and says when the hash differs from the last start’s. Its token ends when its process is stopped, exits cleanly or is given up on. The supervisor doesn’t confine the program (see the trust boundary).

The Build pane runs the user’s own coding harness, as the user, in the bots folder: the view’s environment less Candle’s sign-in variables and the Candle Harness’s own, plus the socket’s path and a build token minted for that pane.

The code runner (research_run) runs one program from an agent’s list, with no shell, held by the operating system: on Linux by the binary itself as an exec stage (candle-harness run-held), which sets Landlock and a seccomp filter on itself and then becomes the program; on macOS by sandbox-exec with a generated profile. A run reads and writes the agent’s research folder, reads the runtime’s own install, has no network, and is stopped at 60 seconds (its whole group) and at 1 GB. The read grant never covers the home folder, ~/.config, ~/.config/candle or the Candle Harness’s home: a runtime that sits in one is refused (RUNTIME_PLACE), and one in a folder right in the home (~/Downloads/node) is granted as that one file. The held program starts with only its three standard streams. On Linux the filter refuses setsid and setpgid, so a script can’t leave its group, every call numbered for the x32 interface, unshare, clone with a namespace flag, bpf, perf_event_open, userfaultfd and the key calls, and gives clone3 ENOSYS; RLIMIT_FSIZE caps a file at 1 GB and RLIMIT_NPROC allows 128 processes past the user’s own. On macOS the runner follows the script’s descendants from spawn and ends them, and at the end stops any process left in that run’s own sandbox: each run’s profile carries a mark (two empty files, one it allows and one it refuses), and only a process macOS answers as held by that mark is taken. A run answers when the program ends, not when the last holder of its output lets go. It’s offered only where that hold is available, and only to an agent whose run list is set.

Views (candle-harness, or candle tui). The screens. A view draws what the core sends it, and sends back what you do: an approval, a typed trade, a click. It holds no key. candle-harness view --pane <pane> is a popped-out view of one pane. candle-harness doctor and the resource sampler attach as views too, as observers, and so do candle-harness ask and candle-harness trigger, which send one message or fire one on call trigger from a script, and candle-harness cards, which only lists cards and is refused any approval. The main view the person last used sends notifications while its terminal isn’t in front.

Engine turns. Each agent turn runs the user’s own harness (Claude Code, Codex, Grok, OpenCode or Pi) as a child of the core, one turn at a time:

  • in the agent’s research folder, research/<agent>/, with a private home of its own, so the user’s own setup of that harness stays out;
  • with the harness’s own sign-in, copied into the private home and handed back if the harness refreshed it;
  • reaching the core only through candle-harness mcp, a stdio MCP server the harness starts, holding a token for that one turn;
  • with each harness’s file tools held to the research folder by the harness’s own sandbox, the operating system, or a gate the Candle Harness adds (Engines has each one’s hold and its limits);
  • with the harness’s own web search and fetch, where it has them, unless the agent’s tool settings deny them.

The core keeps the conversation, so a turn can run on a different engine from the last.

The prompt pane (candle-harness cli-pane). Owner steps, and anything that asks for a passphrase or shows a secret, run in a terminal pane of their own:

  1. Approving such a card mints a single-use ticket for the approving view, valid for 120 seconds.
  2. The view starts the launcher in an embedded terminal and tells the core its pid. The core takes the ticket only from a connection whose peer process, as the kernel reports it, has that pid.
  3. The core checks the card still hashes to what was approved, then answers with the exact command. For a row run with an agent’s key, the answer carries that key, once, to the launcher, which passes it to candle as CANDLE_API_KEY.
  4. The launcher runs candle in the pane, as the user, and reports how it ended by its exit status.

What’s shown or typed in the pane doesn’t pass through the core, with three exceptions the launcher reads back on purpose: vault list’s addresses, labels and roles (as known destinations for funds out), whether the vault opened (vault status’s answer code), and a new key’s prefix. When an owner step makes an agent’s key, the launcher stores it straight into state/keys/, never on screen.

WhatWhereWho reads it
Each agent’s Candle keystate/keys/<agent>.key, mode 0600The core; the candle mcp and candle children it starts, through CANDLE_API_KEY in their environment; the prompt pane’s launcher, for a row run as that agent. A key made in the pane while the CLI profile holds no key of its own is also kept by the CLI as the profile’s working key, as the card says
The view tokenstate/view.token, mode 0600, replaced on every view’s helloAny process that can read the file: a view, once
A turn’s harness tokenIts hash in state/tokens.json (0600); the token itself in state/turns/<turnId>/token (0600) for the turn’s lifeThe turn’s MCP shim. Revoked, and its folder removed, when the turn ends
A code bot’s tokenIts hash in state/tokens.json; the token in the bot’s environment (CANDLE_HARNESS_BOT_TOKEN), or, for a bot run elsewhere, in .harness-bot.json (0600) in its folderThe bot. A new one is issued at each start or bots token, replacing the last; ended when its process is stopped, exits cleanly or is given up on, and when a new folder or command is approved for a bot run elsewhere; revoked when the bot is removed
A Build pane’s tokenIts hash in state/tokens.json; the token in the pane’s environment (CANDLE_HARNESS_BUILD_TOKEN)The harness in that pane, and what it runs. Revoked when the pane or the view closes, or the core stops
A paper test’s tokenIts hash, in the core’s memoryThe test’s command, started by candle-harness build test in the pane. Revoked when the test ends, within 30 minutes
A prompt pane’s ticketThe core’s memory, as a hashThe launcher whose pid the view named
The vaultCandle’s CLI’s own filesThe candle CLI, in the prompt pane, as you
A TEE wallet’s signerCandle’s CLI’s own storeThe candle CLI, when the core runs candle swap … --wallet or candle stake … --wallet with the agent’s key. No passphrase is asked
The CLI’s sign-inThe system keychain, or the CLI’s encrypted fileThe candle CLI, whenever the core or the pane runs it as the profile setup signed in
The user’s harness sign-insEach harness’s own storeThe harness, from a copy in its private home per turn

A key never reaches an engine, a view or the screen: every message the core sends passes a redactor that knows every key and token, and catches common secret shapes in text too (bearer and basic credentials, a URL’s user:pass@ or key parameter, JWTs, a provider’s key in an RPC address’s path, keypair arrays, PEM private keys), and the two answers that carry a secret on purpose (a harness token from harness.register, an agent’s key to its launcher) are added after it. An environment variable, though, is readable by other processes running as you, through /proc/<pid>/environ on Linux; see the trust boundary below.

The configured RPC address (candle.rpcUrl), which often carries a provider’s key, never enters a card, an operation or the journal: a saved command holds --rpc-url @config, and the address goes in only as the command runs. It’s registered with the redactor too.

The core trusts its socket’s view token, and the harness, bot and build tokens it minted, and nothing else.

  • A view is any process that says hello with the current view token. Each says what it is at hello, and can’t change it: the main view, a popped view (one showing a single pane), or an observer (the doctor, the resource sampler). The core takes approving a card, re-sending a trade Candle didn’t answer, a typed candle command, switching between paper and live, and resuming after the kill switch from the main view only, and refuses them from the others (MAIN_VIEW_ONLY), as it does setup’s steps, the skill folders, wallet autonomy and forget vault. A stop is every view’s: the kill switch, and rejecting or withdrawing a card. The observers change nothing: abandoning an operation, a Hacc undo, and picking the active agent, an engine or a preset are refused from them and from candle-harness stop and cards (OBSERVER). “A new address needs a click” is kept by the Candle Harness’s own view code, which never offers it for a key press. This turns a bug in a popped view into a refusal; it doesn’t stop a process holding the token, which says what it likes at hello. So the boundary is the token, and the token is a file only your user can read.
  • The tokens protect against cooperative agents, not hostile code running as your user. Anything that can read your files can read the view token, the agent keys, and a running child’s environment. An agent’s key reaches candle mcp and candle as CANDLE_API_KEY, the only way Candle’s CLI takes one, so it’s in those children’s environments, which only your user and root can read: the same line its key file sits behind. CANDLE_KEYRING_PASSPHRASE, when you’ve set it for the CLI’s file store, is passed to every candle run too, as your shell already holds it; it’s registered with the redactor, so it’s never written or shown. An agent’s shell, once turned on in config, runs as you and can do all of that; cards and the core’s limits don’t cover what it runs. The picker and the doctor say so in amber for any agent with its shell on.
  • Engines are untrusted. A harness token can propose, read and call the core’s tools, scoped to its own agent and its own cards. It can’t approve, switch modes, resume or reach another agent: not its cards, its status, nor its key through candle_cli. There’s one exception, on purpose: in a turn the person started, an agent may switch off any trigger, another agent’s included, since “stop all my triggers” is the person’s ask. In a woken turn it reaches only its own agent’s. A call from a turn that was stopped (Esc, the kill switch, the step cap) is refused (TURN_ENDED) and makes no card at all, whatever kind of card it asked for; in the address gate’s second run, the same request answers the first run’s card rather than making a second; and a turn that began on paper can’t propose once the mode is live (MODE_CHANGED). Its file reads are held to its research folder (Grok’s file tools can also read system folders, never the Candle Harness’s keys or journal), and its shell is off unless the user turns it on. A typed trade, which can run with no press under cards.typedTrades = "instant", is the main view’s alone too.
  • A code bot’s token is untrusted by the core; the program itself is the user’s. A bot token reaches only its own bot’s methods (bot.*) and the bot and signals topics. Every trade it asks for is proposed by the core’s own client with its card’s mode, wallet, agent and slippage bound, held to its card’s figures, the wallet’s setting, its agent’s limits, the kill switch and the pace; a request naming a run setting is refused. It sells only its own, and a request id is never sent twice. Its report is shown as its own words, never as the core’s figures. But a code bot is a program the user runs, trusted like any program run as that user. Unlike an engine, its file reads aren’t held: it can read what the user can, the agent key files and the view token included. The supervisor gives it no key and no CANDLE_* variable, and the core holds the trades it sends to its figures; neither confines the program. Only Candle’s caps on a key hold a program that uses the key file itself.
  • The Build pane’s harness is the user’s own, run as the user with their own setup, which is the point of it. Its build token reaches only core.hello, the build.* reads and paper tests, and bots.proposeCode for its own agent’s bots: the token can’t approve, trade, or change a limit, key, wallet, trigger or another agent’s bot. The harness itself is trusted like any program the user runs: it can read what the user can, key files included.
  • Third-party text from Candle, Hacc and code bots (signals, posts, token names, Candle’s read answers and refusals, a card’s result, a bot’s report), and the pages Pi’s web_fetch reads, reach an engine cleaned for the terminal and marked as data. Engine error text and tool names are cleaned before they reach a view, the journal or a later prompt. Pages a harness fetches or searches with its own web tools arrive as that harness gives them: the core doesn’t sit in front of those tools. The agent’s own files (research_read, skill_read) aren’t cleaned either, since they’re its own. Two things come back to it as its own, not as data: its memory, research/<agent>/memory/, read into every turn as the agent’s own notes; and skills, served as standing guidance, including any folder the user turns on in settings ▾ → skills. An instruction an agent saved to its memory from something it read comes back next turn as its own note.
  • config.toml is trusted. It names the binaries the core and the pane run (candle.cliPath, each engine’s path), each agent’s limits, tool settings and shell, and whether typed trades need a card. So it’s held as the state folder is: the core refuses to start when the config folder or one of its files is another user’s or writable by everyone, and setup writes them 0700 and 0600. The research folders sit under the same home, but every engine’s file tools are held to its own folder. With the shell on, that hold is gone.
  • What the core can’t hold, it names. Commands run outside the core (another terminal, a script, Candle’s app) are held only by Candle’s own caps on the key, and the Candle Harness says so wherever it shows a limit.

The card is the rule, with these exceptions, each shown in Activity and the Journal:

WhatWhy no pressWhere it’s set
Paper tradesThey move nothingAlways
Live buys and swaps inside a wallet’s two figures, and every live sell, from a wallet set to trade inside limits, each at its route’s default slippage or tighterThe person set the wallet to trade inside limits; a receipt replaces the card. A looser slippage bound is a cardsettings ▾ → approvals, by the person only
Reads through Candle’s MCP tools, Hacc’s tools and the Candle Harness’s ownThey readAlways; hidden per agent in its tool settings
Owner reads through candle_cli, run as the signed-in profile: keys list, profile list, auth status, vault status, vault factor list, tee rebinds, secrets list, plugins, doctor, helpThey read; what they print goes back to the agent, and so to its model providerAlways
Hacc adds: a follow, a group, a wallet into a group, a strategy savedAccount state on Hacc that moves no funds; each has an undo the person clicks. Every key on the account sees themAlways. Removals and a rule’s add are cards
The Candle Harness’s own wallet names and groups, which wallets showNames on this machine; each has an undoAlways. Renaming a Candle or vault label runs in the pane
The view: tabs, layouts as a preview, pins, themesThe screen; each has a keep or an undoAlways
Narrowing one of its own triggers (a smaller amount, a level harder to reach, a longer cooldown, tighter filters), or switching triggers off (any, in a turn the person started; its own agent’s, in a woken turn)Each makes a trigger fire less or do less; only the person switches one back on, from the main view. An agent switching off a trigger that sells notifies the personAlways. Anything wider, or a change to another agent’s trigger, is a card. For a trigger that sells, a smaller amount or a level harder to reach is wider: a card
A rules bot’s live exits (its take profit, stop, max hold, trailing exit, and a flatten), from any wallet, one that asks every time includedThe person approved the bot’s exits on its card, and a stop that waits for a press isn’t one. Approved as bot-exit, which only the core gives; a view that sends it is taken as a plain approval. A slippage bound looser than the bot’s approved one, or the same sell still in flight, is a cardThe bot’s card, by the person only
A rules bot’s buys inside its wallet’s two figures, at the bot’s own approved slippage boundAs any trade inside limits, with the bound the person approved on the bot’s card; the person’s own trade at that bound is still a cardsettings ▾ → approvals, and the bot’s card
Narrowing a bot (a smaller stake, fewer at once, a tighter stop, a sooner exit, stricter filters, a bound tightened as far as the route’s default), or pausing oneEach makes a bot buy less, buy on less, or sell sooner; only the person resumes, stops, flattens or removes one, from the main viewAlways. Anything wider, another wallet, or going live is a card. So are exits that would sell an open position now, an exit under 1% or a hold under 5 minutes, an emptied filter list, and a bound under the route’s default; a bound under 10 bps is refused
A code bot’s sells of its own, the exits the core holds for it, and a flatten, from any wallet, one that asks every time includedThe person approved the bot on its card, and the exits it holds are what keep a crashed bot’s positions protected. Approved as bot-exit, which only the core givesThe bot’s card, by the person only
A code bot’s buys and swaps inside its own figures and its wallet’s two figuresIts card’s figures bound every trade it sends through the core; the wallet’s figures as for any tradeThe bot’s card, and settings ▾ → approvals
Lowering a code bot’s figures or tightening its bound as far as the route’s default, or pausing itEach makes it do less; only the person resumes, stops, flattens or removes one, from the main viewAlways. Anything higher, a bound under the route’s default, going live, or another wallet, folder or command is a card
Web fetches and searches, on an engine that has themThey read. But the agent composes the address, so a fetch can carry anything it has read this turn (wallets, balances, positions, notes), never a key, since its reads are held to its research folder. Each fetch is journalled (agent.fetch) and shown in Activity with its full addressOn unless deny names web_fetch or web_search in the agent’s tool settings
A script the agent runs with the code runnerHeld by the operating system to its research folder, with no network and a time and memory limit; it can’t reach a key, the socket or the network[agents.<name>.tools] run, by the person (off by default)
The agent’s own memory filesIts notesAlways

Typed trades are a card by default; cards.typedTrades = "instant" in config runs a typed trade on Enter.

A trigger is saved only by the person’s press on its card, which the agent proposes through harness_triggers; the card’s hash binds the exact line, its expansion (aliases expanded, token names resolved, a P&L level’s window), its agent, its wallet and its mode, paper or live. A trigger fires, and is read, in its own mode, whatever the core’s is. Nothing the agent or a harness calls can save one, switch one on, or fire one.

  • What it fires runs as the trigger’s own, by the core’s own client, never as the person typing: every part is proposed as a card and goes through the wallet’s figures. Inside them it runs with a receipt; over them, or on a wallet that asks every time, it’s a card that waits 60 seconds for a press. cards.typedTrades never applies. Every card and journal row it causes names the trigger. A trigger’s action may be a trade, sell, swap, or a chain or alias of them; never a candle command, an owner step or funds out.
  • triggers.toml is checked against what was approved. state/triggers.json (0600) keeps the hash each trigger was approved as. A trigger added by hand, or changed since, is held until approved again on a card; only on = false by hand is taken.
  • A woken turn is the agent’s, not the person’s. It opens with what woke it, marked as data. Its trades go through the wallet’s figures as an agent’s do. It can’t propose funds out, since that needs the person’s own words that turn, and it may only read triggers and switch off its own agent’s. Each agent’s wakes are capped (4 an hour and 24 a day by default), and a woken turn is counted as it starts.
  • A line that can’t be read isn’t cached. While a trigger’s expansion doesn’t read, it’s shown as not watching (held: UNREADABLE), journalled after a minute, and read again on each beat.
  • The kill switch holds every trigger’s buys, swaps and wakes, and lets its sells through.
  • A firing is recorded before it runs, so a restart never fires one twice.
  • On Free, nothing is proposed, read or fired; a plan that ends holds every trigger until it returns.

A bot is saved only by the person’s press on its card, which the agent proposes through harness_bots; the card’s hash binds the definition, its agent, wallet, paper or live, and its slippage bound. Nothing the agent or a harness calls can save one, widen one, resume one or take one live.

  • It trades as the bot’s own, by the core’s own client, with the agent’s key, never as the person typing. Each buy goes through the wallet’s figures: inside them with a receipt, over them or on a wallet that asks, a card that waits 60 seconds. Each exit is a bot-exit approval (above). The agent’s own limits, Candle’s caps and holds on the key, and the kill switch apply to both. cards.typedTrades never applies.
  • Each decision is recorded before its trade is sent, and each signal is decided once, so a restart neither enters twice nor sells twice: a sell already sent is read back by its operation.
  • It sells only its own: its book is built from its own fills in the journal, and its sells take its own amount (or, where Candle takes only a percent, its share of the wallet, rounded down).
  • bots.toml is checked against what was approved. state/bots.json (0600) keeps the hash and every approved version. What a bot does is the stricter of the file and the core: a pause or stop typed into the file takes; a bot added by hand runs nothing; one changed by hand takes no entries while its exits run as approved.
  • Candle’s breaker: bots and triggers keep to 20 trades a minute on a key (Candle stops a key at 30), a bot’s sell waits after two failed sells of its token on the key, and a route Candle says is failing is waited out. The person’s and the agent’s own trades are never held. When Candle stops a key, the core pauses that agent’s bots.
  • The kill switch holds every bot’s entries and lets its exits sell. A woken turn may only read, pause and narrow bots.
  • On Free, nothing is proposed or entered; a plan that ends holds every bot’s entries until it returns, and its exits still sell.

A code bot is saved only by the person’s press on its card, which a Build pane proposes (bots.proposeCode); the card’s hash binds its folder, command, agent, wallet, paper or live, its three figures and its slippage bound. The code isn’t bound: the core journals its hash at each start instead. Nothing a harness, the bot, the agent or a popped view calls can save one, raise a figure, take it live or resume it.

  • Its trades are the core’s own, by the core’s own client with the bot’s approved run settings, never as the person typing. Each carries the bot’s request id, journalled before it’s proposed, so a lost answer or a restart reads back rather than sends again. Its buys and swaps are checked against its figures (a trade’s, a day’s from local midnight counting what’s under way, open at most) before anything is recorded, then go through the wallet’s figures. Its sells of its own are bot-exit.
  • Exits the core holds are set on a buy or with bot.exits, journalled, and watched by the rules bots’ machinery while the bot is paused, crashed or not connected, and through the kill switch. A held exit and the bot’s own sell never run on one position at once.
  • bots.toml is checked against what was approved, as for rules bots: a code bot changed by hand has its buys held, and the supervisor runs the approved folder and command, never the file’s.
  • The pace, the breaker and the kill switch are the rules bots’ (bot-trades.ts): bots and triggers together keep to 20 trades in 75 seconds on a key.
  • On Free, no bot token is issued and a hello is refused; a plan that ends holds every code bot’s buys, and its sells and held exits still run.
  1. The agent calls candle_trade through the shim (a typed trade comes from a view instead, and a trigger’s or a bot’s from the core’s own client). An argument the core doesn’t know for that tool is refused (UNKNOWN_ARGUMENT), and so is a base asset (SOL, USDC, ETH, USDG) as the trade’s token, live or on paper (BASE_ASSET): sold as one, it would be a conversion every limit lets through as a sell. The core classifies the trade, values it from a fresh market read, and checks it:
    • the kill switch: while paused, buys and swaps are refused; sells go;
    • a key Candle has held (its volume cap reached, or stopped by a loss limit): buys and swaps are refused in Candle’s words until it lifts; sells go;
    • the agent’s Candle Harness limits: per trade and per day, on buys and swaps. Sells aren’t limited. A buy the core couldn’t price passes, marked unvalued on its card, never valued as zero;
    • Candle’s per-asset cap on the key, where the core can read it.
  2. It becomes a card, bound by a hash to the exact request, the mode, the agent and the wallet, and is sent to every view. Or, on a wallet that trades inside limits, the core checks the wallet’s two figures; inside them it approves the card itself, records the spend before anything is awaited, and the view shows a receipt.
  3. The user presses ⏎ (or clicks). The view sends the card’s id and hash; the core checks both against the card as it stands and runs it once. The kill switch is checked again once the approval and the operation are written, so a kill pressed in between refuses it, with nothing sent.
  4. Candle answers, or doesn’t. An unclear answer is read back from Candle under the same client id before anything else, so nothing is sent twice. An error with no stage on a trade or swap counts as unclear, unless its code is one Candle documents as answered before anything is sent.
  5. The journal records each step, and the panes, the agent and Activity are told.

A funds-out step the agent proposes must match the user’s own message that turn, naming where it goes and how much. A destination that looks like one of the user’s own but isn’t is refused, and a new address is approved only by a click. Funds out always run in the prompt pane, signed by the vault.

The kill switch stops the turn running and withdraws buy and swap cards; it keeps sells, each on a fresh card. It doesn’t stop a new turn from a new message, and it can’t stop anything outside the core. Safety says what it does on screen.

WhereWhat goesWith
Candle, api.alpha.candle.tvEvery Candle call: reads, trades, cardsEach agent’s key, through candle mcp or the candle CLI; the owner’s sign-in for owner steps and owner reads
Hacc, api.hacc.fun (Candle’s data service)Signal, market, token and wallet reads, and Hacc’s follows, rules, groups and strategiesEach agent’s own Candle key, as a bearer token
The model provider each harness usesThe conversation, the tools’ answers, and the agent’s memory and skillsThe harness’s own sign-in
The Build pane’s harness’s providerWhat the user asks it, what it reads in the bots folder (and anything else it reads as the user), and what it reads through candle-harness build: every bot’s book and health, its own agent’s code bots’ logs, and its own agent’s record. Under that harness’s own termsThe user’s own sign-in to it
Wherever a code bot’s own code connectsWhatever the user’s code sends; its trades and reads through the core go to Candle and Hacc as aboveNothing of the Candle Harness’s
Pages the agent looks upSearches and page addresses, which the agent composes and which can carry what it read this turn, through the harness’s own web tools; Pi’s web_fetch is the Candle Harness’s own, public addresses only (the harnesses’ own fetch tools aren’t held to public addresses: OpenCode’s can be turned off per agent)Nothing of the Candle Harness’s
A Solana RPCA TEE wallet’s trades, through the user’s CLI; and from the core, the balance of a stopped TEE wallet, which no Candle read listscandle.rpcUrl, else the RPC the CLI’s profile names, else Solana’s public endpoint
GitHubThe installer’s downloadsNothing

Notifications stay on the machine: the computer’s own, or the terminal’s sequence or bell. A short headline for Candle’s Quant chat is built, and sent nowhere until Candle has a route for it.

Pi’s web_fetch looks a page’s name up once, refuses it if any address it has is local or private, and then connects to the address it checked, with the name as the Host header and the TLS name, so a name that answers a second lookup with a local address never reaches it. Each redirect is checked the same way, and so is a page reached through a proxy variable. Only a name that can’t be looked up here, with a proxy set, is left to the proxy to look up.

install.sh puts candle-harness in place only after two checks: its SHA-256 against the release’s SHA256SUMS and latest.json, and its Sigstore signature against candledottv/harness’s release workflow at the exact tag. The signature is checked with the user’s own candle verify (the candle on PATH, at the version the Candle Harness needs), else cosign, else gh. With no candle, it offers to install Candle’s CLI at a pinned version, checked against SHA-256s the script carries (taken from Candle’s release only after each file’s signature was verified, when the pin was made). Piped into bash, the script itself runs unchecked, as any curl | bash does, so the one-liner trusts the release’s own copy of install.sh. Its whole body sits in one function called on the last line, so a download cut short runs nothing. It’s signed too, and Installing shows how to check it and the binary by hand first. The script as released can’t be told to skip the signature check. The project’s tests, and its unsigned preview builds, run a copy of it made by scripts/install-switch.ts, in which CANDLE_HARNESS_INSTALL_UNSIGNED_FOR_TESTING=1 installs a release with no signature, the checksum still checked.

The release workflow builds, signs, checks every file’s signature in a separate job with read access only, and installs from the result before it publishes. The release is made as a draft, each file is uploaded once, and only then is it published; a tag already published stops the job, so a fix ships as a new version.

After an update, a core already running keeps the version it started as: the installer, the doctor and the status bar say so, and candle-harness version --core prints it.

Builds before 0.5.2 read a bunfig.toml or .env from the folder they were started in. From 0.5.2 the binary reads neither, so update.

Views, the shim, code bots and the Build pane speak JSON-RPC 2.0 over the socket, state/core.sock, mode 0600. The attach protocol describes every method, topic and event, and which role may call each; the bot protocol is a code bot’s side of it.