Bootstrap and GitOps
Why seed-via-API
Section titled “Why seed-via-API”ByteBrew does not have a proprietary bundle or YAML import format for full-stack configuration. This is intentional: every resource you need — models, MCP servers, agents, knowledge bases, schemas — is already manageable through the same REST endpoints the Admin Dashboard uses. A short bash or Python script that calls these endpoints in the right order gives you a reproducible, GitOps-friendly bootstrap with no special tooling. The script is idempotent: run it on every deploy and it only updates what changed.
Before calling any management endpoint, obtain a Bearer token. On a self-hosted CE installation with the default BYTEBREW_AUTH_MODE=local, the engine mints tokens on demand:
BB_TOKEN=$(curl -s -X POST "${ENGINE_URL}/api/v1/auth/local-session" | jq -r '.access_token')The returned JWT is valid for the session. For production automation, create a long-lived API token through Admin Dashboard → API Keys and store it in your CI vault.
The idempotency pattern: read-then-write
Section titled “The idempotency pattern: read-then-write”Always check what exists before writing. The script does GET /resource to find the resource by name, then either POST (if absent) or PATCH (if present). This is the same pattern the Admin Dashboard uses internally. It does not depend on the engine returning a specific error code on duplicates — the script makes the decision itself.
Two helpers implement this uniformly across all resource types:
find_by_name— GET the list, return the ID of the resource with the given name (or empty string)apply_resource— given a list path, a name, and a payload, GET first; if found PATCH by ID, else POST
# find_by_name "/api/v1/models" "openrouter-default" → prints UUID or empty stringfind_by_name() { local list_path="$1" name="$2" curl -s "${ENGINE_URL}${list_path}" \ -H "Authorization: Bearer $BB_TOKEN" \ | jq -r --arg n "$name" '(.items? // .) | map(select(.name == $n)) | (.[0].id // "")'}
# apply_resource "/api/v1/models" "openrouter-default" "name" '<json>' ['<patch-json>']# Second arg: "name" if PATCH uses /resource/{name}, "id" if PATCH uses /resource/{uuid}.# Returns the resource ID (prints to stdout on last line).# patch_payload defaults to post_payload when omitted.apply_resource() { # patch_key: "name" → PATCH /resource/{name} (models, agents, mcp-servers) # "id" → PATCH /resource/{name} (schemas, knowledge-bases use name-based PATCH in 1.1.0+) local list_path="$1" name="$2" patch_key="$3" post_payload="$4" patch_payload="${5:-$4}" local existing_id existing_id=$(find_by_name "$list_path" "$name") if [ -n "$existing_id" ]; then local key="$existing_id" [ "$patch_key" = "name" ] && key="$name" echo " '$name' exists ($existing_id), updating..." >&2 assert_2xx PATCH "${list_path}/${key}" "$patch_payload" else echo " Creating '$name'..." >&2 assert_2xx POST "$list_path" "$post_payload" fi find_by_name "$list_path" "$name"}
assert_2xx() { local method="$1" path="$2" payload="${3:-}" local code code=$(curl -s -o /tmp/bb_resp.json -w "%{http_code}" \ -X "$method" "${ENGINE_URL}${path}" \ -H "Authorization: Bearer $BB_TOKEN" \ -H "Content-Type: application/json" \ ${payload:+-d "$payload"}) case "$code" in 2*) ;; *) echo "ERROR: $method $path → HTTP $code" >&2 cat /tmp/bb_resp.json >&2 2>/dev/null exit 1 ;; esac}Order of operations
Section titled “Order of operations”Resources reference each other by name. Create them in dependency order:
Full bootstrap script
Section titled “Full bootstrap script”The script below provisions a complete working installation: one chat model via OpenRouter, one embedding model, one MCP server (Tavily web search), one knowledge base, two agents (a router and a worker), memory capability on the router, a KB link, one schema, and one agent relation. All secrets come from environment variables.
The script requires only bash, curl, and jq — all commonly available in CI environments.
#!/usr/bin/env bashset -euo pipefail
ENGINE_URL="${ENGINE_URL:-http://localhost:8443}"
# -- Guard: required env vars -------------------------------------------------: "${OPENROUTER_API_KEY:?OPENROUTER_API_KEY env var must be set}": "${TAVILY_API_KEY:?TAVILY_API_KEY env var must be set}"
# -- Helpers ------------------------------------------------------------------
find_by_name() { local list_path="$1" name="$2" curl -s "${ENGINE_URL}${list_path}" \ -H "Authorization: Bearer $BB_TOKEN" \ | jq -r --arg n "$name" '(.items? // .) | map(select(.name == $n)) | (.[0].id // "")'}
assert_2xx() { local method="$1" path="$2" payload="${3:-}" local code code=$(curl -s -o /tmp/bb_resp.json -w "%{http_code}" \ -X "$method" "${ENGINE_URL}${path}" \ -H "Authorization: Bearer $BB_TOKEN" \ -H "Content-Type: application/json" \ ${payload:+-d "$payload"}) case "$code" in 2*) ;; *) echo "ERROR: $method $path → HTTP $code" >&2 cat /tmp/bb_resp.json >&2 2>/dev/null exit 1 ;; esac}
apply_resource() { # patch_key: "name" → PATCH /resource/{name} (models, agents, mcp-servers) # "id" → PATCH /resource/{uuid} (schemas, knowledge-bases) local list_path="$1" name="$2" patch_key="$3" post_payload="$4" patch_payload="${5:-$4}" local existing_id existing_id=$(find_by_name "$list_path" "$name") if [ -n "$existing_id" ]; then local key="$existing_id" [ "$patch_key" = "name" ] && key="$name" echo " '$name' exists ($existing_id), updating..." >&2 assert_2xx PATCH "${list_path}/${key}" "$patch_payload" else echo " Creating '$name'..." >&2 assert_2xx POST "$list_path" "$post_payload" fi find_by_name "$list_path" "$name"}
# apply_capability: GET list, PUT if found, POST if absent.# Capabilities use PUT /agents/{name}/capabilities/{id} for updates.apply_capability() { local agent_name="$1" cap_type="$2" payload="$3" local caps_path="/api/v1/agents/${agent_name}/capabilities" local existing_id existing_id=$(curl -s "${ENGINE_URL}${caps_path}" \ -H "Authorization: Bearer $BB_TOKEN" \ | jq -r --arg t "$cap_type" 'map(select(.type == $t)) | (.[0].id // "")') if [ -n "$existing_id" ]; then echo " Capability '$cap_type' exists ($existing_id), updating..." >&2 assert_2xx PUT "${caps_path}/${existing_id}" "$payload" else echo " Adding capability '$cap_type'..." >&2 assert_2xx POST "$caps_path" "$payload" fi}
# apply_relation: GET list, skip if (source, target) pair already exists, POST if not.# source and target are agent names; the engine resolves them to UUIDs.# Response fields "source" and "target" contain agent names (not UUIDs).apply_relation() { local schema_id="$1" source="$2" target="$3" local rels_path="/api/v1/schemas/${schema_id}/agent-relations" local existing existing=$(curl -s "${ENGINE_URL}${rels_path}" \ -H "Authorization: Bearer $BB_TOKEN" \ | jq -r --arg s "$source" --arg t "$target" \ 'map(select(.source == $s and .target == $t)) | (.[0].id // "")') if [ -n "$existing" ]; then echo " Relation '$source' → '$target' already exists, skipping." >&2 else echo " Creating relation '$source' → '$target'..." >&2 assert_2xx POST "$rels_path" \ "{\"source\": \"$source\", \"target\": \"$target\", \"config\": {}}" fi}
# -- Auth ---------------------------------------------------------------------echo "==> Auth"BB_TOKEN=$(curl -s -X POST "${ENGINE_URL}/api/v1/auth/local-session" | jq -r '.access_token')if [ -z "$BB_TOKEN" ] || [ "$BB_TOKEN" = "None" ]; then echo "ERROR: failed to obtain auth token" >&2 echo "Response: $AUTH_RESP" >&2 exit 1fiecho " Token acquired."echo ""
# -- 1. Chat model ------------------------------------------------------------echo "==> Step 1: chat model"# PATCH payload omits "type" and "kind": immutable after creation.apply_resource "/api/v1/models" "openrouter-default" "name" \ '{ "name": "openrouter-default", "type": "openrouter", "kind": "chat", "base_url": "https://openrouter.ai/api/v1", "model_name": "openai/gpt-4o-mini", "api_key": "'"${OPENROUTER_API_KEY}"'", "is_default": true }' \ '{ "base_url": "https://openrouter.ai/api/v1", "model_name": "openai/gpt-4o-mini", "api_key": "'"${OPENROUTER_API_KEY}"'", "is_default": true }' > /dev/nullecho ""
# -- 2. Embedding model -------------------------------------------------------echo "==> Step 2: embedding model"apply_resource "/api/v1/models" "openrouter-embedding" "name" \ '{ "name": "openrouter-embedding", "type": "openrouter", "kind": "embedding", "base_url": "https://openrouter.ai/api/v1", "model_name": "openai/text-embedding-3-small", "api_key": "'"${OPENROUTER_API_KEY}"'" }' \ '{ "base_url": "https://openrouter.ai/api/v1", "model_name": "openai/text-embedding-3-small", "api_key": "'"${OPENROUTER_API_KEY}"'" }' > /dev/nullecho ""
# -- 3. MCP server (Tavily web search) ----------------------------------------echo "==> Step 3: MCP server"apply_resource "/api/v1/mcp-servers" "tavily" "name" \ '{ "name": "tavily", "type": "http", "url": "https://mcp.tavily.com/mcp/", "auth_type": "api_key", "auth_key_env": "TAVILY_API_KEY", "enabled": true }' \ '{ "url": "https://mcp.tavily.com/mcp/", "auth_type": "api_key", "auth_key_env": "TAVILY_API_KEY", "enabled": true }' > /dev/nullecho ""
# -- 4. Knowledge base --------------------------------------------------------echo "==> Step 4: knowledge base"apply_resource "/api/v1/knowledge-bases" "kb-docs" "name" \ '{ "name": "kb-docs", "description": "Product documentation knowledge base", "embedding_model": "openrouter-embedding" }' \ '{"description": "Product documentation knowledge base"}' > /dev/nullecho ""
# -- 5a. Router agent ---------------------------------------------------------echo "==> Step 5a: router agent"# PATCH omits can_spawn: empty list would wipe existing delegation targets.apply_resource "/api/v1/agents" "router" "name" \ '{ "name": "router", "model": "openrouter-default", "lifecycle": "persistent", "system_prompt": "You are a routing agent. Understand the user request and delegate to the worker agent when detailed research or tool use is needed.", "can_spawn": ["worker"] }' \ '{ "model": "openrouter-default", "lifecycle": "persistent", "system_prompt": "You are a routing agent. Understand the user request and delegate to the worker agent when detailed research or tool use is needed." }' > /dev/nullecho ""
# -- 5b. Worker agent ---------------------------------------------------------echo "==> Step 5b: worker agent"# PATCH omits mcp_servers: empty list would wipe existing server assignments.apply_resource "/api/v1/agents" "worker" "name" \ '{ "name": "worker", "model": "openrouter-default", "lifecycle": "spawn", "system_prompt": "You are a research worker. Use available tools to gather information and return a structured answer.", "mcp_servers": ["tavily"], "knowledge_bases": ["kb-docs"] }' \ '{ "model": "openrouter-default", "lifecycle": "spawn", "system_prompt": "You are a research worker. Use available tools to gather information and return a structured answer.", "knowledge_bases": ["kb-docs"] }' > /dev/nullecho ""
# -- 6. Memory capability on router -------------------------------------------echo "==> Step 6: memory capability on router"apply_capability "router" "memory" '{"type": "memory", "enabled": true, "config": {}}'echo ""
# -- 7. Link knowledge base to worker agent -----------------------------------echo "==> Step 7: link KB to worker"# LinkAgent uses FirstOrCreate internally — always safe to call.assert_2xx POST "/api/v1/knowledge-bases/kb-docs/agents/worker"echo " KB linked."echo ""
# -- 8. Schema ----------------------------------------------------------------echo "==> Step 8: schema"apply_resource "/api/v1/schemas" "support" "name" \ '{ "name": "support", "description": "Customer support workflow", "entry_agent": "router", "chat_enabled": true }' \ '{ "description": "Customer support workflow", "entry_agent": "router", "chat_enabled": true }' > /dev/nullecho ""
# -- 9. Agent relation (router → worker) --------------------------------------echo "==> Step 9: agent relation router -> worker"apply_relation "support" "router" "worker"echo ""
echo "============================================"echo "Bootstrap complete."echo "Schema name: support"echo "Chat endpoint: POST ${ENGINE_URL}/api/v1/schemas/support/chat"echo "============================================"Run it:
export ENGINE_URL=http://localhost:8443export OPENROUTER_API_KEY=sk-or-...export TAVILY_API_KEY=tvly-...bash bootstrap.shPython alternative
Section titled “Python alternative”For teams that prefer Python:
#!/usr/bin/env python3"""bootstrap.py — apply a ByteBrew Engine configuration idempotently."""import os, sysimport requests
ENGINE_URL = os.environ.get("ENGINE_URL", "http://localhost:8443")
for var in ("OPENROUTER_API_KEY", "TAVILY_API_KEY"): if not os.environ.get(var): sys.exit(f"ERROR: {var} env var must be set")
# Authresp = requests.post(f"{ENGINE_URL}/api/v1/auth/local-session")resp.raise_for_status()token = resp.json()["access_token"]H = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
def find_by_name(path: str, name: str) -> str | None: """GET list, return id of item whose name matches, or None.""" r = requests.get(f"{ENGINE_URL}{path}", headers=H) r.raise_for_status() data = r.json() if not isinstance(data, list): data = data.get("items", []) return next((x["id"] for x in data if x.get("name") == name), None)
def apply_resource( path: str, name: str, patch_key: str, payload: dict, patch_payload: dict | None = None) -> str: """GET first; PATCH if found, POST if not. Returns the resource ID.
patch_key: "name" → PATCH /resource/{name} (models, agents, mcp-servers) "id" → PATCH /resource/{uuid} (schemas, knowledge-bases) """ existing_id = find_by_name(path, name) if existing_id: key = name if patch_key == "name" else existing_id print(f" '{name}' exists ({existing_id}), updating...") r = requests.patch( f"{ENGINE_URL}{path}/{key}", headers=H, json=patch_payload if patch_payload is not None else payload, ) else: print(f" Creating '{name}'...") r = requests.post(f"{ENGINE_URL}{path}", headers=H, json=payload) if not r.ok: sys.exit(f"ERROR: {r.request.method} {path} → HTTP {r.status_code}: {r.text}") return find_by_name(path, name)
def apply_capability(agent_name: str, cap_type: str, payload: dict) -> None: """GET capabilities; PUT if found, POST if not.""" path = f"/api/v1/agents/{agent_name}/capabilities" r = requests.get(f"{ENGINE_URL}{path}", headers=H) r.raise_for_status() caps = r.json() if isinstance(r.json(), list) else [] existing = next((c for c in caps if c.get("type") == cap_type), None) if existing: print(f" Capability '{cap_type}' exists ({existing['id']}), updating...") r = requests.put(f"{ENGINE_URL}{path}/{existing['id']}", headers=H, json=payload) else: print(f" Adding capability '{cap_type}'...") r = requests.post(f"{ENGINE_URL}{path}", headers=H, json=payload) if not r.ok: sys.exit(f"ERROR capability {cap_type}: HTTP {r.status_code}: {r.text}")
def apply_relation(schema_id: str, source: str, target: str) -> None: """GET relations; skip if (source, target) pair exists, POST if not. source and target are agent names.""" path = f"/api/v1/schemas/{schema_id}/agent-relations" r = requests.get(f"{ENGINE_URL}{path}", headers=H) r.raise_for_status() rels = r.json() if isinstance(r.json(), list) else [] if any(rel.get("source") == source and rel.get("target") == target for rel in rels): print(f" Relation '{source}' → '{target}' already exists, skipping.") return print(f" Creating relation '{source}' → '{target}'...") r = requests.post( f"{ENGINE_URL}{path}", headers=H, json={"source": source, "target": target, "config": {}} ) if not r.ok: sys.exit(f"ERROR relation {source}→{target}: HTTP {r.status_code}: {r.text}")
# 1. Modelsprint("==> Step 1: chat model")apply_resource( "/api/v1/models", "openrouter-default", "name", {"name": "openrouter-default", "type": "openrouter", "kind": "chat", "base_url": "https://openrouter.ai/api/v1", "model_name": "openai/gpt-4o-mini", "api_key": os.environ["OPENROUTER_API_KEY"], "is_default": True}, {"base_url": "https://openrouter.ai/api/v1", "model_name": "openai/gpt-4o-mini", "api_key": os.environ["OPENROUTER_API_KEY"], "is_default": True},)
print("==> Step 2: embedding model")apply_resource( "/api/v1/models", "openrouter-embedding", "name", {"name": "openrouter-embedding", "type": "openrouter", "kind": "embedding", "base_url": "https://openrouter.ai/api/v1", "model_name": "openai/text-embedding-3-small", "api_key": os.environ["OPENROUTER_API_KEY"]}, {"base_url": "https://openrouter.ai/api/v1", "model_name": "openai/text-embedding-3-small", "api_key": os.environ["OPENROUTER_API_KEY"]},)
# 2. MCP serverprint("==> Step 3: MCP server")apply_resource( "/api/v1/mcp-servers", "tavily", "name", {"name": "tavily", "type": "http", "url": "https://mcp.tavily.com/mcp/", "auth_type": "api_key", "auth_key_env": "TAVILY_API_KEY", "enabled": True}, {"url": "https://mcp.tavily.com/mcp/", "auth_type": "api_key", "auth_key_env": "TAVILY_API_KEY", "enabled": True},)
# 3. Knowledge baseprint("==> Step 4: knowledge base")apply_resource( "/api/v1/knowledge-bases", "kb-docs", "name", {"name": "kb-docs", "description": "Product documentation knowledge base", "embedding_model": "openrouter-embedding"}, {"description": "Product documentation knowledge base"},)
# 4. Agentsprint("==> Step 5a: router agent")apply_resource( "/api/v1/agents", "router", "name", {"name": "router", "model": "openrouter-default", "lifecycle": "persistent", "system_prompt": "You are a routing agent.", "can_spawn": ["worker"]}, {"model": "openrouter-default", "lifecycle": "persistent", "system_prompt": "You are a routing agent."},)
print("==> Step 5b: worker agent")apply_resource( "/api/v1/agents", "worker", "name", {"name": "worker", "model": "openrouter-default", "lifecycle": "spawn", "system_prompt": "You are a research worker.", "mcp_servers": ["tavily"], "knowledge_bases": ["kb-docs"]}, {"model": "openrouter-default", "lifecycle": "spawn", "system_prompt": "You are a research worker.", "knowledge_bases": ["kb-docs"]},)
# 5. Capabilityprint("==> Step 6: memory capability")apply_capability("router", "memory", {"type": "memory", "enabled": True, "config": {}})
# 6. KB link (FirstOrCreate — always safe)print("==> Step 7: link KB to worker")r = requests.post(f"{ENGINE_URL}/api/v1/knowledge-bases/kb-docs/agents/worker", headers=H)if not r.ok: sys.exit(f"ERROR KB link: HTTP {r.status_code}: {r.text}")
# 7. Schemaprint("==> Step 8: schema")apply_resource( "/api/v1/schemas", "support", "name", {"name": "support", "description": "Customer support workflow", "entry_agent": "router", "chat_enabled": True}, {"description": "Customer support workflow", "entry_agent": "router", "chat_enabled": True},)
# 8. Agent relationprint("==> Step 9: agent relation")apply_relation("support", "router", "worker")
print(f"\nBootstrap complete.")print(f"Schema name: support")print(f"Chat endpoint: POST {ENGINE_URL}/api/v1/schemas/support/chat")Install dependencies: pip install requests
Wiring into CI
Section titled “Wiring into CI”Commit bootstrap.sh (or bootstrap.py) to your repository. Add a CI step that runs the script on every push to your deployment branch:
# GitHub Actions example- name: Bootstrap ByteBrew env: ENGINE_URL: ${{ secrets.ENGINE_URL }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} run: bash bootstrap.shThe script is idempotent — running it twice does not create duplicate resources. Existing resources receive a PATCH (or PUT for capabilities) update with the current values from the script.
Store all secrets (ENGINE_URL, API keys) in your CI vault (GitHub Actions secrets, HashiCorp Vault, AWS Secrets Manager, etc.). Never commit them to the repository.
Knowledge document upload
Section titled “Knowledge document upload”After the knowledge base exists, upload documents for indexing. The engine accepts PDF and Markdown files up to 50 MB each via multipart/form-data:
KB_ID=$(find_by_name "/api/v1/knowledge-bases" "kb-docs")
for f in docs/*.pdf docs/*.md; do echo "Uploading $f..." curl -s -o /dev/null -w "%{http_code} $f\n" \ -X POST "${ENGINE_URL}/api/v1/knowledge-bases/${KB_ID}/files" \ -H "Authorization: Bearer $BB_TOKEN" \ -F "file=@${f}"doneWhat is NOT in scope
Section titled “What is NOT in scope”The bootstrap script manages configuration only. Do not attempt to seed or migrate:
| Category | Reason |
|---|---|
| Secrets | Pass through environment variables. Never store in config files or the script. |
| Runtime state | Sessions, messages, agent run history — these belong to users, not deployments. |
| Per-user data | API tokens, user accounts — created through Admin Dashboard or your IdP. |
| UUIDs | Reference resources by name in your scripts. Fetch IDs dynamically when a UUID is required (e.g. entry_agent_id). |