# Slice 1 identities: Gitea bots, Vikunja bots and their tokens Jason runs this guide once per business, by hand (PRD REQ-CRED-1, round 3, 1A and 2A). It creates every service identity slice 1 uses and writes each token to a 0600 file the broker reads. Nothing in v1 mints these tokens for you. Brief: `docs/plans/2026-10-04_slice-1.md`, row SR. Plan on about 20 minutes with an existing Vikunja, and 30 if you start the bundled one. ## Rules for the whole guide - A token never appears on a command line, in a URL, in shell history, in a chat or in the repository. The commands below read secrets from files or a password prompt, and write tokens straight to files. - Run every step in one shell with `umask 077` set, so every file you create is 0600 and every directory 0700. - To check a token file, use `stat`, never `cat`. - The Vikunja owner password and the Gitea admin login are the high-value secrets. They are used only in this guide, and never reach the broker or an agent. ## 0. Set up the shell ```sh umask 077 BIZ=mosaic-stack S="$HOME/.config/mosaic-dev/secrets/$BIZ" # token directory, outside the repo mkdir -p "$S" && chmod 700 "$S" VK=https://vikunja.example # your Vikunja base URL, no trailing slash GITEA=https://git.example # your Gitea base URL EXP=$(date -u -d '+90 days' +%Y-%m-%dT00:00:00Z) # Vikunja token expiry ``` Write `EXP` down. It goes in the business file as each Vikunja token's `expires`. ## 1. Gitea: four bot users and their tokens Gitea tokens don't expire, and the HTTP route for creating one needs a password, so this part uses the web UI. 1. As a site admin, create the users `pm-bot`, `cto-bot`, `coder-bot` and `reviewer-bot` (Site Administration, User Accounts, Create). Give each a long random password, which you won't need again after step 3. None of them is an admin. 2. In `mosaicstack/stack`, add each bot as a collaborator: | Bot | Repository access | |---|---| | pm-bot | Write (labels, assignees and closing need it) | | cto-bot | Write | | coder-bot | Write | | reviewer-bot | Read | No bot is on a protected branch's push or merge allowlist. Pushing to a protected branch is a gated action, and it stays Jason's. 3. Log in as each bot, go to Settings, Applications, and generate one token named `mosaic-stack--` with these scopes: | Bot | Scopes | |---|---| | pm-bot | `write:issue`, `read:repository`, `read:user` | | cto-bot | `write:issue`, `write:repository`, `read:user` | | coder-bot | `write:issue`, `write:repository`, `read:user` | | reviewer-bot | `write:issue`, `write:repository`, `read:user` | Leave every other scope unset, and set none to "public only". Reviewer-bot's `write:repository` is there because Gitea files pull request reviews under the repository scope. Its Read collaborator access is what stops it pushing. Row SR's review checks that before anyone relies on it. 4. Copy each token into its file with an editor, never `echo`: `$S/pm-gitea.token`, `$S/cto-gitea.token`, `$S/coder-gitea.token`, `$S/reviewer-gitea.token`. One line, the token only. 5. Log out of each bot account. Record today's date as each Gitea token's `rotateBy` base. Section 5 has the rotation schedule. ## 2. Vikunja: the instance and the owner account You need Vikunja 2.7.0 or later, with local login on and an owner account that is yours. ### Path A, an existing instance Check that `curl -s "$VK/api/v1/info"` reports `v2.7.0` or later. Use your existing account as the owner. ### Path B, the bundled instance Row S3 ships `packages/tasks/deploy/vikunja/` for this. Until it lands, these commands start the same pinned image the probes used, bound to 127.0.0.1: ```sh VD="$HOME/.local/share/mosaic-dev/vikunja"; mkdir -p "$VD/db" "$VD/files" printf 'VIKUNJA_SERVICE_SECRET=%s\n' "$(openssl rand -hex 32)" > "$VD/env" cat >> "$VD/env" < ``` ### The five bots ```sh api() { curl -sf -H @"$S/vikunja-owner.hdr" -H 'Content-Type: application/json' "$@"; } for r in pm cto coder reviewer sync; do api -X POST "$VK/api/v2/user/bots" \ -d "{\"username\":\"bot-$BIZ-$r\",\"name\":\"$BIZ $r\"}" | jq -c '{id, username}' done ``` Note the five ids. Bot usernames must start with `bot-`, and this guide uses `bot--` so two businesses on one instance don't collide. ### Shares The role bots get write (1). The sync bot gets read (0), so a bug in the poll path can't write. ```sh for r in pm cto coder reviewer; do api -X POST "$VK/api/v2/projects/$P/users" -d "{\"username\":\"bot-$BIZ-$r\",\"permission\":1}" | jq -c '{username, permission}' done api -X POST "$VK/api/v2/projects/$P/users" -d "{\"username\":\"bot-$BIZ-sync\",\"permission\":0}" | jq -c '{username, permission}' ``` ### Scopes From addendum B section 2 (lead decision 52). Every group or verb not listed stays off. ```sh cat > "$S/scopes-sync.json" <<'EOF' {"projects":["read_one","views_buckets","views_buckets_tasks_get"],"projects_views":["read_all"],"tasks":["read_all","read_one"],"tasks_comments":["read_all"]} EOF cat > "$S/scopes-pm.json" <<'EOF' {"tasks":["read_one","create","update"],"tasks_assignees":["create","delete"],"tasks_relations":["create","delete"],"tasks_labels":["create","delete"],"tasks_comments":["create"],"labels":["read_all"],"projects":["views_buckets_tasks"]} EOF cat > "$S/scopes-worker.json" <<'EOF' {"tasks":["read_one","update"],"tasks_comments":["create"],"projects":["views_buckets_tasks"]} EOF ``` cto, coder and reviewer all use `scopes-worker.json`. ### Tokens `mint` sends the request, writes the token straight to its file, and prints only the metadata. Vikunja shows a token once. ```sh mint() { # mint ROLE BOT_ID SCOPES_FILE local r=$1 id=$2 sc=$3 resp="$S/.mint-$1.json" jq -n --arg t "$BIZ-$r-$(date +%F)" --argjson o "$id" --arg e "$EXP" --slurpfile p "$sc" \ '{title:$t, owner_id:$o, expires_at:$e, permissions:$p[0]}' | api -X POST "$VK/api/v2/tokens" --data-binary @- -o "$resp" && jq -j .token "$resp" > "$S/$r-vikunja.token" && jq -c '{id, owner_id, expires_at, starts_tk: (.token | startswith("tk_"))}' "$resp" rm -f "$resp" } mint pm "$S/scopes-pm.json" mint cto "$S/scopes-worker.json" mint coder "$S/scopes-worker.json" mint reviewer "$S/scopes-worker.json" mint sync "$S/scopes-sync.json" ``` Check that each line shows `starts_tk: true` and the `expires_at` you set. Vikunja accepts a past expiry without complaint, so read it. A 400 with code 14002 means a scope name is wrong. Fix the scope file and mint again. ## 4. Check and clean up ```sh rm -f "$S/vikunja-owner.hdr" stat -c '%a %s %n' "$S"/*.token ``` Each of the nine token files must show `600` and a nonzero size. Once row S3 lands, `mosaic` runs the broker's startup probe for every identity. It makes one control call and one call that must be refused, on a task id that doesn't exist, and it refuses to start on any surprise. That probe, passing for all nine, closes row SR's gate. The business file (`~/.config/mosaic-dev/businesses/mosaic-stack.json`, row S1) references each file by absolute path, and never holds a value. One role's entry looks like this. Row S1's validator has the final shape. ```json "coder": { "definition": "coder", "tracker": { "bot": "bot-mosaic-stack-coder", "botId": 0 }, "credentials": { "gitea": { "file": "/home/you/.config/mosaic-dev/secrets/mosaic-stack/coder-gitea.token", "rotateBy": "YYYY-MM-DD" }, "vikunja": { "file": "/home/you/.config/mosaic-dev/secrets/mosaic-stack/coder-vikunja.token", "expires": "YYYY-MM-DD" } } } ``` The sync bot has its own entry under the tracker settings, not under a role. The stack never writes this file. ## 5. Rotation - **Vikunja**, before `expires`: log in as the owner (section 2), mint a new token for the same bot with `mint`, update `expires` in the business file, and restart the broker. Then revoke the old token: `api -X DELETE "$VK/api/v2/tokens/"`. The broker refuses to start with a token past its `expires`, and it treats any 401 as a refusal, never a retry. - **Gitea**, every 90 days or at once if a token may have leaked: log in as the bot, generate the replacement, write it to the file, restart the broker, then delete the old token in the same settings page. Update `rotateBy`. Each rotation gets one line in `docs/SESSIONS.md`: date, who, which identities, and no values. - **Revoking a role at once:** delete its token, the Gitea one in the bot's settings and the Vikunja one with the `DELETE` above, or remove the bot's collaborator access or project share. The broker's next call gets a 401 or 403 and refuses.