docs(remote-mcp): restamp the #956 threat model onto the commit it resolves at (review 637 B1)

Commit 126d76a set docs/remote-mcp/threat-model-anchors.json
generated_against_commit to e3fa3b26 and left docs/remote-mcp/threat-model.md
citing a143cd06, so the document and the fixture named different source
revisions. tests/test_issue_956_threat_model.py asserts the fixture value
appears in the document, and that assertion failed from 126d76a onward.

The mismatch was not cosmetic. Resolving all 58 anchors against each candidate
revision shows 0 unresolved at e3fa3b26 and 21 unresolved at a143cd06, so the
document's own claim about where its file:line citations resolve was false.
The fixture held the accurate value and the document held the stale one.

The B2 fix in ca5f078d inserted a net 16 lines into gitea_mcp_server.py at a
single point, shifting 21 anchors below it. All 21 are re-derived here by that
one uniform offset and each was verified against its recorded expect substring
rather than assumed, so no anchor is guessed and none became ambiguous. Both
artifacts now name ca5f078d, the commit those anchors were taken at, and the
document's inline citations are shifted to match. The boundary table's claim
about what the code enforces is restamped onto the same revision, because it
described that same source tree.

Verified: 58/58 anchors resolve at the declared generation commit, 58/58 at
this head, no two anchors share a file:line location, fixture and document
metadata agree, and tests/test_issue_956_threat_model.py passes 17/17.

