arizukocomponents › onbod

onbod

What it is

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.

Why it exists

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.

How it fits

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.

Standalone usage

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.

SetupGroup, the one canonical path

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:

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.

What onbod does not do

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.

Go deeper