Skip to content

Bootstrap and GitOps

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:

Terminal window
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.

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
Terminal window
# find_by_name "/api/v1/models" "openrouter-default" → prints UUID or empty string
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 // "")'
}
# 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
}

Resources reference each other by name. Create them in dependency order:

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 bash
set -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 1
fi
echo " 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/null
echo ""
# -- 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/null
echo ""
# -- 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/null
echo ""
# -- 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/null
echo ""
# -- 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/null
echo ""
# -- 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/null
echo ""
# -- 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/null
echo ""
# -- 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:

Terminal window
export ENGINE_URL=http://localhost:8443
export OPENROUTER_API_KEY=sk-or-...
export TAVILY_API_KEY=tvly-...
bash bootstrap.sh

For teams that prefer Python:

#!/usr/bin/env python3
"""bootstrap.py — apply a ByteBrew Engine configuration idempotently."""
import os, sys
import 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")
# Auth
resp = 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. Models
print("==> 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 server
print("==> 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 base
print("==> 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. Agents
print("==> 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. Capability
print("==> 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. Schema
print("==> 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 relation
print("==> 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

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.sh

The 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.

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:

Terminal window
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}"
done

The bootstrap script manages configuration only. Do not attempt to seed or migrate:

CategoryReason
SecretsPass through environment variables. Never store in config files or the script.
Runtime stateSessions, messages, agent run history — these belong to users, not deployments.
Per-user dataAPI tokens, user accounts — created through Admin Dashboard or your IdP.
UUIDsReference resources by name in your scripts. Fetch IDs dynamically when a UUID is required (e.g. entry_agent_id).