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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user