Getting started
Start here if you have never run bridge before. It is one static binary: no runtime to install, no container, no build toolchain. Pick your operating system below, and you should be looking at the interface in a few minutes.
A pre-release build
These archives are real and the commands below work, but 1.1.1-dev is a pre-release: nothing has been tagged, and the shape of things can still change under you. The source repository is private, which is why go install is not the install path.
What you need
| Thing | Why |
|---|---|
| A terminal | Terminal on macOS, PowerShell on Windows, any shell on Linux. You will type a handful of lines into it. |
| An API key | From any one of the eleven hosted providers, if you want a hosted model. Not needed if you only ever run local models. |
| Ollama (optional) | Only if you want local models or semantic memory search. Everything else works without it. |
| Go (optional) | Only if you are building from source rather than downloading a binary. Most people should not need it. |
Step 1. Work out which build you need
There is one archive per operating system and processor type. Run the command for your system and match the answer against the notes.
macOS
uname -m
arm64 means Apple Silicon (M1 and later). x86_64 means an Intel Mac, and you want the amd64 archive.
Windows
$env:PROCESSOR_ARCHITECTURE
AMD64 means the amd64 archive. ARM64 means the arm64 one, which is what a Surface Pro X or a Windows-on-ARM laptop reports.
Linux
uname -m
x86_64 means the amd64 archive. aarch64 means arm64, which is what a Raspberry Pi 4 or an ARM cloud instance reports.
Every archive
| GOOS | GOARCH | Unpacked |
|---|---|---|
| darwin | arm64 | 13.7 MB |
| darwin | amd64 | 14.5 MB |
| linux | amd64 | 14.3 MB |
| linux | arm64 | 13.4 MB |
| windows | amd64 | 14.7 MB |
| windows | arm64 | 13.5 MB |
Step 2. Download and install
Each block downloads the archive, checks it against the published checksums, and puts the binary somewhere your shell can find it. Do not skip the checksum: it is the only way to know you are running what was published.
macOS
VER=1.1.1-devARCH=arm64PKG=bridge-${VER}-darwin-${ARCH} curl -fsSLO https://api.clrtstudio.com/bridge/${VER}/${PKG}.tar.gzcurl -fsSLO https://api.clrtstudio.com/bridge/${VER}/SHA256SUMSshasum -a 256 -c SHA256SUMS --ignore-missing tar -xzf ${PKG}.tar.gzxattr -d com.apple.quarantine ${PKG}/bridge 2>/dev/null || truesudo mv ${PKG}/bridge /usr/local/bin/bridgebridge versionWindows (PowerShell)
# Swap amd64 for arm64 on a Windows-on-ARM machine.$VER = "1.1.1-dev"$ARCH = "amd64"$PKG = "bridge-$VER-windows-$ARCH" curl.exe -fsSLO https://api.clrtstudio.com/bridge/$VER/$PKG.zipcurl.exe -fsSLO https://api.clrtstudio.com/bridge/$VER/SHA256SUMS # Compare this hash against the matching line in SHA256SUMS.Get-FileHash "$PKG.zip" -Algorithm SHA256 # Clear the downloaded-from-internet flag, or SmartScreen blocks it.Unblock-File "$PKG.zip" # The archive expands to a folder named after itself.Expand-Archive "$PKG.zip" -DestinationPath "$env:LOCALAPPDATA\bridge" # Put that folder on PATH for future sessions, then reopen PowerShell.[Environment]::SetEnvironmentVariable( "Path", $env:Path + ";$env:LOCALAPPDATA\bridge\$PKG", "User") bridge.exe versionLinux
VER=1.1.1-devARCH=amd64PKG=bridge-${VER}-linux-${ARCH} curl -fsSLO https://api.clrtstudio.com/bridge/${VER}/${PKG}.tar.gzcurl -fsSLO https://api.clrtstudio.com/bridge/${VER}/SHA256SUMSsha256sum -c SHA256SUMS --ignore-missing tar -xzf ${PKG}.tar.gzsudo install -m 755 ${PKG}/bridge /usr/local/bin/bridgebridge versionIf bridge version printed something, you are installed
It reports the version, the commit it was built from, the Go toolchain and your platform. If instead you got command not found, the binary is not on your PATH; see troubleshooting at the end of this section.
Step 3. First run
# Creates ~/.bridge/config.yaml with every provider listed# and switched off. Written at mode 0600, because it is about# to hold your API keys.bridge init # Starts the interface on 127.0.0.1:7844 and opens a browser.bridge serve --openThe browser lands on the Config page, because no provider is enabled yet. Paste an API key into one of them, switch it on, and save: the engine is rebuilt in place, without a restart. Then use the tabs across the top.
| Tab | What it is for |
|---|---|
| Chat | Talk to the model. Streaming, tool calls and conversation history. |
| Agents | Build a saved agent: a system prompt, a model, and which tools and skills it may reach. Run one from the panel beside the form. |
| Workflows | Chain those agents into a graph. The editor shows the execution levels it computed, and a run streams step by step. |
| Skills | Install and browse Agent Skills packs. The model loads one when it decides it needs it. |
| Memory | Search what it has remembered, and prune what it should not have. |
| Spend | What every call cost, broken down by provider, task and model. |
| Config | Providers, keys, budget ceiling, memory and MCP servers. |
| Policy | Which model answers which kind of request, and what it may cost. |
Step 4 (optional). Local models and semantic memory
Skip this if you only want hosted models. Install it if you want prompts that never leave the machine, or memory search that understands meaning rather than matching words.
# The embedding model. Small, and what semantic recall needs.ollama pull nomic-embed-text # A chat model to run locally. Pick one you have disk and memory# for: parameter counts in the tens of billions are tens of# gigabytes on disk. Start small if you are only trying it out.ollama pull llama3.2 # Tells you what bridge can and cannot see, and why.bridge setup # Same check, but pulls the embedding model and patches the# config where it safely can.bridge setup --fixWhen something goes wrong
| What you see | What to do |
|---|---|
| command not found: bridge | Check where it landed, then move it to /usr/local/bin (macOS, Linux) or add its folder to PATH (Windows). |
| “bridge” cannot be opened because the developer cannot be verified | Remove the quarantine flag with xattr -d com.apple.quarantine, or open it once from Finder with right-click, Open. |
| Windows protected your PC (SmartScreen) | Run Unblock-File on the downloaded archive before extracting, or choose More info then Run anyway. |
| permission denied when running ./bridge | chmod +x bridge |
| go install fails with 404 or 410 | Download the archive for your platform from the install steps above. |
| The server refuses to start on 0.0.0.0 | Set server.auth_token in the config, or pass --auth-token. |
| Semantic search returns nothing useful | Run bridge setup to see what is missing, install Ollama, and pull the embedding model. |
| Every call is refused before it is sent | Raise budget.ceiling_usd, or check the Spend page to see where it went. |
Configuration
One YAML file at ~/.bridge/config.yaml, written at mode 0600 because it holds credentials. The web UI is the intended editor, and saving from it rewrites the file with a fixed header rather than preserving hand-written comments.
# ~/.bridge/config.yaml (mode 0600)providers: anthropic: enabled: true api_key: sk-ant-... default_model: claude-sonnet-4-6 ollama: enabled: true base_url: http://localhost:11434 default_model: llama3.3:70b budget: ceiling_usd: 5.00 policy_path: ~/.bridge/policy.yaml memory: enabled: true db_path: ~/.bridge/memory.db embedder: provider: ollama model: nomic-embed-text skills: enabled: true dir: ~/.bridge/skills agents: enabled: true dir: ~/.bridge/agents workflows: enabled: true dir: ~/.bridge/workflows server: addr: 127.0.0.1:7844Every key
| Key | Meaning |
|---|---|
| providers.<name>.enabled | Whether this provider is registered on the Brain at all. |
| providers.<name>.api_key | Credential. Written to a file at mode 0600; never leaves the machine except to that provider. |
| providers.<name>.base_url | Override the endpoint. This is how Ollama is pointed at a non-default host. |
| providers.<name>.default_model | Model used when the policy does not pin one. |
| providers.<name>.auth | How credentials are resolved for this provider: api_key, oauth, adc or cli. Empty means api_key. |
| providers.<name>.headers | Extra request headers. This is where a vendor that meters by attribution header, such as OpenRouter, gets one. |
| budget.ceiling_usd | Hard USD ceiling. Checked before the request is sent, so an over-budget call never bills. |
| policy_path | Where the routing policy lives. Hot-reloaded on a 2s file-stat poll. |
| memory.enabled | Turn the memory store on. |
| memory.db_path | SQLite file. Defaults to ~/.bridge/memory.db. Use :memory: for an ephemeral store. |
| memory.embedder.provider | Which provider supplies embeddings. ollama is the shipping local option. |
| memory.embedder.model | Embedding model. Defaults to nomic-embed-text. |
| memory.embedder.base_url | Endpoint for the embedder, when it is not the provider's default. |
| memory.embedder.backfill_batch_size | How many records the backfill worker embeds per pass. |
| memory.embedder.backfill_interval_seconds | How often the backfill worker wakes. |
| memory.prune.* | Automatic pruning. Importance-aware, so high-value records survive a sweep. |
| chat.db_path | Conversation history store. Defaults to ~/.bridge/chats.db. |
| spend.persist_path | Where the spend ledger is written so totals survive a restart. Defaults to ~/.bridge/spend.json. |
| server.addr | host:port for the web UI. Defaults to 127.0.0.1:7844. |
| server.auth_token | Required before the server will bind anything other than localhost. |
| mcp.servers[] | MCP servers to connect at startup. See the MCP section for the per-entry fields. |
| skills.enabled | Whether installed skills are exposed to the model. Off by default, so upgrading a 1.0 config does not silently change what the model sees. The first bridge skills add turns it on. |
| skills.dir | Where bridge skills add installs to. Defaults to ~/.bridge/skills. |
| skills.extra_roots | Extra directories to index, for a checkout you maintain yourself rather than copy in. Earlier roots win a name collision. |
| agents.enabled | Whether saved agents are loaded and exposed to the chat as delegation tools. |
| agents.dir | One YAML file per agent lives here. Defaults to ~/.bridge/agents. |
| agents.max_delegation_depth | Bounds agent-as-tool recursion, so an agent that can reach itself stops before the budget does. Zero means the shipped default of 2. |
| workflows.enabled | Whether saved workflows are loaded and exposed to the chat as delegation tools. |
| workflows.dir | One YAML file per workflow lives here. Defaults to ~/.bridge/workflows. |
| workflows.max_parallel | How many steps of one execution level run at once. Zero means the shipped default of 4. |
| auth.store_path | Where OAuth tokens are written. Defaults to ~/.bridge/credentials.json, mode 0600, and bridge refuses to read it if the group or world can. |
| auth.google.client_id | The Desktop-app OAuth client you registered in your own Google Cloud project. bridge ships no client id of its own. |
| auth.google.client_secret | That client's secret. Held in the same 0600 file as the API keys. |
| auth.google.scopes | Override the scopes requested during the Google flow. |
Providers
Fourteen providers behind five adapters. Four are written against the vendor's own API. The other ten speak the OpenAI chat-completions wire format, so they share one adapter and arrive as entries in a preset table: base URL, default model, key variable and privacy tier.
Every provider
| Name | Adapter | Key variable |
|---|---|---|
| anthropic | native | ANTHROPIC_API_KEY |
| openai | native | OPENAI_API_KEY |
| gemini | native | GEMINI_API_KEY, then GOOGLE_API_KEY |
| ollama | native | none, local |
| cerebras | compat | CEREBRAS_API_KEY |
| deepseek | compat | DEEPSEEK_API_KEY |
| fireworks | compat | FIREWORKS_API_KEY |
| groq | compat | GROQ_API_KEY |
| lmstudio | compat | none, local |
| mistral | compat | MISTRAL_API_KEY |
| openrouter | compat | OPENROUTER_API_KEY |
| together | compat | TOGETHER_API_KEY |
| vllm | compat | none, local |
| xai | compat | XAI_API_KEY |
A preset fills the blanks for you
bridge init writes every provider into the config, disabled, with a default model already filled in and a base URL for the ones that need to be told where to look. Configuring one is pasting a key and flipping enabled, not reading a vendor's reference to find an endpoint.
Three of these run on your own machine
ollama, lmstudio and vllm take a base URL and no credential, and they report the local_only privacy tier. A call marked local_only in the routing policy can reach those and nothing else.
Credentials
An API key is the default and works everywhere. OAuth is supported where the vendor sanctions it, which today means Google, and Anthropic developer profiles held by the ant CLI. Which paths are open to a third-party client is a terms question rather than a technical one, so this section says plainly what works and what does not.
What each provider accepts
| Provider | API key | Consumer subscription |
|---|---|---|
| anthropic | yes | no |
| gemini | yes | not applicable |
| openai | yes | no |
| xai | yes | no |
| ollama, lmstudio, vllm | not applicable, local | not applicable |
| everything else | yes | no |
A consumer subscription grants no API access
A Claude Pro or Max plan, a ChatGPT Plus plan, an X Premium plan and a Google One AI plan buy access to that vendor's own application. None of them is an exception. The API is a separate product with separate billing, and the subscription does not include it. bridge does not replay the token another application holds, for three reasons: presenting someone else's OAuth client id is credential misuse whether or not it is detected; bridge is downloaded and run by people who did not choose that risk and would be the ones whose accounts are terminated; and the client id, token shape and headers are internal details of somebody else's app, so every change they make is an outage for every install.
providers.<name>.auth
| Value | What it resolves to |
|---|---|
| api_key | The default, and what an empty value means. Uses the block's api_key, then the vendor's conventional environment variable. |
| oauth | A token from the credential store, minted by bridge login and refreshed in the background where the vendor issued a refresh token. |
| adc | Google Application Default Credentials, as written by gcloud auth application-default login. Gemini only. |
| cli | Shell out to the vendor's own CLI for a short-lived token. Anthropic only, via ant auth print-credentials. |
Resolution order, first match wins
- 1A credential in ~/.bridge/credentials.json for that provider, refreshed first if it has expired and carries a refresh token.
- 2api_key in the provider's config block.
- 3The provider's conventional environment variable.
- 4Provider-specific discovery: Google ADC for gemini, the ant CLI for anthropic.
- 5Failure, with a message naming the provider and what it looked for.
# Google, path one: credentials you already have.# Then set auth: adc on the gemini provider block.gcloud auth application-default loginbridge login gemini # Google, path two: your own OAuth client. Register a# Desktop app client in your own Google Cloud project,# put the id in auth.google.client_id, then authorise.# bridge ships no client id of its own, on purpose: one# embedded in a downloadable binary is not a secret.bridge login gemini # Anthropic: a developer profile held by the ant CLI.# Then set auth: cli on the anthropic provider block.ant auth loginbridge login anthropic # What is stored, and when it expires. Tokens are never# printed: a terminal scrollback is a bad place for one.bridge login --statusWhere the credentials live
Tokens go to ~/.bridge/credentials.json at mode 0600, beside the config file that holds the API keys. bridge refuses to read either file if its mode lets the group or the world read it, and tells you to chmod 600 rather than carrying on.
Routing policy
A separate YAML file, hot-reloaded on a 2s file-stat poll, that maps each task kind to an ordered list of provider:model candidates. Privacy tier and the per-task cost cap filter the list, then the first viable candidate wins.
# ~/.bridge/policy.yaml# Re-read on a 2s file-stat poll. No restart.tasks: reason: privacy: cloud_ok prefer: - anthropic:claude-opus-4-7 - anthropic:claude-sonnet-4-6 fallback: - openai:gpt-5 max_cost_usd: 0.10 # local_only is enforced, not advisory. A call marked # this way cannot reach a hosted provider even if one # is listed above it. protect: privacy: local_only prefer: - ollama:llama3.3:70b max_cost_usd: 0.0Task kinds
| Kind | What it is for |
|---|---|
| classify | Short, cheap, high-volume judgements. The default policy sends these to the smallest model. |
| reason | The hard thinking. Routed to the most capable model the cost cap allows. |
| generate | Drafting and writing. Mid-tier by default. |
| protect | Anything that must not leave the machine. Local only in the default policy. |
| embed | Embedding requests. Local only, and the default prefer list is empty until a provider is wired in. |
Privacy tiers
| Token | Enforcement |
|---|---|
| cloud_ok | Any provider, hosted APIs included. The default when the field is omitted. |
| sensitive_redact | Hosted providers are allowed, but the layer above is expected to redact before sending. |
| local_only | Local providers only. A call marked this way can never reach a hosted API, whatever the prefer list says. |
Pinning a model
A model set on the call itself wins over the policy's pick. If no registered provider can price that model, the call falls back to the policy rather than failing.
Memory
Typed records in local SQLite, with FTS5 keyword search and pure-Go cosine over stored vectors. Recalled records fold into a single system message at the head of the transcript, so a provider adapter that drops the system role would silently lose them.
Record kinds
| Kind | Half-life | What it holds |
|---|---|---|
| working | 24 h | The current task's scratch. Halves in relevance every day. |
| episodic | 720 h | What happened, turn by turn. Written automatically after each call. Halves every 30 days. |
| profile | never | Durable facts about the user or the work. Does not decay. |
Decay is a half-life, not an e-folding time
Importance decays as pow(0.5, age/half_life), so a working record sits at exactly half its importance after 24 hours and a quarter after 48. Recall returns the decayed value, not the base one, because that is the number a cutoff runs against.
Recall modes
| Mode | How it ranks |
|---|---|
| keyword | SQLite FTS5 over record content, with the query's tokens OR-joined so multi-word searches still hit. |
| semantic | Cosine similarity over stored embeddings. Needs an embedder; without one the flag is ignored rather than erroring. |
| hybrid | Both, blended by HybridWeight. 0 is pure keyword, 1 is pure semantic, and the backend defaults to 0.6. |
| recent | Straight reverse-chronological, no scoring. The escape hatch when ranking is getting in the way. |
Semantic recall needs an embedder
Ollama implements the embedder interface locally, so semantic recall needs no cloud key and no prompt leaves the machine to get one. Run ollama pull nomic-embed-text, point the embedder block at it, and a background worker backfills embeddings for records written before it was configured.
Agents
A saved agent is a system prompt, a model or a task to route by, a memory scope, a budget and an allowlist over the tool pool. One YAML file each under ~/.bridge/agents, so a team can review a change to one in a merge request.
# ~/.bridge/agents/researcher.yamlid: researchername: Researcherdescription: Gathers source material on one topic and reports what it found. system: | You research a single topic and report what you find. Quote your sources. Say plainly when you could not verify something, rather than filling the gap. # No model, so the routing policy picks one. A policy change# then reaches every saved agent without editing a file.task: reasonprivacy: cloud_okmax_cost_usd: 0.25 # Glob-matched against the tool pool. A bare "*" means everything.tools: - fs_* - bridge_skill_* skills: - "*" memory: mode: by_tags tags: [research]Every field
| Field | Meaning |
|---|---|
| id | Stable kebab-case handle. It is also the filename and the seed for the tool name, so renaming an agent means creating a new one. |
| name | The human label shown in pickers and logs. Required. |
| description | What the agent is for. It doubles as prompt surface when the agent is delegated to, so write it for a model as well as a person. |
| system | The system prompt. It goes in the first message, which every adapter maps to the upstream API's native system mechanism. |
| model | Pin a model id. Leave it empty and the router decides, so a policy change reaches every saved agent without editing files. |
| task | Task kind for routing: classify, reason, generate, protect or embed. Empty means generate. |
| privacy | cloud_ok, sensitive_redact or local_only. Empty means cloud_ok. |
| max_tokens | Cap output tokens per turn. Zero leaves the provider default in place. |
| max_cost_usd | Hard per-call ceiling for this agent. The Brain's own budget still applies on top. |
| max_tool_hops | Cap the tool loop for this agent. Carried for callers that build a per-agent Brain; zero uses the Brain's setting. |
| tools | Allowlist over the tool pool, matched with glob syntax. A bare * means everything. Naming none inherits the Brain's whole set, which is not the same as none. |
| skills | Allowlist of skill names, same glob rules. Matching skills are rendered into a catalogue block appended to the system prompt. |
| memory.mode | How much memory is hydrated per call: none, recent, full or by_tags. Empty means none. |
| memory.tags | Restricts retrieval when the mode is by_tags. |
| metadata | Free-form labelling for the UI: owner, source, colour. It is never sent to the provider. |
Naming no tools is not the same as naming none
The allowlist is exact when you list something. Leave tools out entirely and the agent inherits whatever the running instance has registered, because the call carries no tool list and that reads as unspecified rather than empty. An agent that must have no tools at all needs a toolless instance to run against.
How the chat reaches them
| Tool | What it does |
|---|---|
| bridge_agent_list | Lists the saved agents: id, name, purpose, model, and how many tools and skills each can reach. Reads files on the machine and makes no model call. |
| bridge_agent_run | Runs one saved agent and returns what it produced. The agent does not see the calling conversation, so the input has to be a standalone brief. |
| bridge_workflow_list | Lists the saved workflows with the inputs each one takes and how many steps it runs. No model call. |
| bridge_workflow_run | Runs one workflow end to end and returns a per-step summary plus the final output. A missing required input is rejected before anything executes. |
Why a list and run pair rather than one tool per agent
Tool definitions are re-sent on every provider turn, so thirty agents would be thirty schemas in every request forever, whether or not the turn has anything to do with agents. The tool set is also fixed when the engine is built, so a per-agent tool would mean reopening the memory database and respawning every MCP subprocess each time somebody renamed a prompt. The pair costs one extra round trip when the model actually delegates, and nothing when it does not. Delegation depth is capped, at two by default, so an agent that can reach itself stops before the budget does.
Workflows
A workflow is a DAG over those agents. Steps are sorted into levels, and everything on a level runs at the same time, bounded per level. A step is either an agent invocation or an inline prompt, never both, because allowing both would make which one wins a behaviour a reader of the YAML cannot see.
# ~/.bridge/workflows/brief-to-draft.yamlid: brief-to-draftname: Brief to draftdescription: Outline a topic, research it, then write the draft. inputs: - name: topic description: What to work on required: true steps: # An inline step: prompt, and optionally system, model or task. - id: outline name: Outline it task: reason prompt: | Produce a short outline for: {{.inputs.topic}} # An agent step: agent and input, never prompt. The agent brings # its own system prompt, model and tools, so setting them here is # rejected at save. - id: research name: Gather sources agent: researcher input: "{{.inputs.topic}}" # outline and research have no edge between them, so they are one # level and run at the same time. draft waits for both. - id: draft name: Write the draft depends_on: [outline, research] prompt: | Write this up, following the outline exactly. Outline: {{.steps.outline.text}} Notes: {{.steps.research.text}}Step fields
| Field | Meaning |
|---|---|
| id | Kebab-case node id, unique in the workflow. Later steps read this step's output as {{.steps.<id>.text}}. |
| name | Human label for the trace row. Defaults to the id. |
| agent | Id of a saved agent to invoke. Mutually exclusive with prompt. |
| input | Template rendered into the agent's input message. Only meaningful with agent. Empty falls back to the outputs of depends_on, then to the workflow input named input. |
| prompt | Inline user-prompt template. Mutually exclusive with agent. |
| system | Optional system-prompt template. Only meaningful with prompt: an agent already carries its own. |
| model | Pin a model for this step. Only meaningful with prompt; empty lets the router decide. |
| task | Task kind for routing this step. Only meaningful with prompt. |
| depends_on | Step ids that must finish first. This is the only ordering mechanism; position in the file is not. |
| when | Gate template. The step runs only if the rendered value is non-empty and not false, 0 or no. A gated-out step skips its dependents too. |
| for_each | Template that must render to a JSON array. The step then runs once per element with .item and .index bound, and the trace carries one row per iteration. |
| max_cost_usd | Cap the spend of this single step. Zero means no step-level cap. |
Input fields
| Field | Meaning |
|---|---|
| name | The template key, reached as {{.inputs.<name>}}. Alphanumeric with - or _ separators. |
| description | What to type. It is shown on the run form and returned to a model listing the workflow. |
| default | Used when the caller omits the input or passes an empty string. |
| required | An absent value after the default is applied fails the run before the first step executes. A required input with a default never fails. |
Templates are checked at save, not at run
Every free-text field is a Go text/template over .inputs and .steps.<id>.text, parsed with missing keys treated as errors. A typo fails when you press save rather than three paid steps later. Cycles are rejected the same way, with the members of the cycle named.
A failed step skips its dependents
Nothing downstream of a failure runs, because its inputs would not exist. A step gated out by when is recorded as skipped and takes its dependents with it for the same reason. Run bridge workflows show to print the levels a graph actually produces.
Skills
A skill is a directory holding a SKILL.md whose head is YAML frontmatter, optionally with reference files beside it. It is the Agent Skills format, so a pack authored elsewhere loads here unchanged. Only the name and description sit in context; the model pulls the body when it decides it needs it.
---name: pdf-formsdescription: > Fill and flatten AcroForm PDFs. Use when a request means writing values into an existing PDF form rather than generating one.license: MITallowed-tools: [Bash, Read]metadata: category: documents---Frontmatter fields
| Field | Meaning |
|---|---|
| name | Required. The unique key in the catalogue, taken from frontmatter rather than the directory so a pack can be moved without renaming its skills. |
| description | Required. The one-paragraph what-this-is-and-when-to-use-it line. It is what the model reads when deciding whether to load the skill, so a vague one is a skill the model never reaches for. |
| license | Carried through verbatim so a UI can show attribution for a vendored pack. |
| allowed-tools | Advisory hint naming the tools the skill expects to call. bridge does not enforce it. A comma-separated string is accepted as well as a list. |
| metadata | Free-form frontmatter map, flattened to strings. Pack authors use it for category tagging. |
Everything below the closing delimiter is the body: the instructions the model reads once it loads the skill. Packs install under skills.dir from a local directory or a shallow git clone.
# From a directory you already have, or from a git URL,# which needs git on your PATH. Nothing is executed at# install time: a skill is prompt material plus files.bridge skills add ./my-skillsbridge skills add https://github.com/google/skills # What is indexed, and what was skipped and why.bridge skills list # One skill in full, body included.bridge skills show pdf-formsHow the model reaches them
| Tool | What it does |
|---|---|
| bridge_skill_search | Matches a query against installed skill names and descriptions and returns one line per hit. Nothing is loaded, so searching is cheap. An empty query lists the catalogue. |
| bridge_skill_read | Loads one skill in full: its instructions plus a listing of the reference files bundled with it. |
| bridge_skill_file | Reads one bundled file by its path, confined to the skill's own directory. Absolute paths and parent references are refused, and a file over 256 KiB comes back truncated with a note. |
Bundled file reads are confined to the skill
The path handed to bridge_skill_file comes from a model and the pack is third-party content, so absolute paths and parent references are refused, and a symlink that resolves outside the skill directory is refused after the join rather than before it. Without that confinement the tool is an arbitrary local file read wearing a friendlier name.
Model Context Protocol
bridge is an MCP client, targeting protocol revision 2025-06-18 over stdio and Streamable HTTP. A server's tools appear on the Brain alongside your own Go tools.
mcp: servers: - name: filesystem transport: stdio command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/srv"] prefix: fs enabled: true - name: search transport: http url: https://mcp.example.com/mcp headers: Authorization: Bearer ${SEARCH_TOKEN} enabled: trueServer entry
| Field | Meaning |
|---|---|
| name | Identifies the server. Also the default tool-name prefix. |
| transport | stdio or http. |
| command | argv for a stdio server, for example ["npx", "-y", "@scope/server"]. |
| url | Endpoint for an http server, using the Streamable HTTP transport. |
| headers | Extra headers sent on http connections. Where an auth token goes. |
| prefix | Prefix applied to this server's tool names. Defaults to the server name. |
| enabled | Whether to connect at startup. |
Why tool names get prefixed with an underscore
Anthropic and OpenAI both reject function names outside ^[a-zA-Z0-9_-]{1,64}$, so a dotted name would fail at request time whatever bridge allowed. A server named echo exposing a tool named echo arrives as echo_echo. A remote tool whose name cannot be expressed in that set is skipped with a warning rather than silently mangled.
A server that fails to connect does not block startup
Each server gets one 10-second attempt at bundle time; a failure is logged and skipped so one broken stdio command cannot hang the UI. The client keeps retrying in the background, but a server that comes up later is not added to the running Brain until you save the config page again.
CLI reference
Eight subcommands, parsed with the standard library's flag package. There is no shell-completion script and no config-file discovery beyond the --config flag.
bridge serve
Start the embedded web UI. Binds localhost by default and refuses any other address without an auth token.
| Flag | Effect |
|---|---|
| --config | Path to the config file. |
| --addr | host:port to bind. Overrides server.addr. |
| --bind | Set only the bind host, keeping the port from config. |
| --auth-token | Token required for non-localhost binds. Overrides server.auth_token. |
| --open | Open the UI in the default browser once it is listening. |
| --log-level | debug, info, warn or error. |
bridge init
Create the config file with every provider present and disabled. Written at mode 0600 because it will hold API keys.
| Flag | Effect |
|---|---|
| --config | Where to write the file. |
bridge setup
Check prerequisites: whether the Ollama daemon is reachable, whether the embedder model is present, and whether the config agrees.
| Flag | Effect |
|---|---|
| --config | Config file to check against. |
| --model | Embedder model to look for. |
| --fix | Pull the embedder model and patch the config where it can. |
bridge login
Run the OAuth flow a vendor sanctions and write the result to ~/.bridge/credentials.json at mode 0600. Only gemini and anthropic have a path; every other provider is told to use an API key, with the reason.
| Form | What it does |
|---|---|
| bridge login <provider> | Authorise one provider. Opens a browser by default. |
| bridge login --status | List what the store holds: provider, method, expiry and whether it can refresh. Tokens are never printed. |
| bridge login <provider> --logout | Delete the stored credential for that provider. |
| Flag | Effect |
|---|---|
| --config | Config file holding the OAuth client registration and the store path. |
| --status | Show stored credentials and exit, without naming a provider. |
| --logout | Delete the named provider's credential instead of minting one. |
| --device | Use the device-code flow rather than a browser loopback. This is the headless path. |
bridge skills
Manage the Agent Skills catalogue. Packs install under skills.dir. A git source is cloned shallow, which needs git on your PATH. Nothing is executed at install time.
| Form | What it does |
|---|---|
| list | Every indexed skill with the first sentence of its description, then anything that was skipped and why. |
| add <directory|git-url> | Install a pack from a local directory or a shallow clone. The first install also sets skills.enabled. |
| remove <pack> | Delete an installed pack from the skills directory. |
| show <name> | One skill in full: description, bundled files, and the body the model would load. |
| Flag | Effect |
|---|---|
| --config | Config file naming the skills directory and any extra roots. |
bridge agents
Inspect saved agents. Authoring happens on the Agents page or in an editor: an agent is mostly a system prompt, and a system prompt wants an editor rather than a flag.
| Form | What it does |
|---|---|
| list | Id, name, model, and how many tools and skills each agent can reach. |
| show <id> | One agent in full: its tool name, model, task, both allowlists, and the system prompt. |
| Flag | Effect |
|---|---|
| --config | Config file naming the agents directory. |
bridge workflows
Inspect saved workflows. show prints the computed execution levels, which is the fastest way to check that a graph runs in the order you think it does.
| Form | What it does |
|---|---|
| list | Id, name, step count and what each workflow does. |
| show <id> | Declared inputs, then the execution levels: everything printed on one line runs in parallel. |
| Flag | Effect |
|---|---|
| --config | Config file naming the workflows directory. |
bridge version
Print the version, the commit the binary was built from, the Go toolchain and the platform.
Go library
The root package is interfaces only. It imports no concrete provider, router, memory store or transport, so you can replace any one of them without forking the others.
Module access
The module path is gitlab.com/withvishal/bridge/tool. The repository is private, so go get resolves only for machines with credentials for it.
package main import ( "context" "fmt" "os" "gitlab.com/withvishal/bridge/tool" "gitlab.com/withvishal/bridge/tool/budget" "gitlab.com/withvishal/bridge/tool/memory/sqlite" "gitlab.com/withvishal/bridge/tool/providers/anthropic" "gitlab.com/withvishal/bridge/tool/providers/ollama" yamlrouter "gitlab.com/withvishal/bridge/tool/router/yaml") func main() { claude, _ := anthropic.New( anthropic.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY"))) local, _ := ollama.New( ollama.WithBaseURL("http://localhost:11434")) // Passing "" loads the policy embedded in the binary. router, _ := yamlrouter.Load("~/.bridge/policy.yaml") store, _ := sqlite.Open("~/.bridge/memory.db") defer store.Close() b, _ := bridge.New( bridge.WithProvider(claude), bridge.WithProvider(local), bridge.WithRouter(router), bridge.WithMemory(store), bridge.WithBudget(budget.New(budget.WithCeiling(5.00))), bridge.WithMaxToolHops(10), ) defer b.Close() resp, err := b.Call(context.Background(), bridge.Call{ Task: bridge.TaskReason, Privacy: bridge.PrivacyCloudOK, Prompt: "What did we decide about the billing refactor?", }) if err != nil { // ErrBudgetExceeded is returned before the request is // sent, so nothing was billed. panic(err) } fmt.Println(resp.Text) fmt.Printf("%s via %s, $%.5f\n", resp.Model, resp.Provider, resp.CostUSD)}Options accepted by bridge.New
| Option | Effect |
|---|---|
| WithProvider | Register a provider. Repeatable. With no router set, the first registered provider wins. |
| WithRouter | Choose provider and model per call. router/yaml is the shipping implementation. |
| WithBudget | Attach a USD ceiling, checked before each request goes out. |
| WithMemory | Attach a memory store. Recalled records fold into one system message at the head of the transcript. |
| WithEmbedder | Record an embedder for semantic recall. Does not start a backfill worker; the store owns that. |
| WithTools | Register Go functions the model can call. Repeatable. |
| WithMCPClients | Surface an MCP server's tools on the Brain. |
| WithMaxToolHops | Cap tool-call rounds in one turn. Default 10. |
| WithMaxParallelTools | Cap tools executing concurrently. Default 4. |
| WithDefaultModel | Model to use when neither the call nor the policy pins one. |
| WithLogger | Supply an *slog.Logger. Without one, bridge stays quiet. |
Interfaces
| Interface | Contract |
|---|---|
| Provider | A model backend. Name, Models, Privacy, EstimateCost, Call, Stream. One package per provider under providers/. |
| Router | Chooses provider and model for a call, given the candidates and the remaining budget. router/yaml is the shipping implementation. |
| BudgetTracker | EstimateCost, Charge, Remaining, Spent. Checked before the request goes out, so an over-budget call never bills. |
| Memory | Add, Recall, Prune, Close. Deliberately narrow. memory/sqlite adds backfill and auto-prune on the concrete type. |
| Embedder | Name, Dimensions, Embed. Optional. Ollama implements it locally, so semantic recall needs no cloud key. |
| Tool | An LLM-callable function. Registered directly, or surfaced from an MCP server with a prefix. |
| agent.Store | List, Get, Save, Delete. Saved agents are one YAML file each, so a team can review them in a pull request. |
| workflow.Executor | RunAgent and RunPrompt. The workflow engine knows nothing about the Brain; the caller supplies this, which is why its whole test suite runs without a socket. |
Streaming
Stream returns a channel of typed chunks. Per-turn usage is not forwarded: it folds into one terminal usage chunk covering every hop the tool loop took, so a caller totalling usage never double-counts a multi-hop turn.
chunks, err := b.Stream(ctx, bridge.Call{ Task: bridge.TaskGenerate, Prompt: "Summarise the incident report.",})if err != nil { // The first turn's handshake is opened synchronously, // so a bad key surfaces here rather than mid-stream. return err} for c := range chunks { switch c.Type { case bridge.StreamChunkText: // Deltas are not cumulative. Concatenate them. fmt.Print(c.Delta) case bridge.StreamChunkToolCall: fmt.Printf("\n[tool] %s\n", c.ToolCall.Name) case bridge.StreamChunkUsage: // One terminal usage chunk per turn, aggregated // across every hop the tool loop took. fmt.Printf("\n%d in / %d out\n", c.Usage.InputTokens, c.Usage.OutputTokens) case bridge.StreamChunkError: return c.Error }}HTTP endpoints
What the embedded server answers. These back the web UI rather than forming a stable public API: they are documented so you can script against a running instance, not so you can depend on them across versions.
| Method | Path |
|---|---|
| GET | / |
| GET | /chat |
| POST | /chat |
| GET | /chat/stream |
| GET | /config |
| POST | /config |
| GET | /policy |
| POST | /policy |
| POST | /config/mcp/test |
| GET | /agents |
| POST | /agents |
| POST | /agents/delete |
| GET | /api/agents/{id}/run |
| GET | /workflows |
| POST | /workflows |
| POST | /workflows/delete |
| GET | /api/workflows/{id}/run |
| GET | /skills |
| POST | /skills/install |
| POST | /skills/remove |
| GET | /memory |
| POST | /memory/prune |
| POST | /memory/record/{id}/delete |
| GET | /memory/backfill |
| GET | /spend |
| GET | /api/recent-calls |
| GET | /api/chats |
| POST | /api/chats |
| GET | /api/chats/{id} |
| PATCH | /api/chats/{id} |
| DELETE | /api/chats/{id} |
| GET | /api/providers |
| GET | /api/models/{provider} |
| GET | /login |
| POST | /login |
| POST | /logout |
Limits
Constants in the source, not tuning knobs. Each row names the file it is read from.
| Parameter | Value |
|---|---|
| Tool-call hops per turn | 10 |
| Tools executed in parallel | 4 |
| Tool name pattern | [a-zA-Z_][a-zA-Z0-9_]{0,63} |
| MCP connect timeout, per server | 10 s |
| MCP initialize handshake | 15 s |
| Policy reload poll interval | 2 s |
| Memory half-life, working | 24 h |
| Memory half-life, episodic | 720 h |
| Memory half-life, profile | none, does not decay |
| Default bind address | 127.0.0.1:7844 |
| Agent delegation depth | 2 |
| Workflow steps in parallel, per level | 4 |
| Skill catalogue line length | 200 chars |
| Skill file read cap | 256 KiB |
| Minimum Go toolchain | 1.23 |
Changelog
Two entries, neither of them tagged. Nothing has been published under this name yet, and the first tagged release will replace both. 1.1 is additive: no 1.0 public API changed shape, and a 1.0 config.yaml boots unmodified.
1.1.1-dev
Provider brand marks in the UI, and the fixes found by auditing 1.1 and by watching a real install fail.
Fixed
- Every stdio MCP server was dead on arrival. The client took its lifetime from the context it was handed to connect with, and the caller cancelled that as soon as the handshake returned, which is the correct thing to do with a deadline. The server was killed microseconds after reporting its tools: the config page showed it connected, and every tool call after that returned a closed transport. The connect context is a deadline now and nothing more.
- Saving the config page while a chat was streaming could kill the process. Closing the memory store nulled its database handle, and the episodic write that Brain fires after a call reads that handle without a nil check, so an ordinary race became a panic in a goroutine with nothing above it to recover.
- Saving the config page reset accumulated spend to zero, so a budget ceiling stopped counting everything spent before the save. Recorded spend is replayed into the tracker on every rebuild now, not only at startup.
- The config page destroyed any MCP command containing a space. A saved argument list was rendered back into the text input without quoting, so the next save split C:\Program Files\node\npx.cmd into two arguments and the server stopped starting. The user never touched the field.
- Routing policy is hot-reloaded on edit, as described. The router has always supported it, but the server never switched it on, so editing policy.yaml by hand did nothing until the next restart.
- serve --bind ::1 failed to listen. The port was recovered by searching for the last colon and rejoined with another, which is fine for IPv4 and produces a malformed address for an IPv6 literal.
- The MCP test-connect result interpolated the server's self-reported name and error text into the page without escaping, on the page that holds every provider API key.
- Installing a skill pack rejected a path ending in a backslash, which is what both PowerShell and cmd produce when you tab-complete a directory.
- Release archives no longer print a warning per file when extracted with GNU tar. They are built on macOS, where tar copies the com.apple.provenance attribute into the archive as a header GNU tar does not recognise.
- Release archives expanded their contents straight into the current directory, so the documented install steps failed: tar refused to write ./bridge over an existing bridge/ directory, and the xattr and mv steps referenced a bridge/bridge path that never existed. Each archive now contains one bridge-<version>-<os>-<arch>/ directory.
- The config page listed three of the fourteen providers. The view builder, the form handler and the chat page's picker each hardcoded the old list, so eleven providers were configurable in the YAML and invisible in the UI.
- Saving the config page wiped a hand-written auth or headers field, because the handler rebuilt each provider block from a literal naming only four fields.
- Upgrading a 1.0 config reported agents and workflows as disabled. Those blocks do not exist in a 1.0 file and a plain bool reads absent as false; both flags are now tri-state, where absent means on.
- sqlite.Open, agent.NewFileStore and workflow.NewFileStore now expand a leading ~ and create a missing parent directory. The paths the documentation names were failing for anyone calling the library directly.
Changed
- Providers show the vendor's own mark on the config page and in the spend breakdown. Nine ship a mark; the five without one get a monogram rather than an approximated logo.
- The spend breakdown gives each provider a distinct colour. It previously knew three providers and rendered the rest as identical grey bars.
- The install documentation describes one path: the archives on api.clrtstudio.com.
1.1.0-dev
The release where bridge stops being only a client and becomes a place to build things.
Agents
- A saved agent is a system prompt, a model or a task to route by, a memory scope, a budget and an allowlist over the tool pool. One YAML file each under ~/.bridge/agents, so a team can review them in a merge request.
- agent.Runner compiles an Agent into a bridge.Call. The tool allowlist works because Call.Tools overrides the Brain's registered set, which is load-bearing rather than incidental.
- The agent package imports neither skills nor web. The skills catalogue arrives through an injected accessor, so the package can be tested with no Brain at all.
Workflows
- A YAML DAG over agents and inline prompts. Kahn's algorithm returns levels rather than a flat order, so everything on a level runs concurrently, bounded per level.
- Templated with text/template under missingkey=error, over .inputs and .steps.<id>.text. A typo is an error at save, not an empty string three steps later.
- A failed step skips its transitive dependents instead of running them without their inputs. Cycles are rejected at save with the members named.
- workflow.Executor is an interface the caller supplies, which is why the engine's whole test suite runs without opening a socket.
Skills
- The Agent Skills format: a directory with a SKILL.md carrying YAML frontmatter, optionally with references/ and scripts/ beside it. The same layout github.com/google/skills uses.
- Progressive disclosure is the point. Only name and description sit in context; the model pulls the body with bridge_skill_read and a bundled file with bridge_skill_file. A hundred skills cost about a hundred lines, not a hundred documents.
- Catalog.ReadFile is path-confined against .., absolute paths and symlinks that escape the skill directory, with tests for each. Without that it is an arbitrary local file read wearing a tool's name.
- Installs from a directory or a shallow git clone. Nothing is executed at install time.
Providers
- Gemini is a real adapter: generateContent, streamGenerateContent, systemInstruction folding and functionDeclarations. Gemini has no system role in contents and calls the assistant model, so both are translated. It also reports a safety refusal as HTTP 200 with finishReason SAFETY, which is handled, because otherwise it reads as a successful empty answer.
- providers/compat is one adapter for the OpenAI chat-completions wire format with a preset catalogue over it. That is how ten vendors arrive for one adapter's worth of test surface: xai, groq, deepseek, mistral, together, fireworks, cerebras, openrouter, lmstudio and vllm. Adding the eleventh is a preset, not a package.
- providers/xai and providers/openrouter are named wrappers over their presets, so callers do not have to know compat exists.
- Anthropic accepts an OAuth bearer as well as an API key. Supplying both is refused at construction, because upstream rejects a request carrying both and silently preferring one would make the other look broken.
Credentials
- New auth package: an OAuth 2.0 authorization-code flow with PKCE on a loopback listener, the device-code flow, refresh, and a credential store at ~/.bridge/credentials.json mode 0600 that refuses to read a file the group or world can.
- bridge login covers the paths a vendor sanctions: Google, via your own OAuth client or gcloud auth application-default login, and Anthropic, via a developer profile held by the ant CLI.
- bridge does not replay another application's OAuth client id. A Claude Pro or Max, ChatGPT Plus, or X Premium subscription grants no API access to a third-party client, and bridge login openai says so rather than returning an unknown-provider error.
Web UI and CLI
- Three pages: Agents, Workflows, Skills. Agents get a form and a live run panel, workflows get a YAML editor with the computed execution levels and a streaming trace, skills get a browser and an installer.
- The chat reaches all three through tool pairs: bridge_agent_list and bridge_agent_run, bridge_workflow_list and bridge_workflow_run, and the three skill tools. A pair rather than one tool per saved definition, because tool schemas are re-sent on every provider turn. Delegation depth is capped so an agent that can reach itself stops before the budget does.
- New subcommands: bridge login, bridge skills, bridge agents and bridge workflows.
- scripts/release.sh cross-compiles all six targets and writes SHA256SUMS and latest.json.
Fixed
- claude-opus-4-7 was priced at 15.00 / 75.00 per Mtok in budget/pricing.yaml, roughly 3x its real rate. Every budget gate against it was wrong in the expensive direction.
- The Anthropic model catalogue reported a 200K context window for Opus and Sonnet. Both are 1M, so ModelInfo.ContextTokens was under-sizing every transcript by 5x.
- /favicon.ico read web/static/logo.svg, which did not exist. It 404'd on every page load since the route was added.
- The chat avatar was drawing a cartoon face left over from an earlier identity, inline in a JavaScript string.
Known gaps
- The pricing blocks added for gemini, xai, groq, deepseek, mistral, together, fireworks, cerebras and openrouter are marked UNVERIFIED in budget/pricing.yaml. They were entered from general knowledge, not from each vendor's pricing page. Confirm them before leaning on a budget ceiling for those providers.
- OpenRouter is deliberately near-empty in the pricing table, so its spend does not count against a bridge ceiling unless you list the models you route to. A wrong number would be worse than a known absent one.
- Gemini's long-context premium tier is not modelled, so a very long Pro call under-estimates.
1.0.0-dev
Initial import: a Go library, a CLI and an embedded web UI for routing calls across providers with budget tracking, persistent memory and MCP tools.
Library
- Brain coordination layer over provider, router, budget, memory and tool interfaces, built with functional options. The root package imports no concrete implementation.
- Brain.Call and Brain.Stream share one pipeline: route, budget gate, memory hydrate, bounded parallel tool loop, charge and record.
- Public surface is API-frozen in shape. Breaking changes get a deprecation cycle once a version is tagged.
Providers
- Anthropic (Messages API), OpenAI (Responses API) and Ollama, with streaming, tool use and a unified StreamChunk event model.
- Gemini and OpenRouter packages exist but carry no tests yet, and are not shipping features.
- Every adapter translates a system-role message into the upstream API's native system-prompt mechanism, which is what memory hydration depends on.
Memory
- SQLite backend with FTS5 keyword search and pure-Go cosine over stored vectors. Working, episodic and profile records.
- Importance decay is pow(0.5, age/half_life), literal half-life semantics.
- Recall returns effective decayed importance, not the base value.
- Lazy embedding backfill worker and automatic pruning, both started by the concrete store rather than the narrow Memory interface.
Routing
- YAML policy per task kind mapping to prefer and fallback lists of provider:model slugs, hot-reloaded on a 2s file-stat poll.
- Privacy tier and per-task cost cap filter candidates before the first viable one wins.
MCP
- Client targeting protocol revision 2025-06-18, over stdio and Streamable HTTP.
- A version mismatch warns rather than failing, since initialize, tools/list and tools/call have been stable across revisions.
- Tool names are prefixed with _ as the separator, to stay inside the pattern Anthropic and OpenAI both enforce.
Web UI
- Embedded with go:embed. No Node, no build step. Chat, config, policy, memory and spend.
- Conversation history persisted to SQLite; spend persisted across restarts.
- The server refuses to bind a non-localhost address without an auth token, because the config holds API keys.
CLI
- bridge serve, init, setup and version, on the standard library's flag package.
- init writes the config at mode 0600 with every provider present and disabled.
Build
- Pure Go, CGO disabled, one static binary. Go 1.23 floor.
- Two direct dependencies: modernc.org/sqlite and gopkg.in/yaml.v3.