Files
stack/docs/plans/2026-10-20_agent-credentials.md

9.3 KiB

Brief: a credential store per agent, own git identity, pushes to a remote

Sage, 2026-10-20, for mos-0974 (MVP T1 item 6) and the MVP plan (jason-master-restart/projects/mosaic-mvp/PLAN.md item 6, GAP-MAP G11 and G13; TARGET.md: "credentials store available for all agents; secrets protected with RLS equivalent" and "Agent uses own credentials for git and other actions"). Gitea #1697. Owner Megavolt (Opus 5.5, a new builder seat), reviewer Duckworth, gate owner Sage. Base: refactor at the brief commit. Design first. The first deliverable is agents/megavolt/work/queue-ROW/DESIGN.md.

This row draws a security boundary. Sage reviews the design's authority model, and Mos rules on it before any code is written.

What exists today (GAP-MAP, measured at 9ae8c73ac)

  • G11. Credential references exist per instance for Gitea and Vikunja only (packages/business/src/credentials.mjs, packages/bus/src/credentials.mjs, vocabulary.mjs:103). One Claude env file is shared by every seat.
  • G13. Agents use their own Vikunja token (packages/tasks/src/adapter.mjs). Git goes through the host socket (packages/cli/src/git-socket.mjs). Gitea tokens are loaded but never used.
  • The development repository's helper (scripts/git-credential-mosaic.mjs, wired in .git/config for git.mosaicstack.dev) pushes as one identity. Until this row lands, seats push only their own <seat>/* branches through it (mos-0981).

