baectl is a command-line HTTP client for the admin API
(/admin/v1/*). It ships as a static binary at /usr/local/bin/baectl inside
both the dev and production images, alongside baesrv. Run it with
docker exec/container exec against a running container — it needs no Rust
toolchain and no network access to build or install.
docker exec bae baectl create profile main anthropic-sonnet \
--allowed-tool get_current_timebaectl covers profile and key management, plus one local scaffolding
command, baectl setup, that generates a runnable
deployment (compose file/script, .env, bae-config.toml) before a server
exists to talk to. It does not open sessions or send messages — those hit the
client port (8080) with a client/session key and are documented in the
Client API and the guides.
When run inside the same container as baesrv (the documented deployment —
docker exec/container exec), baectl needs zero flags: it finds the
admin port on loopback and reads the admin key the server wrote to disk at
startup.
Precedence, highest to lowest:
--admin-addr <HOST:PORT>BAE_ADMIN_ADDRenv var- default:
127.0.0.1:8081
A bare host:port value is used as plain HTTP (http://host:port — the
admin port never speaks TLS). A value that already contains :// is used
verbatim, for the rare case of reaching baectl over an SSH tunnel or through
a TLS-terminating proxy.
Precedence, highest to lowest:
--admin-token <TOKEN>/BAE_ADMIN_TOKENenv var — sent verbatim asAuthorization: Bearer <token>. Use this for scripting or an operator-held key that isn't backed by a local file.--admin-key-file <PATH>/BAE_ADMIN_KEY_FILEenv var — reads the plaintext admin key from an explicitly named file (surrounding whitespace is trimmed). If this file is named explicitly and cannot be read, that is a hard runtime error (exit1) — the operator asked for it specifically.- The default probed path,
/var/lib/bae/admin-key.pem— read the same way, but a missing file here is not an error;baectlsimply proceeds with no token. If the server enforces admin auth, the request then fails with401andbaectlprints the guidance in Errors below.
--admin-token, --admin-key-file, and --admin-addr are global flags —
valid before or after the subcommand, on every command.
See Admin authentication for how the server-side key file is created and rotated.
Verb-first, resource-typed positional, mapping 1:1 onto the admin API's CRUD surface. Profiles support the full create/list/get/update/delete set; keys support create/list/delete only — there is no single-key-get or key-update endpoint (keys are immutable besides revocation).
| Command | Admin endpoint |
|---|---|
baectl create profile <name> <primary_provider> |
POST /admin/v1/profiles |
baectl list profiles |
GET /admin/v1/profiles |
baectl get profile <id> |
GET /admin/v1/profiles/{id} |
baectl update profile <id> <primary_provider> |
PUT /admin/v1/profiles/{id} |
baectl delete profile <id> |
DELETE /admin/v1/profiles/{id} |
baectl create key <name> <profile_id> |
POST /admin/v1/keys |
baectl list keys |
GET /admin/v1/keys |
baectl delete key <id> |
DELETE /admin/v1/keys/{id} |
baectl auth create key |
(local only — no API call) |
baectl setup |
(local scaffolding — no API call, except post-launch create profile/create key run inside the container) |
--help is available on every command and subcommand (baectl --help,
baectl create --help, baectl create profile --help, …).
baectl create profile <name> <primary_provider> [flags]
Positionals (required):
| Positional | Description |
|---|---|
name |
Unique profile name. |
primary_provider |
The name of a [providers] entry declared in bae-config.toml (e.g. anthropic-sonnet) — not a provider id or model. See Configuration — [providers]. |
Flags (optional):
| Flag | Description |
|---|---|
--fallback <NAME> |
A fallback [providers] registry name, repeatable, tried in order after the primary fails. |
--mcp-server <NAME> |
MCP server name to enable, repeatable. Omitted entirely → mcp_servers: []. |
--allowed-tool <NAME> |
Client-side tool name to allow, repeatable. Omitted entirely → allowed_tools: [] (no client-side tools permitted). |
--json |
Print the raw JSON response instead of a human summary. |
baectl does not validate --mcp-server/primary_provider/--fallback
names against the live MCP/provider registries — both registries are
config-file-driven and can differ across restarts. A typo'd MCP server name
is caught non-fatally at session-creation time (see
MCP Servers); an unresolvable
primary_provider is fatal at session-creation time (see
Profiles — Fatal primary / non-fatal fallback).
baectl never builds or sends provider config (URL, auth token, max
tokens) — that is entirely operator-managed bae-config.toml on the
server, listable via GET /admin/v1/providers.
Output (human):
created profile
id: pro_a1b2c3d4e5f6…
name: main
created_at: 2026-07-06T18:26:01.123Z
Output (--json): the raw {id, name, created_at} document the API
returned.
Errors: duplicate name (409 duplicate_name), malformed body
(400 bad_request). See Errors.
baectl list profiles [--limit <N>] [--cursor <C>] [--json]
No positionals.
| Flag | Description |
|---|---|
--limit <N> |
Fetch a single page of at most N items. Opts out of auto-pagination. |
--cursor <C> |
Fetch a single page starting at this opaque cursor. Opts out of auto-pagination. |
--json |
Print raw JSON instead of a human table. |
Pagination: with neither --limit nor --cursor, baectl follows
next_cursor until it is null and returns the full result set — a
human running baectl list profiles never needs to know the API is
cursor-paginated. Passing either flag opts back into raw single-page
behavior, for scripting.
Output (human, auto-paginated or single-page): a fixed-width table,
columns ID NAME PRIMARY_PROVIDER. An empty result prints no profiles found
(not an empty table with only headers).
Output (--json):
- Auto-paginated (default): a flat JSON array of every profile.
- Single-page (
--limit/--cursorgiven): the raw page document,{"items": [...], "next_cursor": ...}.
baectl get profile <id> [--json]
| Positional | Description |
|---|---|
id |
Profile id. |
| Flag | Description |
|---|---|
--json |
Print the raw JSON document instead of a human summary. |
Output (human): every field of the profile — id, name,
primary_provider (registry name), fallback_providers (registry names),
mcp_servers, allowed_tools, created_at, updated_at. Empty list fields
print (none).
Output (--json): the full Profile object, same shape as a list
item.
Errors: 404 not_found if the id doesn't exist or was deleted.
baectl update profile <id> <primary_provider> [--name <NAME>] [flags]
Full replacement (PUT) — mirrors the admin API, which always overwrites
every field.
| Positional | Description |
|---|---|
id |
Id of the profile to replace. |
primary_provider |
The [providers] registry name (see create profile). |
| Flag | Description |
|---|---|
--name <NAME> |
New name. Optional — see below. |
(same config flags as create profile) |
--fallback, --mcp-server, --allowed-tool, --json. |
--nameis optional, filling a gap in the admin API.PUT /admin/v1/profiles/{id}always requires anamein its body, butupdate profile's positional signature has none. When--nameis omitted,baectlfirstGETs the current profile and reuses its existing name, so a plainbaectl update profile <id> <primary_provider>changes the provider reference without renaming. Pass--nameto rename during the same replace.
Any repeatable flag left unset (--fallback, --mcp-server,
--allowed-tool) serializes as an explicit empty array in the PUT body —
a full replacement clears fields that aren't re-specified, exactly like a
direct PUT call would.
Output: same as get profile (human full summary, or --json the
replaced Profile object).
Errors: 400 bad_request, 404 not_found.
baectl delete profile <id>
Soft-deletes the profile. No flags, no --json (the API returns
204 No Content).
Output: deleted profile <id> on stdout.
Errors:
404 not_found— no profile with this id.409 profile_in_use— the profile still has active client keys.baectl's message names the suggested next steps: runbaectl list keysto find them, thenbaectl delete key <id>for each, then retry.
baectl create key <name> <profile_id> [--json]
| Positional | Description |
|---|---|
name |
Human label for the key. |
profile_id |
Id of the profile this key is bound to. Must be a non-deleted profile. |
| Flag | Description |
|---|---|
--json |
Print the raw JSON response instead of a human summary. |
Output (human):
created key
id: key_a1b2c3d4e5f6…
name: my-agent
key: bae_1a2b3c4d…
prefix: bae_1a2b
profile_id: pro_…
created_at: 2026-07-06T18:26:05.000Z
The plaintext key field is shown exactly once, in both human and
--json output, followed by a stderr warning:
baectl: copy the key now — it cannot be retrieved again. It is never
logged or cached — copy it immediately.
Errors: 400 bad_request (blank name), 422 profile_unavailable (the
referenced profile does not exist or is deleted).
baectl list keys [--limit <N>] [--cursor <C>] [--json]
Same shape and pagination behavior as list profiles.
Output (human): table, columns ID NAME PREFIX PROFILE_ID. Empty result
prints no keys found.
Output (--json): flat array (auto-paginated) or {items, next_cursor}
(single page).
baectl delete key <id>
Revokes the client key (cascades to its session keys and open sessions — see Admin API → Client Keys). No flags.
Output: revoked key <id> on stdout.
Errors: 404 not_found.
baectl auth create key [--name <NAME>] [--out-dir <DIR>]
This command never calls the admin API. It is a local key-generation utility for pre-provisioning one shared admin credential across multiple independent server replicas. See Admin authentication → multi-replica walkthrough for the full flow.
| Flag | Description |
|---|---|
--name <NAME> |
Name recorded in the hash file (display only, on the server). Default provisioned-admin. |
--out-dir <DIR> |
Directory to write the two output files into. Default . (current directory). |
Writes two files, both with 0600 permissions:
-
<out-dir>/admin-key.pem— the plaintextbae_admin_<48 hex chars>token, single line with a trailing newline (readers must trim). This is the live credential — treat it like a password. Copy it to whereverbaectl/operators run, at the pathBAE_ADMIN_KEY_FILEresolves to. -
<out-dir>/admin-key-hash.pem— a pretty-printed JSON document the server ingests at boot:{ "key_hash": "b8f15df49ca3acc355c07ed98cc11d61d7e172db85ceab49cc3ef02381f983c5", "prefix": "bae_admin_1a2b", "name": "provisioned-admin" }Drop this file onto every replica's data volume at the path
BAE_ADMIN_KEY_HASH_FILEresolves to, before that replica's first boot.
The token is generated with 192 bits of CSPRNG entropy (24 random bytes,
hex-encoded) and hashed with unsalted SHA-256 over its exact bytes, then
encoded as 64 lowercase hexadecimal characters — see
Key security. The deterministic format has no
salt or tunable parameters, so baectl and the server independently produce
the same digest without shared code or out-of-band configuration.
Output: stdout prints the two file paths (scriptable); stderr prints handling guidance for each file.
Errors: a runtime error (exit 1) if either file cannot be written
(e.g. --out-dir doesn't exist or isn't writable).
baectl setup [--dev] [--apple] [--dir <DIR>]
The interactive quickstart wizard: a short series of defaulted stdin/stdout
Q&A questions that produce a runnable deployment — a launcher, a .env, and a
bae-config.toml — in --dir, and can immediately launch it. It is the one
baectl command that is a local scaffolding tool: it runs before a
server exists to talk to, and never links admin_client.rs host-side (the
host has no route to the loopback-only admin port). The one exception is the
optional final launch step, where setup shells out to docker exec/
container exec running baectl create profile/baectl create key
inside the just-started container — the same auto-configuration path
every other documented baectl invocation already uses. This makes setup
the one baectl command that both generates local files and drives a live
server in the same invocation.
Run it on the host, not inside the image. setup drives your host's
container engine (docker compose up -d / ./bae-setup.sh), so it must run
where that engine is — not inside the production image, whose entrypoint is
baesrv and which carries no docker/container client. baectl ships as a
self-contained static binary inside the image; copy it out once and run it on
the host:
cid=$(docker create ghcr.io/prettysmartdev/better-agent-engine:latest)
docker cp "$cid":/usr/local/bin/baectl ./baectl
docker rm "$cid" >/dev/null
./baectl setup(Or use the baectl a source build produced.)
Flags:
| Flag | Description |
|---|---|
--dev |
Use the image tags a local make image/make image-max produces (better-agent-engine:latest / :max) instead of the published GHCR tags (ghcr.io/prettysmartdev/better-agent-engine:latest / :max). For contributors iterating on a local build. |
--apple |
Emit bae-setup.sh (a shell script driving Apple's container CLI) instead of docker-compose.yml. Both output modes read the same .env. |
--dir <DIR> |
Directory to read/write the three generated files in. Default . (current directory), mirroring auth create key's --out-dir convention. |
No flag is required — baectl setup with no arguments still produces a
complete, working setup, consistent with every other baectl/baesrv
command's "no required flags" convention.
Runs top to bottom; [default] is shown inline and a bare enter accepts it.
On a directory with an existing, complete setup, the wizard is skipped
entirely in favor of a Launch/Edit choice (see
Idempotency below); when Edit is chosen, every
question's default below is pre-filled from the existing files instead.
The two mode flags are also answerable interactively: passing --apple or
--dev on the command line pre-fills and skips its question, while omitting the
flag makes setup ask it (both default to "no", i.e. docker-compose.yml and
the published image tags), so baectl setup with no flags still produces a
complete setup:
-
Use Apple's
containerCLI (instead of docker-compose)? — defaultN; skipped when--appleis passed. Chosen before anything else because it decides which launcher file the idempotency check looks for. -
Use locally-built (
make image) image tags? — defaultN; skipped when--devis passed. -
Image variant? (
standard/max) — defaultstandard. -
Provider(s) — at least one is required (a profile needs a
primary_provider). The wizard prints "At least one provider is required" and asks for the first provider unconditionally; each subsequent one is gated by Add another provider? (defaultN). Per provider:- Provider kind? (
anthropic/openai) — defaultanthropic. - Registry name? — default
<kind>-default(e.g.anthropic-default). Must be unique among providers added this run; re-prompts on collision or a blank answer. - Model? — default
sonnet-5(anthropic) /gpt-5.6-luna(openai). Not validated against a live model list — a placeholder you can edit later. - Auth token env var name? — default
ANTHROPIC_API_KEY/OPENAI_API_KEY. Stored inbae-config.tomlas${VAR}. - Secret value — only asked if that env var isn't already exported in
setup's own process environment (in which case its value is captured silently, with no prompt). See Secret handling below.
- Provider kind? (
-
MCP server(s) — zero is valid. Looped, gated each time by Add an MCP server? (default
N). Per server:- Which? (
filesystem/fetch/github/custom) — defaultfilesystem.filesystem— stdio,command=npx,args=["-y","@modelcontextprotocol/server-filesystem",<dir>]. Asks Server name? (defaultfilesystem) and Directory to expose? (default/data).fetch— stdio,command=uvx,args=["mcp-server-fetch"]. Asks Server name? (defaultfetch) only.github— http,url=https://api.githubcopilot.com/mcp/,headers.Authorization=Bearer ${GITHUB_TOKEN}. Asks Server name? (defaultgithub) and prompts forGITHUB_TOKENthe same way a provider secret is collected.custom— asks Server name?, Transport? (stdio/http/sse, defaultstdio), then either Command? (defaultnpx) + Args? (space-separated) forstdio, or URL? forhttp/sse.
- Server names must be unique within this run; re-prompts on collision or a blank answer.
- Which? (
-
Other
BAE_*env vars — each optional, its documented server default shown as the default answer; only an answer that differs from the default is written to.env(an unset key means "use the image's built-in default," not "unset"). Asked in this fixed order:BAE_ADDR(0.0.0.0:8080),BAE_LOG(info),BAE_SHUTDOWN_TIMEOUT(30),BAE_TURN_TIMEOUT(120),BAE_SANDBOX_DRIVER(docker). If the image variant ismax, two more questions follow:- MAX web port? — default
3000. - MAX password? (blank = MAX generates one on first boot) — default
blank. A non-blank answer is written to
.envasBAE_MAX_PASSWORD.
BAE_DB_PATH,BAE_ADMIN_ADDR,BAE_CONFIG,BAE_ADMIN_KEY_FILE,BAE_ADMIN_KEY_HASH_FILE, andBAE_OTEL_LOGare not asked — they are wired to fixed container-internal paths by the generated launcher itself, or (forBAE_OTEL_LOG) only matter once[telemetry]is enabled, whichsetupdoes not configure (see[telemetry]is never generated below). - MAX web port? — default
-
Launch now? — default
Ywhen the wizard is running interactively,Notherwise. See Launch step below.
Secret handling. For each secret env var (a provider's auth token,
GITHUB_TOKEN for the github MCP server), setup resolves a value in this
order:
- Already captured earlier in this same run (two providers sharing a var) → reused silently.
- Already exported and non-empty in
setup's own process environment → captured into.envwith no prompt. - On the Edit path with an existing
.envvalue → asks Keep the existing value for VAR? (defaultY); the existing value is never re-echoed to the terminal. - Otherwise prompts Value for VAR? (blank to skip). A blank answer
leaves the variable out of
.env— the${VAR}reference is still written tobae-config.toml, and the variable is listed in a one-time warning printed at the end of the run. Resolution then fails at connect time with the server's existing "unresolved${ENV_VAR}" error.
Known limitation: typed secret values are echoed to the terminal as
you type them — this first cut has no rpassword-style masking. The token
appears in your terminal scrollback/history the same way an inline curl
secret already would; be aware of this if your terminal session is logged or
shared.
Before asking anything, setup checks --dir for the launcher matching this
run's mode (docker-compose.yml, or bae-setup.sh with --apple), .env,
and bae-config.toml:
- None present → the normal wizard runs (fresh setup).
- All three present → prints a summary (image variant, provider names,
MCP server names) and asks
Edit this configuration? (No = launch the saved config as-is)
(default
N):- No (Launch) — reuses the three files verbatim; does not run the wizard, does not create a profile/key (it assumes the ones from the original run still exist — see the note under Launch step).
- Yes (Edit) — backs up the current files to
<file>.bak(one generation deep — a second consecutive edit overwrites the.bak), then re-runs the wizard with every default pre-filled from the existing files, regenerates all three files, and offers to launch.
- The launcher for the other mode is present (e.g.
docker-compose.ymlexists but--applewas passed this run) → treated as a launcher/flag mismatch, handled the same as partial state below. - One or two of the three files present (partial/corrupted state) →
warns which file(s) are missing/mismatched and asks
Overwrite and run a fresh setup? (default
N) before proceeding; a decline leaves every file untouched and exits successfully.
Non-interactive stdin (stdin is not a TTY — e.g. piped from
/dev/null or a CI job): every question above resolves to its default with
no prompt printed at all, as if you hit enter through the entire wizard —
except the launch question, which defaults to N in this mode
specifically (auto-launching from unreviewed, defaulted answers is a
footgun setup avoids). On an existing complete setup, the Edit-vs-Launch
choice also resolves to Launch (reuse verbatim, never an unattended
overwrite).
Both output modes reference the same .env and bae-config.toml — only the
launcher differs.
bae-config.toml (mode 0644) — a provenance header comment, then
[mcp] (always present, even with zero servers) followed by any
[[mcp.servers]] entries, then [providers] followed by the
[[providers.entries]] entries:
# Generated by `baectl setup` (variant: standard, flags: (none), unix: 1752684000).
# Re-run `baectl setup` in this directory to launch or edit it.
[mcp]
[[mcp.servers]]
name = "filesystem"
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
[providers]
[[providers.entries]]
name = "anthropic-default"
provider = "anthropic"
model = "sonnet-5"
auth_token = "${ANTHROPIC_API_KEY}"
[telemetry] is never emitted — an absent section keeps telemetry
disabled per config_file.rs's contract. Add it by hand afterward if you
want OpenTelemetry export (see
Configuration — [telemetry]).
.env (mode 0600) — a provenance header, then secrets (provider auth
vars in provider order, then MCP secret vars, then any remaining secret such
as BAE_MAX_PASSWORD), then non-default BAE_* overrides in the fixed
step-4 order. Only values you supplied or changed appear:
# Generated by `baectl setup` (variant: standard, flags: (none), unix: 1752684000).
# Holds secrets and non-default BAE_* overrides. Sourced by the launcher.
ANTHROPIC_API_KEY=sk-ant-...
BAE_LOG=debugdocker-compose.yml (default mode, 0644) — one service (baesrv for
standard, bae-max for max), publishing 8080 (and the chosen MAX port
too, for max); 8081 (the admin port) is never published:
# Generated by `baectl setup` (variant: standard, flags: (none), unix: 1752684000).
# Re-run `baectl setup` in this directory to launch or edit it.
services:
baesrv:
image: ghcr.io/prettysmartdev/better-agent-engine:latest
env_file: .env
environment:
BAE_CONFIG: /etc/bae/config.toml
volumes:
- bae-data:/var/lib/bae
- ./bae-config.toml:/etc/bae/config.toml:ro
ports:
- "${BAE_ADDR_PORT:-8080}:8080"
restart: unless-stopped
volumes:
bae-data:bae-setup.sh (--apple mode, 0755) — functionally equivalent, driving
Apple's container CLI directly (no compose equivalent exists for it);
container named bae (bae-max for max), stopped/removed first so a
re-run is idempotent; same "never publish 8081" rule:
#!/usr/bin/env bash
set -euo pipefail
# Generated by `baectl setup` (variant: standard, flags: --apple, unix: 1752684000).
# Re-run `baectl setup` in this directory to launch or edit it.
cd "$(dirname "$0")"
# Read only BAE_ADDR_PORT from .env, without evaluating the file.
BAE_ADDR_PORT="$(sed -n 's/^BAE_ADDR_PORT=//p' .env 2>/dev/null | tail -n1)"
BAE_ADDR_PORT="${BAE_ADDR_PORT:-8080}"
container volume inspect bae-data >/dev/null 2>&1 || container volume create bae-data
container stop bae >/dev/null 2>&1 || true
container rm bae >/dev/null 2>&1 || true
container run -d --name bae \
--publish "${BAE_ADDR_PORT}:8080" \
--volume bae-data:/var/lib/bae \
--volume "$(pwd)/bae-config.toml:/etc/bae/config.toml:ro" \
--env-file .env \
--env BAE_CONFIG=/etc/bae/config.toml \
ghcr.io/prettysmartdev/better-agent-engine:latestThe script never sources .env — its values (provider/MCP secrets, arbitrary
overrides) could contain shell metacharacters that sourcing would evaluate. It
reads only the one host-port override it needs (BAE_ADDR_PORT) literally, and
hands every variable to the container through --env-file .env (which the
container CLI parses itself, never evaluating it as shell). If you chose a
non-default BAE_ADDR, the publish/health-check port above is that address's
port rather than 8080.
None of these four files are excluded by .gitignore except .env
(matched by the existing .env/.env.* entries — it holds live secrets).
docker-compose.yml, bae-setup.sh, and bae-config.toml are ordinary,
trackable files: a team that wants to commit its generated deployment
alongside the repo (or a subdirectory of it) can do so; setup does not
force that choice either way.
If you answer Launch now? with yes (or accept the saved config's default Launch choice on a re-run):
-
Warns once about any declined/unresolved secrets.
-
Checks that the required engine binary (
dockerfor the default mode,containerfor--apple) is onPATH— a clean "docker/containernot found on PATH" runtime error (exit1) if not, rather than a raw shell "command not found." -
Runs
docker compose up -d(or executes./bae-setup.sh), streaming its output. -
Polls
GET /healthz(up to ~30 tries, 2s timeout + 0.5s backoff) before proceeding, on the port the server actually listens on —8080by default, or the port half of a non-defaultBAE_ADDR. The launcher publishes andsetuppolls that same port, so choosing e.g.BAE_ADDR=0.0.0.0:9090yields a coherent9090:9090mapping rather than an unlaunchable one. -
On a fresh setup only (not the verbatim re-launch path), creates a profile named
default(primary_provider= the first provider you added) and a client key nameddefault— by runningbaectl create profile/baectl create keyinside the container (docker compose exec/container exec), since the admin port is loopback-only inside the container and is never published to the host. The plaintext key is printed exactly once, with the samebaectl: copy the key now — it cannot be retrieved againstderr warningcreate keygives directly, followed by a ready-to-copyBAE_URL/BAE_API_KEYexport example.The Launch-only re-run path (existing, unedited config) does not repeat this step — it assumes the profile/key from the original run still exist. If they were deleted, re-run
baectl setupand choose Edit (even with no answer changes) to recreate them, or runbaectl create profile/create keyby hand inside the container. -
If
maxand the MAX password was left blank, prints the retrieval command for MAX's self-generated password file.
If you decline to launch, setup prints the exact manual command
(docker compose up -d or ./bae-setup.sh) — re-running baectl setup in
the same directory offers the launch step again without redoing the Q&A.
--dev and no local image built yet. setup does not check whether the
--dev image tag actually exists locally (e.g. via docker image inspect)
at file-generation time — only the eventual docker compose up -d/
container run step would fail, surfacing the engine's own "no such image"
error verbatim. Run make image/make image-max first if you pass --dev.
| Exit | When |
|---|---|
0 |
Wizard completed (files written, launched or not); or the user declined an overwrite/fresh-setup confirmation (no files changed). |
1 |
--dir doesn't exist, isn't a directory, or isn't writable (checked before any prompt); an existing bae-config.toml/.env/launcher fails to parse on the Edit/summary path; the engine binary is missing from PATH; the engine exits non-zero while launching; the server never becomes healthy within the timeout; the in-container baectl create profile/create key call fails. |
2 |
An unknown flag or invalid flag value (clap-level usage error, e.g. a non-existent flag). |
Errors: every failure prints baectl: <message> to stderr, matching
every other baectl command's convention.
--fallback <NAME> (on create profile / update profile) takes a plain
[providers] registry name — the same kind of bare name primary_provider
and --mcp-server already take. Repeat it for multiple fallbacks; they are
tried in order after the primary fails. There is no compound spec, no
provider:model syntax, and no client-side validation against the live
registry (see create profile).
Example:
baectl create profile main anthropic-sonnet \
--fallback anthropic-haiku --fallback openai-gptPer aspec/uxui/cli.md's convention (shared with baesrv):
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
Runtime error — connection failure, or any admin API error response (all RFC 7807 bodies), or an unexpected/unparseable response body. |
2 |
Usage error — a missing required positional or unknown flag (clap reports these itself). |
All errors print baectl: <message> to stderr; stdout carries only
command results, so it stays scriptable.
Every non-2xx admin API response is an RFC 7807 problem document (see
Admin API → Errors). baectl matches on the type
slug and maps it to a clean, actionable message (always exit 1):
type |
baectl message |
|---|---|
unauthorized |
The three-option auth guidance block (see below). |
profile_in_use |
The API's detail, plus: run baectl list keys to find the profile's active keys, then baectl delete key <id> for each, then retry. |
profile_unavailable |
The API's detail, plus (the referenced profile does not exist or was deleted). |
bad_request, not_found, duplicate_name, any other/unknown slug |
The API's detail verbatim — already specific about the offending field/id/name. |
No token resolved, and the server enforces admin auth — baectl prints:
baectl: admin API rejected the request: no valid admin token was supplied (401 unauthorized).
Provide an admin token in one of these ways (highest precedence first):
1. --admin-token <token> (or the BAE_ADMIN_TOKEN env var)
2. --admin-key-file <path> (or the BAE_ADMIN_KEY_FILE env var)
3. the default key file at /var/lib/bae/admin-key.pem, which baesrv writes on
first boot — reachable automatically when baectl runs inside the same
container as baesrv (e.g. `docker exec bae baectl …`).
Server unreachable (wrong --admin-addr, server not running, admin port
not yet bound):
baectl: could not connect to admin API at 127.0.0.1:8081 — is baesrv running and is --admin-addr correct?
Version skew (a 2xx response body that doesn't parse as expected JSON —
baectl and baesrv built from different versions):
baectl: unexpected response from admin API — check that baectl and the server are the same version
- Admin API reference — the underlying REST surface
baectlwraps. - Admin authentication guide — how the bootstrap key is created, rotated, disabled, and pre-provisioned.
- Configuration reference — every
BAE_*env var, including the onesbaectlreads. aspec/uxui/cli.md— CLI design conventions shared bybaesrvandbaectl.