Adds DEMO_BOT_PERSONA env var (inline JSON or file:<path>) so self-hosted instances can replace Bruno-in-Berlin with their own demo account identity and content pools. No-op for trails.cool — the built-in Bruno persona remains the default. - DemoPersona type + Zod schema validation - loadPersona() cached at boot; falls back to default on any failure - ensureDemoUser throws DemoPersonaUsernameClashError when the persona username is already a real user; server declines to schedule demo jobs for that process - isDemoUser flag moved from hardcoded "bruno" check to loader-supplied boolean computed from the running persona - docs/demo-persona.md explains the schema + operator flow Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.4 KiB
Demo persona
The Journal's demo bot (see the demo-activity-bot spec) ships with a default Bruno-in-Berlin persona. Self-hosted instances can override the identity and voice without touching code by supplying DEMO_BOT_PERSONA to the journal container.
Region is separate (DEMO_BOT_REGION) — an operator may mix-and-match a non-Berlin region with the default Bruno persona, or vice versa, though it usually reads better to keep persona and region stylistically consistent.
Supplying a persona
Two forms are accepted:
- Inline JSON —
DEMO_BOT_PERSONA='{"username":"hamish",...}'. Workable for short personas; gets ugly fast once the pools are realistic. - File path —
DEMO_BOT_PERSONA=file:/etc/trails-cool/persona.json. The journal container reads the file once at boot; mount the file via a volume or Docker secret.
A missing / invalid / unreadable persona falls back to the built-in default with a single warn log line. The bot never crashes over a bad persona.
Schema
{
"username": "<kebab-case, 2–32 chars, matches /^[a-z0-9][a-z0-9_-]{1,30}$/>",
"displayName": "<string, 1–200 chars>",
"bio": "<string, 0–200 chars>",
"locales": ["en"] | ["de"] | ["en","de"],
"content": {
"names": {
"en": ["<3 to 50 non-empty strings>"],
"de": ["<3 to 50 non-empty strings>"]
},
"descriptions": {
"en": ["<3 to 50 non-empty strings>"],
"de": ["<3 to 50 non-empty strings>"]
}
}
}
Every locale listed in locales MUST have both a names pool and a descriptions pool. Locales omitted from locales may also omit their pools.
Sensible pool size is 10–15 entries per pool. Fewer than 3 is rejected; more than 50 is rejected. The generator samples deterministically from the walk's start-time, so very small pools produce visibly-repeating copy in the daily feed.
Example: Hamish the English-only Scottish collie
{
"username": "hamish",
"displayName": "Hamish",
"bio": "Border collie. Arthur's Seat regular.",
"locales": ["en"],
"content": {
"names": {
"en": [
"Arthur's Seat summit patrol",
"Holyrood Park morning rounds",
"Royal Mile crumb inspection",
"Dean Village bridge audit",
"Inverleith perimeter check",
"Blackford Hill squirrel count",
"Water of Leith investigation",
"Hamish vs. the seagulls",
"Portobello beach reconnaissance",
"Cramond causeway survey"
]
},
"descriptions": {
"en": [
"Sniffed an impressive number of bins. All catalogued.",
"Weather: dreich. Ears: flat.",
"One dropped scone located. Consumed as evidence.",
"Diplomacy established with the resident Labrador.",
"Route completed. Biscuits expected.",
"Found three sticks. Returned with one."
]
}
}
}
Rollout
- Write
persona.json, mount it via your compose override or Docker secret. - Set
DEMO_BOT_PERSONA=file:/path/to/persona.jsonin your SOPS env. - Set
DEMO_BOT_ENABLED=true. - Restart the journal container.
On boot, the worker logs demo-bot user ensured with the persona's user id. If that username is already in use by a real human user, the worker logs a demo persona username clash error and declines to schedule the demo jobs — pick a different username and restart.
Rolling back is the same in reverse: unset DEMO_BOT_PERSONA, restart. The built-in default returns.