Add explicit edge and direct-gateway access contracts, SSH server-side refusal/canary checks, and public MCP continuation/integrity checks. Validated by the isolated e2e gate, 502 contract tests, independent native review and both actual PR CI checks on the exact candidate. Production Stage3 remains separately planned.
25 KiB
Access-route contract inventory at 4cc434a
Focused inventory of the source-defined access contracts on Laboratory
main 4cc434a80a0f70ea3a448f387a09b699f04e8979 (2026-10-14, the
PR #126/#127 lane state), with the redirect/cancellation round deltas
(commits 3da0c13, 64f40ab), this inventory itself (73403b7), and
the review-fix round deltas (commits 5c53b1c, 06136f8) marked
explicitly. Public-MCP integration round (2026-10-15, on top of
a02eb9d): the anonymous public catalog section below replaces the
former "planned, NOT deployed, NOT accepted" placeholder — the endpoint
shipped as ad99aae, was Root-live-accepted on 2026-10-15, and the
lane now carries its contract. Every asserted contract carries an exact
source anchor (file:line at the named commit) or a timestamped
accepted-evidence pointer. Companion docs:
the owner requirement and access matrix live in
ACCESS-VALIDATION.md; the implementation record
in ACCESS-VALIDATION-IMPLEMENTATION.md;
lane usage in scripts/access-validation/README.md.
Anchors to files untouched by the redirect/cancellation round are
identical at 4cc434a and on top of it.
Public HTTPS Git
- Edge: apex catch-all proxies everything unmatched to the forge
(
caddy/Caddyfile:177at4cc434a); git smart-HTTP is served by the forge behind it. Canonical-shape redirects the edge really answers:/about→/about/(caddy/Caddyfile:56),/agents(68),/agents/console(74),/contribute(118),/observatory(146) permanent;www.oss-oo.io→ apex permanent (caddy/Caddyfile:188);/donate→donate.oss-oo.io(114). - Public ref advertisement:
scripts/verify-live.sh:33runsgit ls-remote --heads https://oss-oo.io/<repo>.git(read-only);watchdog/watchdog.py:45probes/OSS-OO/laboratory.git/info/refs?service=git-upload-packexpecting 200. - Lane proof: stage 2
git-https/public-anonymous-read(legitimate anonymous read of a PUBLIC fixture — native VM1005 evidence), stage 3git-https/public-fetch. - Every HTTPS Git probe, including anonymous reads, requires the final canonical URL. Redirects are refused so an HTTP fallback cannot count as a successful HTTPS transport proof (policy below).
Authenticated HTTPS Git
-
Credentials reach git only through a generated
GIT_ASKPASShelper under a neutral env name, with station/system git config neutralized (GIT_CONFIG_GLOBAL=/dev/null,GIT_CONFIG_NOSYSTEM=1,GIT_TERMINAL_PROMPT=0,GIT_ALLOW_PROTOCOL=http:https) —scripts/access-validation/av_common/clients/git_https.py:63(_run_git) at4cc434a. -
Acceptance correction (2026-10-15): Git executes with an owned neutral temporary cwd and HOME, with ancestor repository discovery fenced. Caller-local credential helpers, askpass settings and extra headers and caller
.netrccredentials are not inherited. SSL_CERT_FILE and SSL_CERT_DIR references remain explicitly preserved. Pushes take refs/objects from the explicit caller repository via an owned local shared mirror, whose configuration and hooks are independent; no source configuration/files are changed. Preparation and transport share the existing per-check Deadline. Tests exercise general and URL-scoped synthetic credential bait, anonymous refusal without a secret, selected-token authentication, and pushes from ordinary and linked-worktree subdirectory sources. All explicitly addressed local ref namespaces are preserved, including remote-tracking and notes refs. Even with the preparation mirror's ownremote.origin.mirror=true, the explicit target URL/refspec push updates only the requested destination refs. -
Redirect seam pin (this round, commit
3da0c13): every git invocation initially carried-c http.followRedirects=initial(git_https.py:63_git_argvon3da0c13). Acceptance correction (2026-10-15): HTTPS targets, including anonymous reads, and all credential-bearing probes now pinhttp.followRedirects=false, both generally and for the exact operator-named URL. All redirects are errors; the operator must supply the final canonical repository URL, including for a same-origin path redirect. A matching URL-scoped command-line pin is necessary because a URL-scoped local setting overrides a general command-line setting. Only anonymous reads from an explicitly named HTTP target retaininitial. Verbatim from git-config(1) (the manual page shipped with git 2.43.0 on the workstation,man git-config— the official git documentation):http.followRedirectsWhether git should follow HTTP redirects. If set to true, git will transparently follow any redirect issued by a server it encounters. If set to false, git will treat all redirects as errors. If set to initial, git will follow redirects only for the initial request to a remote, but not for subsequent follow-up HTTP requests. Since git uses the redirected URL as the base for the follow-up requests, this is generally sufficient. The default is initial.initialis not an origin or hop bound: one initial request may traverse a redirect chain. Real-Git acceptance probes against the native candidatec0b6ad7followed A→B→C and supplied the synthetic askpass credential after C challenged; a verified HTTPS→HTTP redirect did the same. The currentfalsepins prevent either request from reaching a redirect destination. These are real-binary loopback regressions instage1_contract/tests/test_redirect_boundaries.py, including the URL-scoped local override and the compatible anonymous redirect chain. No live hostile-route drill is claimed. -
Native e2e proof shape:
scripts/e2e-local.sh:551-555clone+commit+ push over loopback HTTP with user/pass in the URL (e2e-only fixture; the lane's transport-honest idgit/e2e-loopback-http-push). -
Lane proofs: stage 1
git-https/askpass-fetch,wrong-token,push; stage 2git-https/authenticated-fetch,wrong-token(on the distinct private fixture, auth-shaped refusal required),canary-push(synthetic write +commit.idreadback + verified cleanup); stage 3git-https/canary-pushbehind the fail-closed canary gate.
Pinned-host SSH Git (deployed public port 2222)
- Deployed port: host sshd keeps 22, git-over-ssh lands on 2222 —
compose.prod.yml:31-32(${FORGEJO_SSH_PORT:-2222}:22),compose.pve.yml:17-19,compose.yml:63(FORGEJO__server__SSH_PORT, default 2222). - Known-hosts normalization: non-22 ports write the host field as
[host]:portin the tempUserKnownHostsFileunderStrictHostKeyChecking=yes—av_common/clients/git_ssh.py(ls_remote/push/_run_git_ssh) at4cc434a; pinned bystage1_contract/tests/test_git_ssh.py(test_known_hosts_host_field_port_shapes).accept-new/first-use is never used anywhere in the seam; the captured keyscan is verified against the approvedhost_key_pinsfail-closed BEFORE any subprocess exists. - Historical accepted evidence (timestamped, see the table below): Root 2026-10-14T05:47:19Z verified the deployed pinned host and invalid-key rejection; authenticated SSH clone was deliberately NOT repeated then.
- Lane proofs: stage 1 banner/keyscan parse + hermetic transport
contract; stage 2 optional SSH overlay (authorized fetch, wrong-key,
unauthorized-repo, synthetic canary push); stage 3
ssh/authorized-fetch,ssh/wrong-key, gatedssh/canary-push.
Public REST reads
/api/v1/versionanonymous:scripts/verify-live.sh:23,watchdog/watchdog.py:42.- Registry presence
/v2/→ 401:watchdog/watchdog.py:44. - Client:
av_common/clients/http.pyrest_request(redirect policy below applies).
Authenticated MCP (https://oss-oo.io/mcp)
- Route + edge guard:
caddy/mcp.caddy—@mcp path /mcp /mcp/*(line 18), anonymous die at the edge with 401 (line 31), livenessGET /mcp/healthzanonymous (line 22), OPTIONS preflight 204; imported bycaddy/Caddyfile:162. - Stateless POST JSON-RPC, MCP 2025-06-18: one JSON-RPC 2.0 object per
POST;
GET /mcp→ 405 andDELETE /mcp→ 405 (no SSE stream, no server-side sessions) —agent-gateway/remote_server.py:11-13(contract header),:206-217(do_GET→ 405),:223-225(do_DELETE→ 405). - Batch arrays → 400 (
remote_server.py:303), wrong Content-Type → 415 (:296), budget exhaustion → HTTP 429 withRetry-After(:283-285; per-token sliding window 600 read-class / 60 write-class per minute —caddy/mcp.caddyheader notes, owner decision D7). - Anonymous → 401 before any JSON-RPC processing (edge guard above;
gateway
do_POSTauth). - Acceptance path the lane demands everywhere:
initialize→tools/list→ realtools/callread (get_file) — a health 200 proves liveness only (IMPLEMENTATION doc, "Truthfulness first"). - Unit/protocol proofs:
agent-gateway/test_mcp.py(local stdio surface),agent-gateway/test_remote.py(remote door: auth/scope/ revocation negatives, budgets, caddy snippet). Lane proofs: stage 1mcp/*six legs; stage 2mcp/surface+ 405/400/415 + anonymous; stage 3mcp/surface. - Client:
av_common/clients/mcp.pymcp_raw_post— same redirect policy as REST (below).
MQTT over WebSocket
- Endpoint
wss://events.oss-oo.io/mqtt(any path upgrades; non-upgrade → mosquitto 403), MQTT 3.1.1/5 over subprotocolmqtt—docs/EVENTS.md:94-98; edge proxycaddy/Caddyfile:199-204; plain TCP 8883 deliberately not opened (owner decision D9). - Probe client (CONNECT/CONNACK rc semantics):
watchdog/mqtt_probe.py:53(connect). - Lane proofs: stage 1 CONNACK + rc 5 negative + non-upgrade refusal; stage 2 CONNACK + authorized SUBSCRIBE + canary event through the product path + private-topic silence; stage 3 CONNACK + negative.
LFS / registry presence probes (presence-only, explicitly)
- LFS: protocol batch route
POST <repo>.git/info/lfs/objects/batchwith the native empty-download body andapplication/vnd.git-lfs+jsonmedia types; anonymous 401 proves the endpoint. Native evidence: 401 on VM1005 (v4 round). NO LFS content transfer is attempted or claimed. - Registry: package API list route
GET /api/v1/packages/{owner}(scope-gated byread:package; native 403→200 diagnosis in the IMPLEMENTATION doc) plus the public/v2/401 presence shape. NO OCI content round-trip is attempted or claimed. - Lane proofs: stage 2/stage 3
lfs|registry/endpoint-presence.
Anonymous public MCP catalog — deployed, accepted (ad99aae)
POST https://oss-oo.io/mcp/public is LIVE and independently accepted:
merged as ad99aae ("feat(mcp): add anonymous public repository
exploration", PR #132), Root live acceptance 2026-10-15 against the
production edge. Source of truth: agent-gateway/mcp_public.py (981
lines at a02eb9d) + its serving side in agent-gateway/remote_server.py
(_public_post, and do_GET/do_DELETE/do_OPTIONS for /mcp/public).
Offline protocol proofs: agent-gateway/test_public.py (81 cases).
- Exactly four tools —
public_search_repositories,public_repo_info,public_list_tree,public_get_file— one coherent find → orient → browse → read flow. Every other name (authenticated-surface, registrar, garbage) is refused with the bounded unknown-tool error and NO upstream call (mcp_public.call_public_tool). - Anonymous by construction: the server strips any inbound
Authorization header before work (
remote_server._public_post); the upstream client never attaches one and never reads credential material; it also never follows redirects (any 3xx is a refusal — server-side behavior; the lane client keeps the shared lane redirect policy). - Uniform refusal: private repo, nonexistent repo, and missing path
all answer ONE identical bounded
isErrorrefusal — "not found or not public" (mcp_public.NOT_FOUND_MSG) — never differentiated. - Complete chunked retrieval:
public_get_fileserves base64 chunks over HTTP Range with per-chunk verification (206, content-range offset/total, exact delivered length, ETag equal to the git blob sha); continuation cursors are HMAC-signed and opaque; concatenating decoded chunks toeofreproduces the file byte-for-byte (git blob digestsha1("blob <len>\0" + bytes)plus total-length check). A 0-byte file is complete at offset 0. A completeness-checked full-body/git/blobsfallback exists behind the upstream blob-size knob; no arbitrary size cap exists on the Range path. - Pagination continuity:
public_list_treewalks byte-wise-sorted windows pinned to one immutable commit via signed cursors (has_more⟺next_cursor, exact termination; continuations must repeat owner/repo and MUST match the cursor — path/ref are ignored on continuations, the cursor is authoritative; tampered/invalid cursors are refused).public_search_repositoriespages with an unknown total: a FULL page never reads as complete. - Method/transport contract — TWO serving shapes, bound to the
DECLARED target (Root-observed live edge, 2026-10-15): at the
public edge (caddy in front of the service) every non-POST —
GET,DELETE,OPTIONSincluded — answers 405 with a bounded text/plain body, structurally NOAllowheader, and noAccess-Control-Allow-*headers; at the direct gateway (the service itself)GET/DELETE /mcp/public→ 405 withAllow: POST, andOPTIONS /mcp/public→ 204 with deliberately NOAccess-Control-Allow-*headers. The stage-2/3public_mcpsections carry a REQUIREDtargetkey (direct-gateway|public-edge) and the evaluator binds statuses, the Allow-header expectation, and the receipt wording to that target — one surface's answer can never be relabeled as the other's proof (mis-pairing fails in BOTH directions, pinned by tests and runner rigs). Per-client-IP budget before any upstream work → HTTP 429 withRetry-After(mcp_public.check_budget). The authenticated/mcpsurface is unchanged (regression rows above). - Client seam:
av_common/clients/mcp.pypublic_*helpers — the anonymous path has NO token parameter at all (credential-freedom is structural; stage 1 proves it from the receiving journal), and thepublic_read_file/public_list_treewalkers turn the cursor contracts into complete verified results — a truncated transfer, an offset discontinuity, or a digest mismatch raises and can never pass. - Lane proofs (route
public-mcp, allread-only): stage 1 pins the full contract against the in-process fake (stage1_contract/fakes.pyFakePublicMcp, hostile knobs for truncated/corrupt-transfer and snapshot-expiry drills, plus anedge_modethat mirrors the caddy public edge) — 15 rows incl. exact tool scope, search-page semantics, cursor tamper/mismatch refusals, the 429 budget, BOTH method/CORS surfaces (direct gateway and public edge), credential-freedom, truncated-transfer-fails, and the distinct snapshot-expired refusals failing the walk. Stage 2 carries an OPTIONALpublic_mcpconfig section (inert without it; config names fixture NAMES only — no credential envs exist on this surface; the REQUIREDtargetkey declares which surface the URL names) — 7 rows against the configured stack. Stage 3 carries the same 7 rows against a real named environment, inert without thepublic_mcpsection.
No SSE, no server-side sessions, and no authentication exist or are claimed on the public surface. Codex-native client proof exists but is a separate package; this lane makes NO client-acceptance claim (ChatGPT/ZCode/Codex client receipts remain their own stage-3 slots).
Redirect & authentication-boundary policy (2026-10-14 round, 2026-10-15 acceptance corrections)
The HTTP clients no longer rely on urllib's default redirect behavior
(at 4cc434a rest_request/mcp_raw_post used bare
urllib.request.urlopen; git pinned no http.followRedirects — no
test anywhere pinned cross-origin Authorization stripping). The
explicit policy (av_common/clients/http.py open_with_policy on
3da0c13, applied identically by mcp.py):
- Authenticated REST/MCP requests (any Authorization header present,
with case-insensitive header names) NEVER
follow a cross-origin redirect (scheme, host, or port differ): the
client refuses with a deliberate
unsafe-redirecterror carrying the 3xx answer, and the header is never sent to the other origin (proven by recording sinks in the tests). Never a silent retry without the header. - https→http downgrade is refused for ANY request, authenticated or not.
- Same-origin redirects are followed, Authorization preserved
(journal-pinned on the followed hop by the review round
06136f8), bounded byDEFAULT_MAX_REDIRECTS = 3hops with a per-callmax_redirectsoverride for callers that need it. 303 (and 301/302 for body-carrying requests) downgrade to GET without a body; 307/308 preserve method and body exactly (both semantics journal-pinned from what the server actually received, review round06136f8). Rewriting to GET removes Content-Type, Content-Length, and Transfer-Encoding regardless of header-name casing. Per-hop socket timeouts derive from the remainingnetutil.Deadline. - Loopback carve-out, precisely: there is none for credentials —
the cross-origin refusal applies on loopback exactly as anywhere
else. What IS allowed is an UNAUTHENTICATED same-scheme cross-origin
redirect (no credential boundary exists to cross — nothing to
exfiltrate); the tests prove this on loopback because that is where
the fakes live (
test_unauthenticated_cross_origin_loopback_followed). - Refused Locations: userinfo credentials
(
scheme://user:pass@...), non-http(s) schemes, syntactically invalid or out-of-range ports (review round5c53b1c— previously such Locations crashed with an uncaughtValueErrorat the origin comparison instead of refusing), malformed URL authorities (including invalid IPv6 brackets and missing hosts), and 3xx answers without a Location header. Locations must be ASCII URI references: spaces and non-ASCII characters must be percent-encoded. Raw 3xx answers reach the policy before urllib can parse a malformed Location; refusals therefore return a ClientResult rather than escaping as a parser exception. Refusal texts never quote the Location. - Hop-limit rationale: a small bound (3) is a SAFETY bound against redirect loops — it terminates what would otherwise spin until the deadline; it is not an invented performance cap, and callers can override it explicitly when a legitimate route needs more hops.
- The followed chain lands in
ClientResult.meta["redirects"]in scrubbed form (scheme://host[:port]/path— no query strings, no userinfo, no header material; IPv6 literal hosts re-bracketed, review round5c53b1c), safe underav_common.report.scrub. MCP retains that chain on HTTP-error results as well as successes. - TLS probes (
av_common/clients/tls.pytls_probe) hit the named host:port directly over a raw socket — there is no HTTP layer and hence no redirect surface; pinned bystage1_contract/tests/test_tls.py(test_probe_connects_to_the_named_host_directly). - HTTPS Git and authenticated HTTP Git refuse every redirect with general and
URL-scoped
http.followRedirects=falsepins (citation above). Canonical repository URLs are required. Only anonymous, explicitly named HTTP targets retaininitial; they do not carry an askpass secret, and that setting does not promise a single-hop or single-origin bound.
Operator cancellation (round 64f40ab)
SIGINT (KeyboardInterrupt) and SIGTERM (converted by
av_common/cancellation.py install()) interrupt a runner at the next
interpreter boundary; the runner finalizes the report immediately
(in-flight row and every never-run inventory row →
notrun/cancelled-by-operator; completed rows keep their real
verdicts; pre-existing honest notrun reasons are never relabeled),
writes it, and exits 130 (the 128+SIGINT shell convention, carried
for both signals). Cancellation never fabricates a passed or failed
verdict — OperatorCancelled derives from BaseException precisely so
no except Exception probe boundary can swallow it into a "failed
check". Temp hygiene survives (askpass helpers, known_hosts cleanups
run in finally blocks).
What each existing validator proves today
| Validator | Proves | Pointers |
|---|---|---|
scripts/e2e-local.sh |
disposable Forgejo/DB: REST repo/PR changes, actual git clone/push over loopback HTTP, local stdio + remote MCP, registrar/join/renewal, cookie console/CSRF, mock mail, broker/bridge, internal WS tests | scripts/e2e-local.sh (git legs at 548-560); coverage table in ACCESS-VALIDATION.md |
agent-gateway/test_mcp.py, test_remote.py |
MCP initialize/discovery/read/write, protocol/auth/scope/revocation negatives, budgets, shared MCP caddy snippet | agent-gateway/test_mcp.py; agent-gateway/test_remote.py (sections incl. budget_section, caddy_section) |
scripts/verify-live.sh |
apex A record, HTTPS apex, /api/v1/version, www redirect, optional public ls-remote |
scripts/verify-live.sh:12-34 |
watchdog/watchdog.py |
seven expected HTTP statuses incl. git ref advertisement, MCP healthz, registry 401; its own no-redirect HTTP prober | watchdog/watchdog.py:41-46, :396-408 |
watchdog/mqtt_probe.py |
authenticated MQTT-WS CONNECT/CONNACK | watchdog/mqtt_probe.py:53 |
| Lane stage 1 | client contracts against in-process loopback fakes incl. the redirect policy, its refusals, and the anonymous public-MCP catalog contract (exact four-tool scope, chunk/digest verification, uniform refusal, method/CORS, budget) | scripts/access-validation/README.md; IMPLEMENTATION doc stage table |
| Lane stage 2 | authenticated E2E on the isolated stack (identity, scopes, private separation via an independent outsider user, canary writes with native-schema readback + verified cleanup, MQTT product-path events) + optional anonymous public-MCP rows behind the public_mcp config section |
same |
| Lane stage 3 | exact-release real-route public checks + gated canary writes behind the fail-closed gate chain + optional anonymous public-MCP rows behind the environment's public_mcp section |
same |
Historical accepted evidence (timestamped — NOT current observations)
| Evidence | At (UTC) | What it proved | Pointer |
|---|---|---|---|
Root live acceptance of deployed PR131 == 4cc434a |
2026-10-14T05:47:19Z | 18/18 public cases; native files/containers; HTTPS Git + authenticated MCP read; SSH pinned host + invalid-key rejection; authenticated SSH clone deliberately NOT repeated | ~/.codex-logs/oss-oo-masterbrain-2/20261014-execution/laboratory-pr131-live-v1/ROOT-LIVE-ACCEPTED.json (outside git; sha256s of its proofs inside) |
| Root VM1005 final acceptance of the v4 lane tree | 2026-10-13 | stage-2 board incl. the five-scope owner token; native refusal shapes (wrong-token auth-shape, commit.id, LFS 401, registry scope gate) |
IMPLEMENTATION doc, v4 sections |
| v4.1 native release-shape captures | 2026-10-13 | tag/ref/commit API shapes on the real public v1.74.0 release (read-only) | IMPLEMENTATION doc, v4.1 section |
Historical results are evidence with a timestamp, never a claim about the live platform at read time.
Honest gaps (registered, not silent)
- Real SSH handshake proof is VM1005-only: the git-SSH transport has hermetic injected-runner proof on workstations; a real ssh handshake / git-over-ssh has never executed there (by constraint). Wrong-key/unauthorized negatives fail (not pass) on local refusals, so a fail-closed state cannot fake a server-side verdict.
- Browser proofs
unsupported: cookie-jar e2e evidence can never mark a browser check passed; a rendered-browser proof does not exist in the lane yet. - LFS/registry content round-trips not attempted: presence probes only (see above); object upload/download/pull proofs are a future configured-target round.
- Client receipts pending: Codex/ChatGPT/ZCode rows pass only on
verified
client-runreceipts, which do not exist yet. - Public MCP: the contract is deployed and accepted (see its
section) and stage 1 pins it against fakes; the CONFIGURED stage-2
execution against a standing isolated stack and any stage-3 real-route
run of the new rows had not been executed yet at integration time
(2026-10-15) — those rows report
notrununtil a configured round runs them. - Redirect policy proven against fixtures, not the live edge: the refusal semantics are pinned by loopback contract tests; the live Caddy redirects are canonical same-origin path redirects (anchored above) and stage 3 exercises the real routes, but no live cross-origin refusal drill has been run (it would require a deliberately hostile route Root has not authorized).