arizuko › components › onbod
onbod is the onboarding daemon. It admits new users (invite token, OAuth bind, queued admission) and stands up the group folder for each one via container.SetupGroup — the single canonical path for creating a group.
It owns three tables: invites, onboarding, and auth_users. gated runs the schema migrations; onbod is the only daemon that writes those rows.
Two problems would have no good answer without onbod. First, a new user reaching the bot has nothing — no record in auth_users, no folder, no skills, no settings, no default tasks. Something has to put them through identity confirmation, decide whether to admit them, and bring them online.
Second, group creation is not mkdir. container.SetupGroup creates <groups>/<folder>/ and logs/, copies the prototype if one is named, seeds .claude/skills/ with the standard skill set, writes default settings, and chowns everything to the container UID. Skipping any step yields a folder the agent cannot use.
The rule is firm: operators never mkdir a group by hand. Stand it up through onbod (invite redemption or the /v1/users path) so it gets the full set of files.
new JID reaches a channel adapter
|
v inbound message
|
gated no auth_users row → writes awaiting_message
|
onbod poll loop sends auth link via gated outbound API
| /onboard landing → OAuth (GitHub/Google/Discord)
| gate match → onboarding queue (per-gate daily limit)
| admitFromQueue (~60s) → approved
| container.SetupGroup(folder, prototype)
v auth.Mint(sub, scope=[users:read self], iss=onbod)
user gets redemption token, dashd verifies it via auth.VerifyHTTP
Gate matching uses ONBOARDING_GATES: a list of github-org, google-domain, or catch-all rules with optional daily caps. A user who matches no gate stays queued. Admitted users with a second JID on another channel auto-link to their existing group rather than getting a fresh one.
onbod is also a JWT issuer. At invite redemption it calls auth.Mint with the same HS256 key every other daemon verifies against, issuing a short-TTL token scoped to users:read on the user’s own sub. The token bootstraps the user into dashd before they get a full session from proxyd’s OAuth flow.
onbod runs as a daemon against the shared SQLite store and the gated outbound API. It will not run without ROUTER_URL pointing at a reachable gated — the poll loop sends auth links through gated, not directly to platforms.
export DATA_DIR=/srv/data/arizuko_demo
export ONBOD_LISTEN_ADDR=:8080
export ROUTER_URL=http://gated:8080
export AUTH_SECRET=$(openssl rand -hex 32)
export CHANNEL_SECRET=$(openssl rand -hex 32)
export AUTH_BASE_URL=https://example.com
export ONBOARDING_ENABLED=1
export ONBOARDING_GATES='github-org:myorg:5,catch-all:0'
export GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=...
./onbod
Set ONBOARDING_ENABLED=0 and onbod exits immediately. Health: GET /health returns 200 when the database is reachable; queued users see their position on /onboard, which refreshes every 30 seconds.
Every group folder in an arizuko instance was created by one function: container.SetupGroup(cfg, folder, prototype). It runs the same five steps in the same order:
cfg.GroupsDir; reject anything that escapes.mkdir -p the group folder and its logs/ subdirectory..claude/skills/ with the standard skill set, write default settings.A folder built any other way is missing one of these steps. The agent then fails on first turn — no skills loaded, or no write permission on logs, or a missing settings file. There is no fallback path that fills the gaps later; the cost of skipping SetupGroup is recreating the folder from scratch.
onbod does not send messages, run the agent, or schedule tasks. Its scope is narrow: turn an unknown JID into a row in auth_users, a folder on disk, and a token the user can present to other daemons.
specs/5/1-auth-standalone.md — auth and admission model.specs/5/5-uniform-mcp-rest.md — /v1/invites and /v1/users surface.specs/4/26-prototypes.md — the prototype mechanic SetupGroup copies from.