~bigbes/sr-ht-spec

ref: 096d25aaa374a0977f1ff5cb36a89753cc0c1d1f sr-ht-spec/mcpsrv/mcpsrv.go -rw-r--r-- 12.4 KiB
42d4ee13 — Eugene Blikh 26 days ago
feat(mcpsrv): spec_propose write tool (Phase 3)

The agent-facing half of the write plane over MCP. spec_propose uploads
whole documents and returns {proposal, url, merged}, calling the same
service.Propose the REST PUT will — one implementation of If-Match,
provenance and auto-merge behind both surfaces, never two that drift.

- Writer is an optional Backend field: nil keeps the server read-only
  (the three read tools, unchanged), so a read-only deploy or a test
  needs no mutable backend. Set, it registers spec_propose with neither
  the read-only nor the idempotent hint — proposing twice opens two
  proposals.
- The acting agent is resolved from the bearer token on the tool call by
  the resolver middleware now installed on /mcp; the read tools never
  needed it. The ACL stays in service/: service.Propose refuses a
  non-agent, so an anonymous or owner caller is rejected there.
8edca94e — bigbes 27 days ago
feat(mcpsrv): MCP read tools, with Host validation replacing the SDK guard

Adds the Phase 2 read tools (spec_search, spec_read, spec_list) over the
service layer.

Two security fixes came out of building them.

The read plane could serve proposal content. gitx resolves ref names, and
service.resolveRev passed any string through, so rev=proposals/42 made the
READ plane hand back unreviewed text — which would then flow into agent
context as though approved, the single failure this service exists to
prevent. ValidateReadRev now admits only the approved-head sentinel or a
full 40-character object name, at the layer all three surfaces share.
Abbreviations are refused too: one that is unique today can become
ambiguous later, so a pinned revision would silently stop meaning one
thing.

The MCP SDK's DNS-rebinding guard rejects a loopback listener whose Host
is not loopback, which is exactly nginx forwarding to 127.0.0.1 — it would
403 only in production, passing every local test. The SDK offers no
allowlist, so the guard is disabled and replaced by a stricter check: Host
must equal the configured origin, or a loopback name for development. A
rebinding attack carries the attacker's name in Host and fails it. An
unusable origin logs loudly rather than quietly unguarding the endpoint.