docs(guides): slice 1 identities runbook (row SR); lead decision 56

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-04 21:58:49 -05:00
co-authored by Claude Opus 5.5
parent 8c4460f35e
commit c9c1699a05
3 changed files with 302 additions and 0 deletions
+3
View File
@@ -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
+278
View File
@@ -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-<role>-<date>` 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" <<EOF
VIKUNJA_SERVICE_PUBLICURL=http://127.0.0.1:3456/
VIKUNJA_DATABASE_TYPE=sqlite
VIKUNJA_DATABASE_PATH=/db/vikunja.db
VIKUNJA_WEBHOOKS_ENABLED=false
VIKUNJA_SERVICE_ENABLEREGISTRATION=false
EOF
docker run -d --name mosaic-vikunja --restart unless-stopped --user "$(id -u):$(id -g)" \
-p 127.0.0.1:3456:3456 -v "$VD/db:/db" -v "$VD/files:/app/vikunja/files" \
--env-file "$VD/env" \
vikunja/vikunja@sha256:e2204a1c1c6a81e833c2b3a5442be182ca2335b54c2e7e37578cc3fe12a27cfc
docker exec -it mosaic-vikunja /app/vikunja/vikunja user create -u jason -e you@example.invalid
VK=http://127.0.0.1:3456
```
`user create` asks for the password when you leave out `-p`. If your
build doesn't prompt, stop and set it some other way. Don't put the
password on the command line.
### Owner login for this guide
This writes the owner's session header to a file. The password comes
from a prompt, and the token never reaches the terminal.
```sh
python3 - "$VK" jason "$S/vikunja-owner.hdr" <<'PY'
import getpass, json, sys, urllib.request
base, user, out = sys.argv[1:4]
pw = getpass.getpass("Vikunja owner password: ")
req = urllib.request.Request(base + "/api/v2/login",
data=json.dumps({"username": user, "password": pw}).encode(),
headers={"Content-Type": "application/json"})
body = json.load(urllib.request.urlopen(req))
if "token" not in body:
sys.exit("login answered without a token field; stop and report the keys: %s" % sorted(body))
open(out, "w").write("Authorization: Bearer " + body["token"] + "\n")
print("owner header written")
PY
```
The probes logged in at `/api/v2/login`. The `token` field name is
assumed from v1, so the script stops if it's missing. Section 4 deletes
the header file.
## 3. Vikunja: the project, the board and the bots
### The project and its board
In the web UI, as the owner:
1. Create the project `mosaic-stack`, and note its id (in the URL).
2. Open its Kanban view and rename the buckets: To-Do becomes `todo`,
Doing becomes `in-progress`, Done becomes `done`. Add `in-review` and
`blocked`. Each title appears once.
3. In the view's settings, check that the done bucket is `done` and the
default bucket is `todo`. Leave bucket configuration on manual.
4. Create the labels the business file template lists, and note each id.
The business file names labels by id until row S3 settles which
labels a bot can see.
```sh
P=<project id>
```
### 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-<business>-<role>` 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 <pm id> "$S/scopes-pm.json"
mint cto <cto id> "$S/scopes-worker.json"
mint coder <coder id> "$S/scopes-worker.json"
mint reviewer <reviewer id> "$S/scopes-worker.json"
mint sync <sync id> "$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/<old id>"`. 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.
+21
View File
@@ -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.