From c9c1699a05cdd02b7f85bf08d8cab87124272675 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 4 Oct 2026 21:58:49 -0500 Subject: [PATCH] docs(guides): slice 1 identities runbook (row SR); lead decision 56 Co-Authored-By: Claude Opus 5.5 --- docs/SESSIONS.md | 3 + docs/guides/slice-1-identities.md | 278 ++++++++++++++++++++++++ docs/plans/2026-09-26_lead-decisions.md | 21 ++ 3 files changed, 302 insertions(+) create mode 100644 docs/guides/slice-1-identities.md diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 0bba391a..29c01eaa 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -483,3 +483,6 @@ are never rewritten or removed; corrections are new entries. 2026-10-05T02:52:27Z | Sage (T3 Claude Code, thread 1ef1e4f8) | slice 1 brief | brief 43c48d7a; epic #1515, row issues #1516-#1524; queue rows 34-42 (revs 64-72), rows 34-37 briefed (revs 73-76); lead decision 54 2026-10-04 | rocko | Slice 1 S2 (#1519), row 37 preparation | Read brief, data model/addenda A+B and v2 prototype; preparation packet pinned; implementation held for schema v3 and Sage start instruction; S4 not started. 2026-10-05T02:55:50Z | Sage (T3 Claude Code, thread 1ef1e4f8) | schema v3 | Darkwing's schema v3 (7ed83178) rerun matched on Node 26; lead decision 55 keeps the task.missing body trigger; Rocko told S2 can start +2026-10-04 | dewey | Slice 1 S5 (#1522), row 40 design | Design note agents/dewey/work/wui/SLICE1-VIEWS.md from data model, addenda A+B and schema v3; questions Q1-Q5 sent to Sage; no code, no commit; build waits for S4. +2026-10-04 | rocko | Slice 1 S2 #1519 row 37 | Started after schema v3 pin verification and Sage authorization; fixture credentials only; Darkwing review and Sage commit pending. +2026-10-05T02:58:34Z | Sage (T3 Claude Code, thread 1ef1e4f8) | slice 1 SR runbook, S5 rulings | docs/guides/slice-1-identities.md drafted for Darkwing's review (row 35); lead decision 56 accepts Dewey's Q1-Q5; Rocko started row 37 diff --git a/docs/guides/slice-1-identities.md b/docs/guides/slice-1-identities.md new file mode 100644 index 00000000..297184ae --- /dev/null +++ b/docs/guides/slice-1-identities.md @@ -0,0 +1,278 @@ +# 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. diff --git a/docs/plans/2026-09-26_lead-decisions.md b/docs/plans/2026-09-26_lead-decisions.md index b86ff23d..afcc6217 100644 --- a/docs/plans/2026-09-26_lead-decisions.md +++ b/docs/plans/2026-09-26_lead-decisions.md @@ -914,3 +914,24 @@ which stay with him. Each item names who decided it and what happened. and the new project id when the reason is `moved`. The v2 schema checks `credential.*` bodies the same way, and an event the reader can't act on is worse than a refused write. +56. **Dewey's S5 questions (2026-10-04).** Source: Dewey, design note + `agents/dewey/work/wui/SLICE1-VIEWS.md` (uncommitted, Dewey's working + file). All five recommendations are accepted. + - Q1: the inbox, tasks, agents and trail reads live in one module in + `packages/bus`, over the broker client. Both the CLI (S4) and the + WebUI server (S5) import it, so REQ-WEB-1's "same data" holds by + construction. The WebUI never opens `bus.sqlite`. Rocko owns it, + because S2 and S4 are both his rows. + - Q2: the broker gains a status read giving the last successful board + and cursor read per project, plus the last poll error. Row S3 builds + it, since the poller is S3's. + - Q3: any event about one task carries the task ref in `subject`. The + `task.create` body also names the `human.input` event that asked + for it, so a trail can begin at Jason's request. These are body + rules, so the kind list doesn't change. Darkwing decides whether a + trigger or the broker enforces each one. + - Q4: the WebUI writes nothing in slice 1, not even `seen`. The inbox + detail shows the `mosaic decide` command to copy. + - Q5: S5 also fixes `escalating` staying set after a throw, and a + takeover followed by Enter in one chunk, because both sit in files + S5 owns. The freeze-ordering test stays in DEFERRED.