What the design must settle

  1. The store. Where each agent's credentials live, per instance, under the row 190 config canon. File modes, ownership and the read path. Which process may read which entry, and how the bus enforces that (the "RLS equivalent"). A process asking for another agent's entry is refused and logged.
  2. Kinds. Gitea (git over HTTPS, and the API), Vikunja, and the model provider. Say which kinds 10-27 needs and which wait. The model provider entry must not copy jwoltje's claude-session.env. If it needs a new provider account or subscription, flag it for Jason and don't pick one.
  3. Git identity and push. Each agent commits with its own name and email and pushes with its own token to a remote it is granted. Which refs an agent may push (its own branches, never the default branch or another agent's), and where that's enforced: the Gitea side (branch protection, token scopes) and our side. How this fits the host git socket, or replaces it.
  4. Lifecycle. Issue, rotate (rotateBy, expires), revoke and delete, and what row 194 (agent lifecycle) calls when an agent is hired or fired. A fired agent's credentials stop working at once.
  5. Who does what. Creating a Gitea user or token, or any account outside the fleet, is Jason's (Q173 exclusions). The design lists each such step as a command or UI action for Jason. Agents never mint accounts.
  6. Migration. Today's seats (jarvis helper, seat tokens) keep working until each moves. The mos-0981 push rule ends only when Mos says so.
  7. What this row closes in G11 and G13, and what stays open.

Boundaries

  • No credential value is read, printed, logged or committed. Tests use fake tokens in scratch roots and a fake Gitea.
  • No change to ~/.mosaic-dev, ~/.config/mosaic-dev, the live units, slot files, ~/.mosaic or the live .git/config of the canonical checkout.
  • No request to git.mosaicstack.dev, tasks.woltje.com or any provider from a test.

Gate

Duckworth approves DESIGN.md, Mos rules on the authority model, then Duckworth approves the manifest. The bus, business, cli and tasks suites and every scripts/test-*.sh are green on Sage's gate, and the design's mutants are killed.

Brief: #436b Gitea grant path, build (schema 3j-3k)

Sage, 2026-10-20, for Vikunja P19 #436 (#436b), mos-1124 (Q190: each seat gets its own Gitea account through this row) and mos-1145 (Gitea moves to 10-27, at risk). Owner Megavolt, reviewer Dewey, gate owner Sage. Gitea #1742.

The design is agents/megavolt/work/436b/DESIGN.md r5 at 1be535aca on megavolt/row199, sha256 68df02cdf890564c6ed0a5426204114389ba7eb0eea1cf04f1bf5938b6de45f4, which Dewey approved in dewey-0763. Section 7 was approved by Duckworth at fa5658941 (duckworth-0067) and is unchanged since. It is not reopened here. The build also takes agents/megavolt/work/436b/BUILD-CARRY.md at 3330c901b (Dewey's N9 and N10), and its BUILD.md cites that file.

Scope: the Gitea half only. The binding (section 2), the executor and its admin credential (sections 3 and 4), failure and restart (section 5, with sage-1578's option (b): a step 0 refusal stays unspent only until the next start), revoke and sweep (section 6), the store records (section 7) and section 9's tests and mutants. The GitHub half (section 10, Bushroot) is not in this row.

Order and schema:

  • It builds on row 210's landing (the design: code waits on 210) and on machine grant row a (row 234), whose section 7 rule and system/grant writer it shares.
  • This row owns SCHEMA_VERSION 3k: its two tables go in 3j-3k.sql on row 234's landing. Row 206 (193b2) moves to 3k-3l and rebases onto this row's landing (Sage, 2026-10-20: row 205 is still queued behind the federation rows, so this row lands first).

Boundaries:

  • Live use waits for Jason. He approves section 8's notes (the admin token can take over any account on that Gitea) and does the one bootstrap step himself (3e): he creates mosaic-grant, its TOTP and its token. No agent creates a Gitea account or token (Q188).
  • No test reaches git.mosaicstack.dev or any provider. Tests use a fake Gitea and fake tokens in scratch roots.
  • Seats keep pushing as jason.woltje until this row is live and Jason grants their accounts (mos-1124).

Gate: Dewey approves the manifest. The bus, business and cli suites and every scripts/test-*.sh are green on Sage's gate, and section 9's mutants are killed, N9's restart test included.

Brief: row 199 follow-up N1, one decision publishes once

Sage, 2026-10-20, from Duckworth's row 199 round 1 note N1 (duckworth-0068, megavolt-0058). Owner Megavolt, reviewer Duckworth, gate owner Sage. Builds on row 199's landing. Gitea #1743.

One decision can push its commit to two fork refs, because checkPublish doesn't refuse while another ticket holds the decision. Pick one fix and say why in BUILD.md: refuse publish-busy in checkPublish, or recheck #spent in publishTarget. Add a test where two tickets race on one decision and exactly one push happens, plus a mutant that removes the check.

Gate: Duckworth approves the manifest. The bus suite and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed.

Brief: row 237 follow-up, one decision lands once, with tests at the broker

Sage, 2026-10-21, from Megavolt's checkLand analysis (megavolt-0068, megavolt-0069) and sage-1651. Owner Megavolt, reviewer Duckworth, gate owner Sage. Builds on row 237's landing d7d943199. Vikunja P19 441.

Landing already has three guards against one decision landing twice. land-busy at packages/cli/src/git-socket.mjs:351 allows one landing per socket at a time. finishLand moves main by compare-and-swap (packages/seat/src/working.mjs:391, refusing base-moved). recordLand reruns #landFacts, which refuses decision-consumed (packages/bus/src/broker.mjs:945). So no end-to-end race exists, and no code change is planned. What's missing is a test that pins each guard.

  • In packages/bus/tests/land.test.mjs, add a test where two tickets pass checkLand on one decision. The first recordLand succeeds and the second refuses decision-consumed, with nothing recorded for it.
  • Run three mutants and list them in BUILD.md: drop the #spent line in #landFacts, drop the old value from the CAS update-ref in finishLand, and drop the land-busy throw at git-socket.mjs:351. Each must be killed by a named test. If the CAS mutant survives packages/seat/tests/working.test.mjs:555, add a test to working.test.mjs that kills it. Otherwise leave that file alone.
  • If a mutant survives with no test that can reach it, stop and send me the analysis. Don't change source to make a test reach it.

Gate: Duckworth approves the manifest. The bus, seat and cli suites and every scripts/test-*.sh are green on Sage's gate, and the three mutants are killed.

Brief: row 237 follow-up N1, a retry after publish-timeout pushes once

Sage, 2026-10-21, from Duckworth's row 237 note N1 (#1743 comment 29810, duckworth-0069) and Megavolt's row 237 BUILD.md residual 2. Owner Megavolt, reviewer Duckworth, gate owner Sage. Builds on row 237's landing d7d943199. Vikunja P19 440.

The git socket gives up with publish-timeout (packages/cli/src/git-socket.mjs:433) while the bus process's push may still be running (publishPush in packages/bus/src/runtime.mjs, entry running). publishRefused then drops the ticket, so row 237's publish-busy check no longer holds the decision. A retry to another branch can then push a second ref to the fork.

Pick one fix and say why in BUILD.md. Either keep the decision held until the bus's push ends, or kill the push on timeout and wait for it to exit before the ticket goes. Add a test that starts a slow push, times it out, retries to another branch, and finds exactly one ref on the fork. Add a mutant that removes the fix. Don't change the publish-timeout refusal text.

Gate: Duckworth approves the manifest. The bus and cli suites and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed.