plans: row 199 design r5, the host derives each agent's fork from the upstream and its own Gitea user (webby-0011 N1), identity optional and required by publish (#1697)

This commit is contained in:
jason.woltje committed 2026-10-20 14:36:20 -05:00
1 parent 12c85ada18
commit 34bfe4a53d
1 file changed
+25 -7
+25 -7
View File
@@ -13,6 +13,14 @@ drops O2 (D10, A3, R2).
r4 answers duckworth-0017 (REVIEW-r3.md at 4065ed59f): B3 in T3 and D9 step 5, B4 in D9 steps 1, 3 and 5, N6
and N7 in D9a, N8 in D9 step 1.
r5 takes webby-0011 N1 (row 194 review, through honker-0009): the host checks at each push that the
remote's owner is the agent's own Gitea user. r4 named one `remote` per project, which can't be a fork per
agent (O1) once two agents publish the same project. So `projects.<p>.remote` now names the upstream
repository, and the host builds each agent's fork URL from it (D9 step 2, D10, T3). It also fixes a conflict
found while building: D8 made `identity` required for every agent whose definition declares `gitea`, while D9
step 2 says today's files load unchanged. Coder declares `gitea`, so r5 makes `identity` optional and
`publish` require it (D8).
## 0. Terms
- **Stack instance**: one Mosaic Stack deployment (row 190). It has one OS user, one config dir
@@ -114,7 +122,8 @@ doesn't pick. A pi model credential also waits for P19 #383.
## 5. Git identity and push (brief item 3)
D8. **Identity.** A new business-file key, `roles.<agent>.identity = {name, email, gitea: {url, user}}`. It is
required when the role definition declares `gitea`. The sandbox sets `GIT_AUTHOR_*` and `GIT_COMMITTER_*` from
optional, because today's business files with a `gitea` agent (coder) must load unchanged. `publish` (D9
step 2) requires it, and requires that the definition declare `gitea`. The sandbox sets `GIT_AUTHOR_*` and `GIT_COMMITTER_*` from
it in place of E7's invalid domain. The email is the one Jason gives the agent's Gitea user, so Gitea links the
commits to that user. Agents without `gitea` keep E7.
@@ -146,9 +155,14 @@ The host checks, in order, and each refusal goes through `broker.refuse()`:
reviewed commit adds it to both ROUTED lists and to coder's `withinRole`. From then on a coder publishes
without a decision, and `decision` is optional for a within-role class only.
2. The project is in the agent's grant, `roles.<agent>.publish: [project]`, and the business file names the
project's remote, `projects.<project>.remote: "https://..."` (`remote-not-granted`). The remote's scheme is
https and its host and port equal `identity.gitea.url`'s (`remote-host-mismatch`). The default https port
counts as 443 on both sides. `remote` is a new key
project's upstream repository, `projects.<project>.remote: "https://HOST/OWNER/REPO"`
(`remote-not-granted`). Its scheme is https and its host and port equal `identity.gitea.url`'s
(`remote-host-mismatch`). The default https port counts as 443 on both sides. The host never pushes to
that URL. It builds the agent's fork, `https://HOST[:PORT]/<gitea.user>/REPO`, from the upstream's
origin and repository name and the agent's own `gitea.user`, and pushes there. So the remote's owner is
the agent's own Gitea user on every push, by construction, and the agent can't name it (webby-0011 N1).
Gitea's refusal of a push to a repository the user doesn't own or collaborate on is the second layer.
An upstream whose owner is the agent's own user refuses `remote-not-fork`, at load and at push. `remote` is a new key
beside `root` and `repo` (`business.mjs:186`), and `identity` and `publish` are new keys on a role entry
(`business.mjs:89`). All three are optional, so today's business files load unchanged.
3. The source is one commit, `C = context.pushCommit`. It is checked three ways, so all three are equal:
@@ -203,7 +217,8 @@ The op table gets no entry that returns a token.
D10. **Which refs.** Our side: only `refs/heads/<gitea.user>/*`, set by the host (D9 step 3). The default
branch and other agents' prefixes can't be named. Gitea side, two options (Q-M1):
- **O1. A fork per agent. Ruled by Mos (mos-1027).** The agent's Gitea user has read on the upstream repo and owns a fork.
- **O1. A fork per agent. Ruled by Mos (mos-1027).** The agent's Gitea user has read on the upstream repo and owns a fork
named like it. The host derives the fork's URL from the upstream and the agent's user (D9 step 2).
Its token can write only repos the user owns or collaborates on, so Gitea itself stops any push to upstream
or to another agent's fork. No branch rules are needed per agent, so none can drift. The cost: work reaches
upstream as a pull request from the fork, opened by a human or a later API verb.
@@ -327,7 +342,9 @@ All tests use scratch roots, fake tokens and a fake Gitea: `git http-backend` be
and that no `roles/*.json` grants it. A force-push attempt is
refused by git's non-fast-forward. A fake remote that answers 302 to a second 127.0.0.1 port: the push
fails, and the second port receives no request with an Authorization header. A remote on the right host
and a different port is refused at step 2. A first publish to an empty remote and to an unrelated remote
and a different port is refused at step 2. Two agents granted one project each land on their own fork:
agent A's push reaches `/<A's user>/REPO` only and B's reaches `/<B's user>/REPO` only, and the fake's
upstream repository receives nothing. An upstream owned by the agent's own user refuses `remote-not-fork`. A first publish to an empty remote and to an unrelated remote
each refuse `remote-unrelated`, and the fake remote receives no objects.
- T4. Token handoff: the token never appears in argv, env, the remote URL, the working repo config,
`changes.jsonl`, events or logs (scan every file under the scratch roots).
@@ -342,7 +359,8 @@ check, allow a `{file}` reference into another agent's dir, make ENOENT on a `st
ENOENT on a `{file}` path silent, a helper that answers any host, drop `http.followRedirects=false`, drop the
`work-not-yours` check, drop the `remote-unrelated` check, add `git.push.remote` to ROUTED, skip the spent
check on the decision, drop the `pushCommit` comparison in `#authorityFor`, push by ref name in place of the
oid, skip the tip check under the lock, and pass a CA flag in production.
oid, push to the upstream URL in place of the derived fork, take the fork's owner from anywhere but the agent's
`gitea.user`, skip the tip check under the lock, and pass a CA flag in production.
## 12. Risks