You are an AI agent installing Jentic One for a human. This runbook shows how
to install Jentic (App, Broker, Database). Two steps are human gates — you must stop,
hand the human a URL, and wait; they are marked HUMAN GATE. (A hardened
install adds a third gate at Step 3.)
Related files: operate.md (start/stop/upgrade/uninstall), troubleshoot.md (when a step fails), harden.md (read before this install touches a real credential), use.md (what to do once installed).
- Never regenerate secrets over an existing install. If
~/.jentic/jentic-one.yamlalready exists, this is a reinstall: keep that file (especially thecredentials.encryptionblock) and~/.jentic/.env, and skip to Step 6 — after confirming the pieces the skipped steps provide still exist: thejenticbinary onPATH(Step 2),~/.jentic/docker-compose.yaml(Step 4), and aVER=line in~/.jentic/.env(also set in Step 2). Recreate any missing one from its step without touching the two secret-bearing files. A rotated encryption key silently makes every stored credential unreadable. - Do not print secret values into chat, logs, or shell history beyond the generated files themselves. Generate secrets with command substitution, not by echoing them. If the human chose the hardened install, never read the secret-bearing files at all.
- Do not invent flags, endpoints, or fallback paths. If a command from
this runbook fails, check troubleshoot.md; if the
symptom is not there, stop and report to the human — never improvise a
recovery. For any
jenticcommand not written here, readjentic <command> --helpfirst. - Shell variables do not survive between steps. Each command block may
run in a fresh shell session. Anything a later step needs
(
VER,PGPASS) is persisted to~/.jentic/.envwhen first set —docker composereads that file automatically because it sits next to the compose file. Never rely on a variable exported in an earlier block. - Ask the human the four questions in Step 0 before writing anything, unless they already told you.
- Run each command block as one script (
bash -e <<'EOF' … EOFor a saved file), not pasted line-by-line into your interactive shell. The blocks depend on the first failing command stopping the block with a branchable exit code (a stopped Docker daemon otherwise leaves no trace to branch on), and the diagnostic blocks end inexit 1— fine for a script, session-ending if pasted into a persistent shell.
| Question | Default | Notes |
|---|---|---|
| Database: SQLite or Postgres? | SQLite | SQLite is fine for a single-host install. For an external/production Postgres follow docker.md instead. |
| Reachable from other machines, or this machine only? | This machine only (127.0.0.1) |
Anything else → read harden.md first; a LAN bind publishes the app, broker, and UI to the network. |
| Enable anonymous usage telemetry? | Off | If yes, the config gets enabled: true plus a random instance_id (UUID) and host_os; if no, an explicit enabled: false records the decision. |
| Is it acceptable that I (the installing agent) could read the instance secrets? | Ask — do not assume | The generated secrets land in files my shell writes and my OS user can read. Fine for trying things out with throwaway keys. If the answer is no (real credentials will be stored), follow the hardened install variant of Step 3. |
The rest of this runbook assumes the defaults; the Postgres and hardened variants are given inline where they differ.
If the human said no to the last question, the goal is that no secret value ever enters your shell, your context, or your transcript. Be honest with the human about the limit first:
As long as I run as the same OS user that owns
~/.jentic, I could read the secret files afterwards — file permissions can't stop that. This variant keeps secrets out of everything I generate, see, and log, and we can verify that from the transcript. For a hard guarantee, a human (or a separate OS user) runs the secret steps — see harden.md.
Then apply these changes, marked HARDENED at the affected steps:
- Step 3 becomes a human gate: you write the config with placeholder
markers; the human runs a one-liner that fills in the secrets (and, on
Postgres,
PGPASS) — the values only ever exist in their shell. - Never read the secret-bearing files back (
jentic-one.yaml,~/.jentic/.env). To inspect the config, print its shape only —grep -oE '^\s*[A-Za-z_]+:' ~/.jentic/jentic-one.yamlemits keys and never values. (Do not filter values out with agrep -vdenylist: folded YAML scalars put secret material on lines the denylist doesn't match, so it fails open.)
Every other step is unchanged — none of them touch secret values.
Check all of these before writing any file. Stop and report to the human if one fails.
docker info >/dev/null # daemon is running (not just installed)
docker compose version # compose v2 is available
curl --version >/dev/null && openssl version >/dev/null
# Ports 8000 (app) and 8100 (broker) must be free. lsof must exist or this
# gate cannot decide — chain it so a missing lsof fails closed instead of
# false-passing on lsof's own error exit:
command -v lsof >/dev/null \
&& ! lsof -iTCP:8000 -sTCP:LISTEN -n -P && ! lsof -iTCP:8100 -sTCP:LISTEN -n -PCreate the state directory. ~/.jentic must be owner-only (0700) — the
files inside are container-readable, so this parent directory is the
host-side protection:
mkdir -p ~/.jentic/logs
chmod 700 ~/.jentic
chmod 777 ~/.jentic/logs # containers run as a non-root user and must write here; safe only under the 0700 parent(The image runs as a non-root user; its numeric uid is an image-build detail,
not a contract. If you ever need the actual value — e.g. for a chown — read
it from the image itself: docker run --rm --entrypoint id ghcr.io/jentic/jentic-one-app:<version>.)
The agent-facing CLI is a single static binary from GitHub Releases (full matrix and cosign verification: installation/cli.md). The snippet resolves the newest release that actually has an asset for this platform. Note it performs no signature verification — when the humans require a verified supply chain, follow cli.md's cosign steps instead:
OS=$(uname -s | tr '[:upper:]' '[:lower:]'); ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
URL=$(curl -fsSL 'https://api.github.com/repos/jentic/jentic-one/releases?per_page=10' \
| grep -Eo '"browser_download_url": *"[^"]*/jentic_[0-9][^"]*_'"${OS}_${ARCH}"'\.tar\.gz"' \
| sed 's/.*"\(https[^"]*\)".*/\1/' | head -n 1)
[ -n "$URL" ] || { echo "ERROR: no recent release has a jentic binary for ${OS}/${ARCH}" >&2; exit 1; }
VER=$(basename "$URL" | sed 's/^jentic_\(.*\)_'"${OS}_${ARCH}"'\.tar\.gz$/\1/')
echo "Installing jentic v${VER} from ${URL}"
curl -fsSL -o /tmp/jentic.tar.gz "$URL"
tar -xzf /tmp/jentic.tar.gz -C /tmp jentic
sudo install /tmp/jentic /usr/local/bin/ && rm -f /tmp/jentic /tmp/jentic.tar.gz
jentic --version
# Persist VER for the rest of the install — later steps run in other shells:
echo "VER=${VER}" > ~/.jentic/.env && chmod 600 ~/.jentic/.envThe sudo install line is a possible human gate: if sudo needs a
password, the block stalls on an interactive prompt — ask the human to run it
(or pre-authorise sudo) rather than waiting silently. Alternatively, avoid
sudo entirely: re-run the block above with the sudo install line replaced
by the user-local install below, and make sure ~/.local/bin is on PATH:
mkdir -p ~/.local/bin
install -m 0755 /tmp/jentic ~/.local/bin/jentic && rm -f /tmp/jentic /tmp/jentic.tar.gzIf it fails: troubleshoot.md — do not improvise a download.
Reinstall guard: if this file already exists, do not touch it. Skip to Step 4 and reuse it as-is.
Four independent 32-byte secrets are generated fresh; the human never chooses them and you never display them:
cat > ~/.jentic/jentic-one.yaml <<EOF
# Generated by an agent following docs/agent/install.md.
# Secrets are machine-generated. On a reinstall REUSE this file — rotating
# credentials.encryption makes existing stored credentials unreadable.
databases:
registry: {backend: sqlite, path: /data/registry.db, schema_name: registry}
control: {backend: sqlite, path: /data/control.db, schema_name: control}
admin: {backend: sqlite, path: /data/admin.db, schema_name: admin}
runtime:
debug: false
log_level: INFO
logging:
file_enabled: true
file_dir: /logs
file_name: app.jsonl
server:
host: 0.0.0.0 # in-container bind; host exposure is decided by the compose port prefix
port: 8000
reload: false
apps: [registry, admin, control, auth]
auth:
canonical_base_url: http://127.0.0.1:8000 # must EXACTLY match the URL agents register with
admin:
auth:
jwt_secret: "$(openssl rand -base64 32)"
invite:
pepper: "$(openssl rand -base64 32)"
credentials:
encryption:
active_id: v1
entries:
- id: v1
material: "$(openssl rand -base64 32)"
providers:
direct_oauth2:
kind: direct_oauth2
redirect_uri: http://127.0.0.1:8000/credentials/oauth/callback
connect:
state_secret: "$(openssl rand -base64 32)"
observability:
metrics: {exporter: none}
tracing: {exporter: none}
search:
enabled: true
search_enabled: true
search_mode: lexical
telemetry:
enabled: false
EOF
chmod 644 ~/.jentic/jentic-one.yaml # the app container's non-root user must read it; ~/.jentic (0700) protects it host-sideAdjustments from Step 0:
-
Telemetry on: replace the
telemetryblock withenabled: true,instance_id: "$(uuidgen | tr 'A-Z' 'a-z')", andhost_os: <linux|darwin|windows>(the host's OS — the container would misreport Linux). -
Postgres: generate and persist the password first:
PGPASS=$(openssl rand -hex 24) # hex only — it crosses YAML and env boundaries unquoted echo "PGPASS=${PGPASS}" >> ~/.jentic/.env
then replace each database entry with
{host: db, port: 5432, name: jentic, user: postgres, password: "$PGPASS", schema_name: <registry|control|admin>}, writing the config in the same shell session so$PGPASSexpands (in a new session, load it first with. ~/.jentic/.env). -
Non-loopback install:
auth.canonical_base_urland theredirect_urimust carry the real public URL — see harden.md. -
HARDENED — human gate: write the same file, but with the literal placeholder
__GENERATE__in place of each of the four$(openssl rand -base64 32)substitutions (use a quoted heredoc,<<'EOF', so nothing expands). Because nothing expands, non-secret substitutions must be pre-expanded before writing the file: with telemetry on, runuuidgen | tr 'A-Z' 'a-z'first and insert the resulting value as the literalinstance_id(it is not a secret — only the four__GENERATE__markers are left for the human). Then hand the human this and wait:Run this once — it replaces each placeholder with a fresh secret that never leaves your shell:
perl -i -pe 's/__GENERATE__/chomp($s=`openssl rand -base64 32`);$s/ge' ~/.jentic/jentic-one.yaml
Confirm completion without reading the file's values (exits non-zero while placeholders remain):
if grep -q __GENERATE__ ~/.jentic/jentic-one.yaml; then echo "NOT DONE — placeholders remain" >&2; exit 1; fi
From here on, never read
~/.jentic/jentic-one.yamlor~/.jentic/.envback into your context. -
HARDENED + Postgres: also write
password: "__PGPASS__"in the three database entries (instead of expanding$PGPASS), skip the agent-sidePGPASSgeneration above, and fold the password into the same human gate:Also run this — it generates the database password, pins it for compose, and fills it into the config:
PGPASS=$(openssl rand -hex 24) echo "PGPASS=${PGPASS}" >> ~/.jentic/.env PGPASS="$PGPASS" perl -i -pe 's/__PGPASS__/$ENV{PGPASS}/g' ~/.jentic/jentic-one.yaml
Confirm with
if grep -q __PGPASS__ ~/.jentic/jentic-one.yaml; then echo "NOT DONE" >&2; exit 1; fi. Steps 4–10 need no secret values, so nothing else changes.
Every other key and its default: configuration reference.
The broker always runs as its own service (JENTIC__APPS=broker) on its own
port. The project name is pinned to jentic so volume names are
deterministic; the 127.0.0.1 port prefix is what keeps a loopback install
off the network. The heredoc delimiter is quoted (<<'EOF'): write the file
exactly as shown, escaping nothing — compose resolves ${VER} (and
${PGPASS}, $HOME) from ~/.jentic/.env and the environment on every
invocation.
cat > ~/.jentic/docker-compose.yaml <<'EOF'
name: jentic
services:
app:
image: ghcr.io/jentic/jentic-one-app:${VER}
environment:
JENTIC_CONFIG_FILE: /etc/jentic/jentic-one.yaml
JENTIC__APPS: registry,admin,control,auth
ports:
- "127.0.0.1:8000:8000"
volumes:
- $HOME/.jentic/jentic-one.yaml:/etc/jentic/jentic-one.yaml:ro
- $HOME/.jentic/logs:/logs
- jentic-data:/data
broker:
image: ghcr.io/jentic/jentic-one-app:${VER}
environment:
JENTIC_CONFIG_FILE: /etc/jentic/jentic-one.yaml
JENTIC__APPS: broker
JENTIC__SERVER__PORT: "8100"
ports:
- "127.0.0.1:8100:8100"
volumes:
- $HOME/.jentic/jentic-one.yaml:/etc/jentic/jentic-one.yaml:ro
- $HOME/.jentic/logs:/logs
- jentic-data:/data
volumes:
jentic-data:
EOF
chmod 600 ~/.jentic/docker-compose.yamlNotes:
- SQLite must live on the named volume, never a host bind mount (why).
- Postgres variant: write this complete file instead (do not hand-merge
fragments). It adds a
dbservice, makes app/broker wait on its healthcheck, and drops thejentic-datavolume (data lives in Postgres):
cat > ~/.jentic/docker-compose.yaml <<'EOF'
name: jentic
services:
app:
image: ghcr.io/jentic/jentic-one-app:${VER}
environment:
JENTIC_CONFIG_FILE: /etc/jentic/jentic-one.yaml
JENTIC__APPS: registry,admin,control,auth
ports:
- "127.0.0.1:8000:8000"
volumes:
- $HOME/.jentic/jentic-one.yaml:/etc/jentic/jentic-one.yaml:ro
- $HOME/.jentic/logs:/logs
depends_on:
db:
condition: service_healthy
broker:
image: ghcr.io/jentic/jentic-one-app:${VER}
environment:
JENTIC_CONFIG_FILE: /etc/jentic/jentic-one.yaml
JENTIC__APPS: broker
JENTIC__SERVER__PORT: "8100"
ports:
- "127.0.0.1:8100:8100"
volumes:
- $HOME/.jentic/jentic-one.yaml:/etc/jentic/jentic-one.yaml:ro
- $HOME/.jentic/logs:/logs
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_DB: jentic
POSTGRES_USER: postgres
POSTGRES_PASSWORD: "${PGPASS}"
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d jentic"]
interval: 5s
timeout: 3s
retries: 5
volumes:
db-data:
EOF
chmod 600 ~/.jentic/docker-compose.yamlPostgres rules: no init scripts on the db service (migrations create
the schemas — why);
never publish 5432 to the host; on a reinstall over an existing
jentic_db-data volume, keep the volume's original password
(POSTGRES_PASSWORD only applies at first initialisation).
Pull through compose so the tag comes from ~/.jentic/.env, not from your
shell:
docker compose -p jentic -f ~/.jentic/docker-compose.yaml pullPull denied → troubleshoot.md.
A one-shot app container applies migrations (this also creates the volume, and for Postgres waits on the db healthcheck):
docker compose -p jentic -f ~/.jentic/docker-compose.yaml \
run --rm -T app python -m jentic_one.migrations.runFailures → troubleshoot.md — the recovery differs critically between a fresh volume and a pre-existing one. Re-running migrations is also the upgrade step — see operate.md.
docker compose -p jentic -f ~/.jentic/docker-compose.yaml up -dPoll liveness — a cold start can take tens of seconds:
for i in $(seq 1 45); do
curl -fsS http://127.0.0.1:8000/health >/dev/null 2>&1 && break
sleep 2
done
curl -fsS http://127.0.0.1:8000/health # app is up
curl -fsS http://127.0.0.1:8100/health # broker is upNever healthy → troubleshoot.md.
The database ships with zero users; there is no default account or password, and the first admin's password must never pass through you. Check whether setup is still required:
curl -fsS http://127.0.0.1:8000/admin/health # → {"setup_required": true|false, ...}-
setup_required: falsetwice in a row (~2 s apart — a single read can be a warmup blip): corroborate before trusting it —/admin/healthswallows database errors and answers with the samesetup_required: falsedefaults, so also rundocker compose -p jentic -f ~/.jentic/docker-compose.yaml run --rm -T app python -m jentic_one.migrations.run --checkand requireOVERALL current. Both good: an admin already exists (reinstall over live data) — tell the human to sign in athttp://127.0.0.1:8000/app/loginand continue to Step 9. -
setup_required: true: tell the human —Open http://127.0.0.1:8000/app/setup and create the first admin account (email + password, minimum 12 characters). I'll wait.
Then poll
setup_requiredevery few seconds until it isfalse. Bound the wait: after ~15 minutes, stop polling and re-ask the human instead of looping forever.
No human available (CI, fleet installs) → troubleshoot.md.
Now connect the agent (you, or the machine you run on) to the install. Use
127.0.0.1, never localhost — the token audience must exactly match
auth.canonical_base_url (details):
jentic register --url http://127.0.0.1:8000This registers a new agent identity and then waits for a human to approve it. It prints the approval link; tell the human:
Approve the new agent at http://127.0.0.1:8000/app/agents — I'll continue automatically once you have.
The command exits successfully once approved and a token is minted. Timeout
(exit 3) → troubleshoot.md;
remote installs also need --broker-url
(why).
Then verify, and optionally write the Jentic skill into your own runtime's native layout (Claude/Cursor/Codex/…):
jentic doctor # identity, token, control-plane reachability — it does NOT probe the broker, and warnings keep exit 0, so read its output rather than branching on the code
jentic skill init # optional: install the usage skill for your runtimejentic catalog list # the API catalog answers (bare `jentic catalog` opens an interactive TUI on a terminal)
jentic access whoami # who you are and what you may callYou are done installing. Now read use.md before doing anything else — it covers how to import an API, request access, and make calls; do not guess commands from package-manager habit. The first two you will need:
jentic catalog search "<api name>" # find an importable API in the public catalog
jentic catalog import <vendor/name> # import it into this instance's registryReport to the human:
- URLs: app/UI
http://127.0.0.1:8000, brokerhttp://127.0.0.1:8100 - Files: config
~/.jentic/jentic-one.yaml, compose~/.jentic/docker-compose.yaml, version/db-password pin~/.jentic/.env, logs~/.jentic/logs/app.jsonl - Data: docker volume
jentic_jentic-data(Postgres:jentic_db-data) - Next: make a first brokered call; day-2 usage patterns in use.md; before storing a real credential, read harden.md.