Refs #708

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-28 17:25:24 -05:00
co-authored by Claude Opus 4.8
parent ca5f078d8a
commit 58bd852188
2 changed files with 50 additions and 48 deletions
+22 -22
View File
@@ -8,10 +8,10 @@
"document. #930's inventory had no such guard and its gitea_mcp_server.py", "document. #930's inventory had no such guard and its gitea_mcp_server.py",
"anchors drifted between 7bf4f125 and aad5c8b4." "anchors drifted between 7bf4f125 and aad5c8b4."
], ],
"generated_against_commit": "e3fa3b263d4b8a04b189e111a345ac5c3a3fe1b6", "generated_against_commit": "ca5f078d8a575ea3e2991771f8b4ea85e3dcaaa0",
"anchors": [ "anchors": [
{ {
"anchor": "gitea_mcp_server.py:24848", "anchor": "gitea_mcp_server.py:24864",
"expect": "mcp_daemon_guard.bind_native_mcp_transport()" "expect": "mcp_daemon_guard.bind_native_mcp_transport()"
}, },
{ {
@@ -27,11 +27,11 @@
"expect": "def assess_transport_for_auth_mint" "expect": "def assess_transport_for_auth_mint"
}, },
{ {
"anchor": "gitea_mcp_server.py:9175", "anchor": "gitea_mcp_server.py:9191",
"expect": "assess_transport_for_auth_mint()" "expect": "assess_transport_for_auth_mint()"
}, },
{ {
"anchor": "gitea_mcp_server.py:9424", "anchor": "gitea_mcp_server.py:9440",
"expect": "assess_transport_for_auth_mint()" "expect": "assess_transport_for_auth_mint()"
}, },
{ {
@@ -39,31 +39,31 @@
"expect": "The transport is selected by deployment configuration" "expect": "The transport is selected by deployment configuration"
}, },
{ {
"anchor": "gitea_mcp_server.py:15465", "anchor": "gitea_mcp_server.py:15481",
"expect": "def _is_client_managed_process" "expect": "def _is_client_managed_process"
}, },
{ {
"anchor": "gitea_mcp_server.py:15495", "anchor": "gitea_mcp_server.py:15511",
"expect": "def _provenance_mutation_block" "expect": "def _provenance_mutation_block"
}, },
{ {
"anchor": "gitea_mcp_server.py:15503", "anchor": "gitea_mcp_server.py:15519",
"expect": "unsupported_manual_launch" "expect": "unsupported_manual_launch"
}, },
{ {
"anchor": "gitea_mcp_server.py:19054", "anchor": "gitea_mcp_server.py:19070",
"expect": "server_provenance" "expect": "server_provenance"
}, },
{ {
"anchor": "gitea_mcp_server.py:21565", "anchor": "gitea_mcp_server.py:21581",
"expect": "def _check_mcp_runtimes_diagnostics" "expect": "def _check_mcp_runtimes_diagnostics"
}, },
{ {
"anchor": "gitea_mcp_server.py:21585", "anchor": "gitea_mcp_server.py:21601",
"expect": "\"ps\", \"-o\", \"pid,lstart,command\"" "expect": "\"ps\", \"-o\", \"pid,lstart,command\""
}, },
{ {
"anchor": "gitea_mcp_server.py:21629", "anchor": "gitea_mcp_server.py:21645",
"expect": "\"ps\", \"eww\"" "expect": "\"ps\", \"eww\""
}, },
{ {
@@ -107,19 +107,19 @@
"expect": "def assert_keychain_access_allowed" "expect": "def assert_keychain_access_allowed"
}, },
{ {
"anchor": "gitea_mcp_server.py:19311", "anchor": "gitea_mcp_server.py:19327",
"expect": "def gitea_list_profiles" "expect": "def gitea_list_profiles"
}, },
{ {
"anchor": "gitea_mcp_server.py:19362", "anchor": "gitea_mcp_server.py:19378",
"expect": "gitea_config.resolve_token(p)" "expect": "gitea_config.resolve_token(p)"
}, },
{ {
"anchor": "gitea_mcp_server.py:19675", "anchor": "gitea_mcp_server.py:19691",
"expect": "def gitea_audit_config" "expect": "def gitea_audit_config"
}, },
{ {
"anchor": "gitea_mcp_server.py:19697", "anchor": "gitea_mcp_server.py:19713",
"expect": "service_summaries(config)" "expect": "service_summaries(config)"
}, },
{ {
@@ -135,19 +135,19 @@
"expect": "_keychain_token(auth.get(\"id\"))" "expect": "_keychain_token(auth.get(\"id\"))"
}, },
{ {
"anchor": "gitea_mcp_server.py:17760", "anchor": "gitea_mcp_server.py:17776",
"expect": "\"jenkins-mcp\"" "expect": "\"jenkins-mcp\""
}, },
{ {
"anchor": "gitea_mcp_server.py:17766", "anchor": "gitea_mcp_server.py:17782",
"expect": "external-mcp" "expect": "external-mcp"
}, },
{ {
"anchor": "gitea_mcp_server.py:17787", "anchor": "gitea_mcp_server.py:17803",
"expect": "\"glitchtip-mcp\"" "expect": "\"glitchtip-mcp\""
}, },
{ {
"anchor": "gitea_mcp_server.py:17792", "anchor": "gitea_mcp_server.py:17808",
"expect": "external-mcp" "expect": "external-mcp"
}, },
{ {
@@ -183,7 +183,7 @@
"expect": "mutation_safe" "expect": "mutation_safe"
}, },
{ {
"anchor": "gitea_mcp_server.py:19155", "anchor": "gitea_mcp_server.py:19171",
"expect": "def gitea_assess_master_parity" "expect": "def gitea_assess_master_parity"
}, },
{ {
@@ -199,7 +199,7 @@
"expect": "/tmp/gitea_issue_lock.json" "expect": "/tmp/gitea_issue_lock.json"
}, },
{ {
"anchor": "gitea_mcp_server.py:10940", "anchor": "gitea_mcp_server.py:10956",
"expect": "def gitea_bootstrap_author_issue_worktree" "expect": "def gitea_bootstrap_author_issue_worktree"
}, },
{ {
@@ -239,7 +239,7 @@
"expect": "os.getpid()" "expect": "os.getpid()"
}, },
{ {
"anchor": "gitea_mcp_server.py:12854", "anchor": "gitea_mcp_server.py:12870",
"expect": "owner_pid_alive" "expect": "owner_pid_alive"
} }
] ]
+28 -26
View File
@@ -5,9 +5,11 @@ What the adversary is, what each boundary protects, and which services may share
- **Issue:** #956 (Remote-MCP threat model), child of epic #929, cross-linked to #955. - **Issue:** #956 (Remote-MCP threat model), child of epic #929, cross-linked to #955.
- **Depends on:** #930 (closed) — `docs/remote-mcp/coupling-inventory.md`. - **Depends on:** #930 (closed) — `docs/remote-mcp/coupling-inventory.md`.
- **Blocks:** #932, #933, #934, #938. - **Blocks:** #932, #933, #934, #938.
- **Generated against commit:** `a143cd065ba06e1a2bdc5143a19ec156e53650ef` (#931's transport - **Generated against commit:** `ca5f078d8a575ea3e2991771f8b4ea85e3dcaaa0` (#708's
bind seam). Originally generated against `aad5c8b42361d380a8eeb07b94b90815e594c2c5` namespace-attachment gate). Originally generated against
(`master`) and re-anchored when #931 shifted the cited lines. `aad5c8b42361d380a8eeb07b94b90815e594c2c5` (`master`), re-anchored at
`a143cd065ba06e1a2bdc5143a19ec156e53650ef` when #931's transport bind seam shifted the
cited lines, and re-anchored again when #708 shifted them further.
- **Scope:** documentation only. This child changes no server behavior. It adds one - **Scope:** documentation only. This child changes no server behavior. It adds one
document, one anchor fixture, and the test that enforces them. document, one anchor fixture, and the test that enforces them.
@@ -29,7 +31,7 @@ document cites an anchor the fixture does not cover.
This guard exists because #930 did not have one. Its inventory was generated at This guard exists because #930 did not have one. Its inventory was generated at
`7bf4f125`; by `aad5c8b4` its `gitea_mcp_server.py` anchors had drifted — the transport `7bf4f125`; by `aad5c8b4` its `gitea_mcp_server.py` anchors had drifted — the transport
bind it cited at line 23750 now lives at `gitea_mcp_server.py:24848`, and its bind it cited at line 23750 now lives at `gitea_mcp_server.py:24864`, and its
client-managed provenance anchor at 14588 now lands in an unrelated function. Nothing client-managed provenance anchor at 14588 now lands in an unrelated function. Nothing
failed, because nothing checked. Anchors into a ~24,700-line module rot silently, and a failed, because nothing checked. Anchors into a ~24,700-line module rot silently, and a
security document that cannot prove its own citations is worse than none, because it is security document that cannot prove its own citations is worse than none, because it is
@@ -73,19 +75,19 @@ authenticate the *caller*, not the *intent*.
## 3. Trust boundaries ## 3. Trust boundaries
"Crossing requires today" is what the code actually enforces at "Crossing requires today" is what the code actually enforces at
`a143cd065ba06e1a2bdc5143a19ec156e53650ef`, not what the design intends. `ca5f078d8a575ea3e2991771f8b4ea85e3dcaaa0`, not what the design intends.
| ID | Boundary | Protects | Crossing requires today | Crossing must require remotely | | ID | Boundary | Protects | Crossing requires today | Crossing must require remotely |
| -- | -------- | -------- | ----------------------- | ------------------------------ | | -- | -------- | -------- | ----------------------- | ------------------------------ |
| B1 | LLM client ↔ MCP server session | A1, A3, A10 — that a mutating session was established through the sanctioned client path | A single configured bind (`gitea_mcp_server.py:24848`) validated against one closed allowlist (`mcp_daemon_guard.py:49`, `mcp_daemon_guard.py:195`) — since #931 the identifier comes from deployment configuration and defaults to the local transport, so the boundary no longer rests on a literal, but it still rests on the *bind* rather than on an authenticated caller; client-managed provenance (`gitea_mcp_server.py:15465`) or a refusal (`gitea_mcp_server.py:15503`); production transport before recovery-authorization mint (`irrecoverable_provenance.py:497`, consumed at `gitea_mcp_server.py:9175` and `gitea_mcp_server.py:9424`) | An authenticated handshake issuing a server-side session identity bound to a principal, with the transport recorded in provenance. The physical proof (a pipe) must become a cryptographic one. | | B1 | LLM client ↔ MCP server session | A1, A3, A10 — that a mutating session was established through the sanctioned client path | A single configured bind (`gitea_mcp_server.py:24864`) validated against one closed allowlist (`mcp_daemon_guard.py:49`, `mcp_daemon_guard.py:195`) — since #931 the identifier comes from deployment configuration and defaults to the local transport, so the boundary no longer rests on a literal, but it still rests on the *bind* rather than on an authenticated caller; client-managed provenance (`gitea_mcp_server.py:15481`) or a refusal (`gitea_mcp_server.py:15519`); production transport before recovery-authorization mint (`irrecoverable_provenance.py:497`, consumed at `gitea_mcp_server.py:9191` and `gitea_mcp_server.py:9440`) | An authenticated handshake issuing a server-side session identity bound to a principal, with the transport recorded in provenance. The physical proof (a pipe) must become a cryptographic one. |
| B2 | Role ↔ role | A9 — that author, reviewer, merger, and reconciler are distinct authorities | **The process boundary only.** The role is a property of the process, read once from `GITEA_MCP_PROFILE` (`gitea_config.py:54`). A caller gets author permissions by connecting to the author process. Review and merge are the operations singled out for extra care (`gitea_config.py:97`) | A per-request principal, so the role follows from the credential presented and cannot be selected by reaching a different endpoint. | | B2 | Role ↔ role | A9 — that author, reviewer, merger, and reconciler are distinct authorities | **The process boundary only.** The role is a property of the process, read once from `GITEA_MCP_PROFILE` (`gitea_config.py:54`). A caller gets author permissions by connecting to the author process. Review and merge are the operations singled out for extra care (`gitea_config.py:97`) | A per-request principal, so the role follows from the credential presented and cannot be selected by reaching a different endpoint. |
| B3 | MCP server ↔ credential store | A3, A8 — that only sanctioned code turns a profile into a token | `_keychain_token` shelling out to the login keychain (`gitea_config.py:956`), dispatched by `resolve_token` (`gitea_config.py:974`) with the reference type built at `gitea_config.py:1015`, gated by `assert_keychain_access_allowed` (`mcp_daemon_guard.py:583`). Inline secrets are rejected at config load (`gitea_config.py:294`) | A credential provider keyed by the *request* principal, returning only that principal's credential, with the source recorded and the value never returned. | | B3 | MCP server ↔ credential store | A3, A8 — that only sanctioned code turns a profile into a token | `_keychain_token` shelling out to the login keychain (`gitea_config.py:956`), dispatched by `resolve_token` (`gitea_config.py:974`) with the reference type built at `gitea_config.py:1015`, gated by `assert_keychain_access_allowed` (`mcp_daemon_guard.py:583`). Inline secrets are rejected at config load (`gitea_config.py:294`) | A credential provider keyed by the *request* principal, returning only that principal's credential, with the source recorded and the value never returned. |
| B4 | MCP server ↔ Gitea | A1, A2 — that only authorized calls reach the forge | A bearer token over TLS. Server-side, nothing distinguishes one role's token from another beyond the account it belongs to | Unchanged at the forge; the endpoint in front of it must refuse unauthenticated and plaintext connections before tool dispatch. | | B4 | MCP server ↔ Gitea | A1, A2 — that only authorized calls reach the forge | A bearer token over TLS. Server-side, nothing distinguishes one role's token from another beyond the account it belongs to | Unchanged at the forge; the endpoint in front of it must refuse unauthenticated and plaintext connections before tool dispatch. |
| B5 | MCP server ↔ caller's filesystem | A7 — that a tool acts on the *caller's* disk or refuses | Nothing. The server's disk *is* the caller's disk. Worktree bootstrap writes directly (`gitea_mcp_server.py:10940`); the active workspace is process-global (`gitea_mcp_server.py:193`, `gitea_mcp_server.py:194`) | An explicit per-tool classification, enforced at dispatch, refusing filesystem tools over a transport that cannot reach the caller's disk. A green verdict about the wrong disk is the failure to prevent. | | B5 | MCP server ↔ caller's filesystem | A7 — that a tool acts on the *caller's* disk or refuses | Nothing. The server's disk *is* the caller's disk. Worktree bootstrap writes directly (`gitea_mcp_server.py:10956`); the active workspace is process-global (`gitea_mcp_server.py:193`, `gitea_mcp_server.py:194`) | An explicit per-tool classification, enforced at dispatch, refusing filesystem tools over a transport that cannot reach the caller's disk. A green verdict about the wrong disk is the failure to prevent. |
| B6 | MCP server ↔ coordination state | A6, A9 — mutual exclusion | Local files and a local SQLite database, with liveness judged from the local process table (`issue_lock_store.py:98`), keyed on paths under one user's home (`issue_lock_store.py:26`, `mcp_session_state.py:27`, `control_plane_db.py:47`) and on `os.getpid()` (`control_plane_db.py:1145`, `gitea_mcp_server.py:12854`). A legacy global slot still exists at `gitea_mcp_server.py:2351`, and the session-pointer file is named per PID (`issue_lock_store.py:83`) | One authority per ownership question, with liveness from session identity and expiry, and atomic acquire, renew, and release across hosts. | | B6 | MCP server ↔ coordination state | A6, A9 — mutual exclusion | Local files and a local SQLite database, with liveness judged from the local process table (`issue_lock_store.py:98`), keyed on paths under one user's home (`issue_lock_store.py:26`, `mcp_session_state.py:27`, `control_plane_db.py:47`) and on `os.getpid()` (`control_plane_db.py:1145`, `gitea_mcp_server.py:12870`). A legacy global slot still exists at `gitea_mcp_server.py:2351`, and the session-pointer file is named per PID (`issue_lock_store.py:83`) | One authority per ownership question, with liveness from session identity and expiry, and atomic acquire, renew, and release across hosts. |
| B7 | Gitea integration ↔ unrelated integrations | A4, A5 — that a Gitea compromise is not a CI and observability compromise | **Nothing.** See §5. The Gitea server reads Jenkins and GlitchTip secrets (`gitea_config.py:851`, reached from `gitea_config.py:837`) and holds the Sentry token (`sentry_incident_bridge.py:190`) | A hard process boundary. This is the boundary #956 exists to create. | | B7 | Gitea integration ↔ unrelated integrations | A4, A5 — that a Gitea compromise is not a CI and observability compromise | **Nothing.** See §5. The Gitea server reads Jenkins and GlitchTip secrets (`gitea_config.py:851`, reached from `gitea_config.py:837`) and holds the Sentry token (`sentry_incident_bridge.py:190`) | A hard process boundary. This is the boundary #956 exists to create. |
| B8 | Tenant ↔ tenant (`prgs` / `mdcps` / `local-lab`) | A2 — that one organization's compromise is not another's | Convention. One configuration declares all three contexts; `resolve_service` fails closed on a *disabled* context (`gitea_config.py:704`) but the credentials of enabled ones remain reachable in-process. A per-profile repository scope exists (`gitea_config.py:499`) | Separate deployments, or at minimum per-tenant credential scopes with no process able to resolve both. | | B8 | Tenant ↔ tenant (`prgs` / `mdcps` / `local-lab`) | A2 — that one organization's compromise is not another's | Convention. One configuration declares all three contexts; `resolve_service` fails closed on a *disabled* context (`gitea_config.py:704`) but the credentials of enabled ones remain reachable in-process. A per-profile repository scope exists (`gitea_config.py:499`) | Separate deployments, or at minimum per-tenant credential scopes with no process able to resolve both. |
| B9 | Deployed code ↔ merged policy | A1, A10 — that the running server enforces the rules that were actually merged | Comparing this process's startup commit against this disk (`master_parity_gate.py:168`), conjoined into a single verdict (`master_parity_gate.py:255`) published by `gitea_mcp_server.py:19155` | Freshness defined against the deployed build identity, with an explicit fail-closed verdict when undeterminable. | | B9 | Deployed code ↔ merged policy | A1, A10 — that the running server enforces the rules that were actually merged | Comparing this process's startup commit against this disk (`master_parity_gate.py:168`), conjoined into a single verdict (`master_parity_gate.py:255`) published by `gitea_mcp_server.py:19171` | Freshness defined against the deployed build identity, with an explicit fail-closed verdict when undeterminable. |
### What no boundary constrains ### What no boundary constrains
@@ -130,10 +132,10 @@ Two flows deserve attention because neither is obvious from the code:
1. **The keychain flow fans out.** B3 is drawn once but resolves credentials for *every* 1. **The keychain flow fans out.** B3 is drawn once but resolves credentials for *every*
configured profile and service, not only the active one. `gitea_list_profiles` configured profile and service, not only the active one. `gitea_list_profiles`
(`gitea_mcp_server.py:19311`) reports each profile's credential status by calling (`gitea_mcp_server.py:19327`) reports each profile's credential status by calling
`resolve_token` on it (`gitea_mcp_server.py:19362`), and `gitea_audit_config` `resolve_token` on it (`gitea_mcp_server.py:19378`), and `gitea_audit_config`
(`gitea_mcp_server.py:19675`) reports service credential status through (`gitea_mcp_server.py:19691`) reports service credential status through
`service_summaries` (`gitea_mcp_server.py:19697`). `service_summaries` (`gitea_mcp_server.py:19713`).
2. **The return path is a flow too.** Content read from Gitea travels back into the model 2. **The return path is a flow too.** Content read from Gitea travels back into the model
and is treated as instruction. This is the ADV2 edge, and it is the only edge in the and is treated as instruction. This is the ADV2 edge, and it is the only edge in the
diagram with no authentication on it, because it is not a request. diagram with no authentication on it, because it is not a request.
@@ -178,16 +180,16 @@ the credential, and an attacker holding the token does not call our tools.
**Finding 3 — Any one role process can resolve every other role's credential.** This is not **Finding 3 — Any one role process can resolve every other role's credential.** This is not
inferred; it is demonstrated by tool output. `gitea_list_profiles` inferred; it is demonstrated by tool output. `gitea_list_profiles`
(`gitea_mcp_server.py:19311`) called from the **author** session reports (`gitea_mcp_server.py:19327`) called from the **author** session reports
`identity_status: "credentials present"` for `prgs-merger`, `prgs-reviewer`, `identity_status: "credentials present"` for `prgs-merger`, `prgs-reviewer`,
`prgs-reconciler`, and every `mdcps` profile, because it calls `resolve_token` on each one `prgs-reconciler`, and every `mdcps` profile, because it calls `resolve_token` on each one
(`gitea_mcp_server.py:19362`). The author process does not merely *have access to* the (`gitea_mcp_server.py:19378`). The author process does not merely *have access to* the
merger's credential — it reads it to answer a status query. B2 is not a credential boundary merger's credential — it reads it to answer a status query. B2 is not a credential boundary
in either direction. in either direction.
**Finding 4 — The Gitea server reads CI and observability secrets.** `gitea_audit_config` **Finding 4 — The Gitea server reads CI and observability secrets.** `gitea_audit_config`
(`gitea_mcp_server.py:19675`) reports `MDCPS Jenkins: enabled, read-only, authenticated`. (`gitea_mcp_server.py:19691`) reports `MDCPS Jenkins: enabled, read-only, authenticated`.
That word `authenticated` is produced by `service_summaries` (`gitea_mcp_server.py:19697`, That word `authenticated` is produced by `service_summaries` (`gitea_mcp_server.py:19713`,
defined at `gitea_config.py:837`), whose default check calls `_keychain_token` on the defined at `gitea_config.py:837`), whose default check calls `_keychain_token` on the
service's own keychain reference (`gitea_config.py:851`). Producing that one line requires service's own keychain reference (`gitea_config.py:851`). Producing that one line requires
the Gitea MCP server to read the Jenkins secret and the GlitchTip secret out of the the Gitea MCP server to read the Jenkins secret and the GlitchTip secret out of the
@@ -195,8 +197,8 @@ keychain. B7 does not exist.
**Finding 5 — Jenkins and GlitchTip are already decomposed; the reach is residual.** Their **Finding 5 — Jenkins and GlitchTip are already decomposed; the reach is residual.** Their
tools live in separately registered servers, marked `external-mcp` tools live in separately registered servers, marked `external-mcp`
(`gitea_mcp_server.py:17760`, `gitea_mcp_server.py:17766`, `gitea_mcp_server.py:17787`, (`gitea_mcp_server.py:17776`, `gitea_mcp_server.py:17782`, `gitea_mcp_server.py:17803`,
`gitea_mcp_server.py:17792`) with their own expected tool sets (`mcp_discoverability.py:9`, `gitea_mcp_server.py:17808`) with their own expected tool sets (`mcp_discoverability.py:9`,
`mcp_discoverability.py:17`). The correct decomposition was already chosen. What remains is `mcp_discoverability.py:17`). The correct decomposition was already chosen. What remains is
a leak across it: the credential *references* still live in the Gitea configuration and are a leak across it: the credential *references* still live in the Gitea configuration and are
still resolved by the Gitea process. #75 bundled these services into one control-plane still resolved by the Gitea process. #75 bundled these services into one control-plane
@@ -207,8 +209,8 @@ GlitchTip, the Sentry bridge runs *inside* the Gitea server, resolving its token
process environment (`sentry_incident_bridge.py:190`) and sending it as a bearer header process environment (`sentry_incident_bridge.py:190`) and sending it as a bearer header
(`sentry_incident_bridge.py:289`). Being an environment variable rather than a keychain item (`sentry_incident_bridge.py:289`). Being an environment variable rather than a keychain item
makes it strictly worse: it needs no keychain prompt and is inherited by every subprocess the makes it strictly worse: it needs no keychain prompt and is inherited by every subprocess the
server spawns — including the `ps` invocations at `gitea_mcp_server.py:21585` and server spawns — including the `ps` invocations at `gitea_mcp_server.py:21601` and
`gitea_mcp_server.py:21629`, reached from `gitea_mcp_server.py:21565`. `gitea_mcp_server.py:21645`, reached from `gitea_mcp_server.py:21581`.
**Finding 7 — The highest-value coordination asset has the weakest gate.** A6 is protected **Finding 7 — The highest-value coordination asset has the weakest gate.** A6 is protected
by filesystem permissions alone (CR14). Corrupting a lease requires no Gitea credential, by filesystem permissions alone (CR14). Corrupting a lease requires no Gitea credential,
@@ -217,8 +219,8 @@ assumes. Every other asset costs an attacker a credential; this one costs nothin
local access, which is exactly ADV5's position. local access, which is exactly ADV5's position.
**Finding 8 — Provenance authenticates the launch, not the caller.** `server_provenance` is **Finding 8 — Provenance authenticates the launch, not the caller.** `server_provenance` is
reported as exactly `client_managed` or `manual_launch` (`gitea_mcp_server.py:19054`), reported as exactly `client_managed` or `manual_launch` (`gitea_mcp_server.py:19070`),
derived from environment inspection (`gitea_mcp_server.py:15465`) with the recognized-key derived from environment inspection (`gitea_mcp_server.py:15481`) with the recognized-key
allowlist at `gitea_config.py:1172` and the generator that emits the marker at allowlist at `gitea_config.py:1172` and the generator that emits the marker at
`gitea_config.py:1233`. Every one of those facts is fixed at process start. A client that is `gitea_config.py:1233`. Every one of those facts is fixed at process start. A client that is
trustworthy at launch and compromised a minute later remains `client_managed` for the life trustworthy at launch and compromised a minute later remains `client_managed` for the life
@@ -257,7 +259,7 @@ holds the token and calls the API instead of the tool.
**D3 — Credential resolution is scoped to the request principal.** A session must resolve its **D3 — Credential resolution is scoped to the request principal.** A session must resolve its
own credential and must have no path to any other principal's. The resolve-every-profile own credential and must have no path to any other principal's. The resolve-every-profile
behavior behind `gitea_mcp_server.py:19362` and `gitea_mcp_server.py:19697` must report behavior behind `gitea_mcp_server.py:19378` and `gitea_mcp_server.py:19713` must report
configured-or-not from configuration alone, without resolving the secret. configured-or-not from configuration alone, without resolving the secret.
*Rationale.* Finding 3. An audit surface that proves a credential exists by fetching it is a *Rationale.* Finding 3. An audit surface that proves a credential exists by fetching it is a
@@ -334,11 +336,11 @@ The client is attached to the local fleet over stdio.
| Boundary | What ADV1 reaches | Stopped by | | Boundary | What ADV1 reaches | Stopped by |
| -------- | ----------------- | ---------- | | -------- | ----------------- | ---------- |
| B1 | Everything the fleet serves. The client *is* the sanctioned launcher: it satisfies the client-managed check (`gitea_mcp_server.py:15465`) by construction, and provenance is never re-verified after launch (Finding 8). | Nothing. The guard authenticates the launch, not the caller. | | B1 | Everything the fleet serves. The client *is* the sanctioned launcher: it satisfies the client-managed check (`gitea_mcp_server.py:15481`) by construction, and provenance is never re-verified after launch (Finding 8). | Nothing. The guard authenticates the launch, not the caller. |
| B2 | All five roles — it is attached to all five namespaces. It can author a PR, approve it from the reviewer namespace, and merge it from the merger namespace. | Only the in-process self-review check, which compares `jcwalker3` (author) against `sysadmin` (reviewer) and **passes**, because Finding 1 made them different accounts while leaving reviewer and merger identical. A9 falls in one sequence of legitimate calls. | | B2 | All five roles — it is attached to all five namespaces. It can author a PR, approve it from the reviewer namespace, and merge it from the merger namespace. | Only the in-process self-review check, which compares `jcwalker3` (author) against `sysadmin` (reviewer) and **passes**, because Finding 1 made them different accounts while leaving reviewer and merger identical. A9 falls in one sequence of legitimate calls. |
| B3 | Every credential in CR1CR10 via CR13, with no additional prompt — the daemon is already sanctioned, so `assert_keychain_access_allowed` (`mcp_daemon_guard.py:583`) returns immediately. | Nothing. | | B3 | Every credential in CR1CR10 via CR13, with no additional prompt — the daemon is already sanctioned, so `assert_keychain_access_allowed` (`mcp_daemon_guard.py:583`) returns immediately. | Nothing. |
| B4 | A1 and A2 in full. | Branch protection at the forge, to the extent configured. | | B4 | A1 and A2 in full. | Branch protection at the forge, to the extent configured. |
| B5 | The operator's checkout and every worktree, through the author tools (`gitea_mcp_server.py:10940`), plus the shared stderr path at `mcp_server.py:13`. | Nothing; the server's disk is the target disk. | | B5 | The operator's checkout and every worktree, through the author tools (`gitea_mcp_server.py:10956`), plus the shared stderr path at `mcp_server.py:13`. | Nothing; the server's disk is the target disk. |
| B6 | All coordination state — no credential required (CR14). It can forge lease ownership and clear decision locks. | Filesystem permissions, which it already satisfies. | | B6 | All coordination state — no credential required (CR14). It can forge lease ownership and clear decision locks. | Filesystem permissions, which it already satisfies. |
| B7 | Jenkins (A4) and GlitchTip (A5) secrets via Finding 4, and CR11/CR12 from its own environment. | Nothing. | | B7 | Jenkins (A4) and GlitchTip (A5) secrets via Finding 4, and CR11/CR12 from its own environment. | Nothing. |
| B8 | Both tenants. | Nothing in-process; only the disabled-context check (`gitea_config.py:704`), which does not apply to enabled contexts. | | B8 | Both tenants. | Nothing in-process; only the disabled-context check (`gitea_config.py:704`), which does not apply to enabled contexts. |