Install forgejo-mcp as a Service on the forgejo Guest (#22) #48
No reviewers
Labels
No labels
needs-info
needs-triage
ready-for-agent
ready-for-human
wontfix
needs-info
needs-triage
ready-for-agent
ready-for-human
review/merge-ready
review/needs-fix
review/needs-human
review/needs-review
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
olympus/infra-forge!48
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "hermes/22-install-forgejo-mcp-server"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A new
forgejo_mcprole and its own play install the pinned forgejo-mcp release as a third Service on theforgejoGuest. The endpoint is credential-less: it holds no Forgejo token, and inpassthroughauth a request without its own credential is refused401.The shape is ADR 0003 (merged as #43); this PR is the role that honours it:
.sha256);forgejo-mcpservice user; systemd unit enabled at boot, restarted on config change;0.0.0.0:8089behind an inventory-scoped host allowlist; an undeclaredHostis refused403;--auth-mode passthroughpassed explicitly, and--allow-operator-token-fallbackdeliberately absent;127.0.0.1:3080); no Edge entry, certificate, DNS record or router rule.The
forgejorole is untouched, so its handler-order contract (the runner restart lands after the Forgejo restart) stays intact.Evidence
Rehearsed on a systemd Guest stand-in (a disposable privileged Debian 13 container running
site.yml's new play unmodified — not the PRE Guest; see below). The role is exercised for real: systemd, the service user, the checksum-first fetch, the readiness probe.changed=11.changed=0.forgejo-mcp.servicecomes backactiveon its own, endpoint answers401.8089→8090): the unit re-renders, the handler restarts, and the endpoint answers401on the new port. The first attempt failed here — the handler ran after the readiness probe, so the probe polled a port the old process no longer held. Fixed withmeta: flush_handlersbefore the probe; that fix is what this run verifies.Host401Host(evil.example)403Hoston any port401(the bare allowlist matches any port)failedthe fetch rather than installing the archive.--check --diff(the repo's rehearsal gate) passes;site.yml --syntax-checkpasses on both inventories;tofu fmt -recursive -checkclean.Two-axis review (
/code-review) on the diff; the one substantive finding —forgejo_mcp_auth_modewas declared in defaults but never passed toExecStart, so the credential-less guarantee rested on an upstream default while a comment claimed otherwise — is fixed in the second commit.Merge Danger
Door: two-way
Reverting is a
git revert: the role directory and the new play drop out, and the Service stops on the next run. No secret was added (the Service is credential-less), so nothing needs rotating. The one thing that does not revert cleanly by itself is a pin mismatch — the role'sforgejo_mcp_forgejo_portrestates theforgejorole'sforgejo_http_portacross plays and must move with it.Blast Radius: one Guest, LAN-only
A third systemd unit lands on a Guest sized for two — RAM/CPU head-room wants watching (recorded in ADR 0003's consequences). The endpoint is reachable by any host on
10.12.0.0/24; it takes no WAN path and no Edge entry, so nothing fronts it from the internet. The--check --diffgate is partial by nature: the role mutates throughcommand/fetchtasks that--checkskips.Not in this PR
forgejo-prebranch"; that branch was retired by #28 and the overrides live onmainper ADR 0004. This PR follows the live convention, not the literal wording.10.12.0.0/24— only from the host running the container's bridged interface.Closes #22
Code review —
main...hermes/22-install-forgejo-mcp-serverTwo-axis review against issue #22 (and the feature spec,
.scratch/forgejo-mcp-spec.md) and the binding design in ADR 0003. Diff pinned at merge-baseb9d6ed0; 3 commits under review (8ca5bc1,dc33b4e,392823c). PR body carriesCloses #22.Standards
(a) Documented-standard violations (hard)
1. GLOSSARY.md —
Service: "Avoid: app, daemon, process." The diff calls the forgejo-mcp Service a "process" at three sites — the exact synonym the glossary bans:Each names the Service itself, not an incidental OS process. Use "Service" (or "instance"). (
app.iniis a literal filename, not a breach.)(b) Baseline smells (all judgement calls)
Duplicated Code — the "allowlist is inventory-scoped" rationale, five times. Nearly verbatim in
group_vars/forgejo/vars.yml,defaults/main.yml(L77–83), thetasks/main.ymlfail_msg,architecture.md, and the runbook. The repo favours a "why" comment at each use-site, which blunts this — but the four-line block is the same shape each time; one anchor plus a pointer would do.Duplicated Code / Data Clumps — the archive stem composed twice.
One stem var (suffix
.tar.gzat the use-site) removes the drift risk.Clean / conforming (checked): ADR 0003 honoured fully — bind
0.0.0.0, port8089, host allowlist,passthrough, no token, no operator-token fallback, loopback upstream, no Edge entry; no ADR contradiction to surface. Glossary terms used correctly otherwise (Service, Guest, Edge, LAN-reachable, Prod, PRE, Operator key). Role style matches its sibling (ansible.builtin.*FQCN, lowercase task names, why-comments, idempotence idioms); the separate role/play realises the ADR's handler-order requirement. No publish-manifest gap (both edited docs are already indocs/wiki-pages.yml).Minor:
group_vars/forgejo/vars.ymlandhost_vars/forgejo-pre.ymlstill lack a trailing newline — matches their prior state, so consistent, just non-POSIX.Spec
(a) MISSING or PARTIAL
1. Rehearsal criterion unmet — the biggest gap. #22: "The role is rehearsed on the PRE Guest before it touches Prod." The PR admits stand-in-only, PRE unreachable. Consequently Testing Decisions #1 ("A second playbook run reports
0 changed") and #2's second half ("anmcpclientinitialize+tools/listround-trip succeeds when the request carries a valid token") are unverified.2. Client-half documentation absent. Spec, Client half (documented, not provisioned): "Installing the
mcpPython package and itsmcp.client.streamable_httpextra, adding themcp_servers:entry, and restarting the Agent … is a runbook section." Story 24 repeats it. The runbook gained a server-side section only; grep finds nomcp_servers/streamable_httpanywhere. (#22's own AC doesn't name it, so arguably deferred — but the spec's story 24 asks for it.)3. Capability seam untested (Testing Decisions #3) — but token scope is ticket #24, so correctly out of scope here.
(b) NOT ASKED FOR (scope creep)
docs/architecture.mdrewrite — not requested by #22; the asked-for runbook content was the client half (story 24). Defensible repo hygiene, but it documents the wrong half.ca-certificatesapt task, theassertallowlist validation, explicit--auth-mode passthrough— none requested. All defensible hardening/dependency declaration; not defects.(c) Looks implemented but WRONG
1. The
401readiness probe is unproven. AC: a credential-less request is "refused401". The role assertsstatus_code: 401for aPOSTof{}to/mcp— but that was exercised only against a systemd stand-in, not the real forgejo-mcp binary, which may answer400/406to a non-JSON-RPC body before the auth gate.2. Idempotence rests on an unverified bet. The role claims the
unarchivetask is idempotent because "the directory persists between runs";unarchivewithoutcreatesdecides viatar --diffattribute comparison (owner/mtime/mode). AC: "a second playbook run reports0 changed" — unproven off the stand-in.3. Branch criterion contradicted. #22: "the PRE overrides park on the
forgejo-prebranch." Overrides land onmain(ADR 0004 retired the branch). The PR acknowledges the conflict; it is a spec/ADR-0004 conflict, not a code defect.Summary — Standards: 1 hard documented-standard violation (the glossary-banned "process" naming the Service, 3 sites) + 2 judgement-call smells (2 Duplicated Code); worst: the glossary breach. Spec: 3 gaps, 2 scope-creep notes, 3 likely-wrong; worst: the rehearsal criterion left unrun on the real PRE Guest, which also leaves the
401probe and the0 changedidempotence unverified.Addressed in
5a1a1e3. The big one — the rehearsal — is now run for real.Standards
process→Service(hard). Fixed at all three sites: the role'sflush_handlerscomment and the two runbook lines. The second runbook line now reads "the previous instance" rather than naming the Service at all.grep -rn process ansible/is clean.roles/forgejo_mcp/defaults/main.yml;group_vars/forgejo/vars.yml,host_vars/forgejo-pre.yml, thefail_msgandarchitecture.mdeach keep a short pointer. Thefail_msgkeeps only the actionable half (where to set it, to fail early).forgejo_mcp_unpacked_diris now derived:{{ forgejo_mcp_archive_name | regex_replace('\.tar\.gz$', '') }}, so the tarball name and the directory cannot drift. Verified it still rendersforgejo-mcp_3.2.0_linux_amd64.Spec
tofu apply -target=…forgejo_pre), ransite.ymlagainstinventories/pre/hostsfrom this branch, then destroyed it. Prod was never in the plan (1 to add, 0 to change, 0 to destroy, onlyforgejo_pre) and is untouched at the end. On the real Guest:changed=30,failed=0);0 changed— Testing Decisions #1, now verified off the stand-in as well;10.12.0.0/24at the pinned port:POST /mcpwith a declaredHostand no credential →401; undeclaredHost→403;enabled/activeand answers401on its own.401probe "may answer 400 to a non-JSON-RPC body" — resolved. It answers401. The probe ran against the real forgejo-mcp binary on PRE (ok), and the curl above hits the same path with{}and gets401, so the auth gate does beat body validation.main).Pushed back — (b) scope creep
architecture.mdis not optional hygiene: itsdocs/index.md"update when" is "the topology changes (a new component, a moved port…)", and this PR adds a component and a port, so leaving it stale is the breach. The runbook §4 server half is a server section, not the client half — it carries the exact probe to run, which the rehearsal above just exercised. Both stay.The
ca-certificatestask, theassert, and explicit--auth-modewere accepted as hardening;--auth-modein particular closed a real gap — it was declared in defaults but never reachedExecStart.