MCP read-scope mutation probe reaches the one-use profile route #1076

Open
opened 2026-10-04 23:07:23 +00:00 by kayg · 3 comments
Owner

The corrected MCP transport reaches the one-use App Password profile route with a read-scoped credential.

Merge-round #867 keeps the original requirement that every registered mutation returns exact HTTP 403 for an MCP read credential. The current registry marks download_app_password_profile as GET, read_only: false, authority: credential, with no declared scopes. With an inert absent token, the live tool returns a typed HTTP 404 instead of that scope denial. No valid profile token was used, and no accepted unauthorized write or secret disclosure is claimed.

Evidence: artifacts/merge-round-7c2/mcp-complete-all.log, head 233be4853. The probe now sends the required MCP method/name headers and checks typed failures. The full loop also exceeded its retained request budget and returned 429 for later scope checks. The probe will pace its individual denial checks below that budget; it does not change server quotas or expected status codes. The separate 48-call protocol storm remains.

Required review: decide and enforce how registry read_only: false applies to a side-effecting GET that has a separate one-use URL credential. Keep direct profile download's public one-use-token behavior and unknown-token 404. Keep App Password authority boundaries. Add a valid disposable profile fixture and test MCP read denial without using third-party credentials. The merge round leaves the exact assertion failed pending this contract decision.

The corrected MCP transport reaches the one-use App Password profile route with a read-scoped credential. Merge-round #867 keeps the original requirement that every registered mutation returns exact HTTP 403 for an MCP read credential. The current registry marks `download_app_password_profile` as GET, `read_only: false`, `authority: credential`, with no declared scopes. With an inert absent token, the live tool returns a typed HTTP 404 instead of that scope denial. No valid profile token was used, and no accepted unauthorized write or secret disclosure is claimed. Evidence: `artifacts/merge-round-7c2/mcp-complete-all.log`, head 233be4853. The probe now sends the required MCP method/name headers and checks typed failures. The full loop also exceeded its retained request budget and returned 429 for later scope checks. The probe will pace its individual denial checks below that budget; it does not change server quotas or expected status codes. The separate 48-call protocol storm remains. Required review: decide and enforce how registry `read_only: false` applies to a side-effecting GET that has a separate one-use URL credential. Keep direct profile download's public one-use-token behavior and unknown-token 404. Keep App Password authority boundaries. Add a valid disposable profile fixture and test MCP read denial without using third-party credentials. The merge round leaves the exact assertion failed pending this contract decision.
Author
Owner

Root cause: session_context treats profile delivery and OIDC callbacks as public before it validates an App Password or an inner McpDispatch request. The fix keeps anonymous capability delivery and sends credential requests through the existing auth allowlist (App Passwords cannot manage auth). GET actions marked read_only:false now use write intent. Added a disposable valid profile regression shared by the authorization matrix and MCP probe; denial must leave the link unspent, then public delivery works exactly once. Audit: setup/register/recovery/reenrol and profile creation/update already meet the auth allowlist; the early public exception was the gap.

Root cause: session_context treats profile delivery and OIDC callbacks as public before it validates an App Password or an inner McpDispatch request. The fix keeps anonymous capability delivery and sends credential requests through the existing auth allowlist (App Passwords cannot manage auth). GET actions marked read_only:false now use write intent. Added a disposable valid profile regression shared by the authorization matrix and MCP probe; denial must leave the link unspent, then public delivery works exactly once. Audit: setup/register/recovery/reenrol and profile creation/update already meet the auth allowlist; the early public exception was the gap.
Author
Owner

Round 7c3 fixes #1076 in the shared route guard. Registry mutations now require write intent even for GET and its implicit HEAD dispatch. Public profile and OIDC callback exceptions apply only outside MCP dispatch and App Password authority. Anonymous one-use profile delivery keeps its existing behavior; authenticated App Password calls keep the normal auth-route 403 boundary.

The audit found two credential GET mutations (one-use profile delivery, OIDC callback), three collaboration GET mutations, and the public edit read action marked as a mutation by the registry. Profile creation and setup/passkey/recovery/invite writes use POST and are not in the generated read-only POST list. All auth routes reject App Password authority, including full credentials. The existing MCP mutation loop covers every non-read-only registry action.

Added a shared disposable valid profile fixture for the authorization matrix and MCP probe: exact 403 denial must leave the link unspent; public delivery must then return 200 once and 404 after consumption. No capability or profile body is printed. Unit fixture tests pass (2 tests); registry GET intent regression passes. Final real-server matrix and MCP gates are pending.

Round 7c3 fixes #1076 in the shared route guard. Registry mutations now require write intent even for GET and its implicit HEAD dispatch. Public profile and OIDC callback exceptions apply only outside MCP dispatch and App Password authority. Anonymous one-use profile delivery keeps its existing behavior; authenticated App Password calls keep the normal auth-route 403 boundary. The audit found two credential GET mutations (one-use profile delivery, OIDC callback), three collaboration GET mutations, and the public edit read action marked as a mutation by the registry. Profile creation and setup/passkey/recovery/invite writes use POST and are not in the generated read-only POST list. All auth routes reject App Password authority, including full credentials. The existing MCP mutation loop covers every non-read-only registry action. Added a shared disposable valid profile fixture for the authorization matrix and MCP probe: exact 403 denial must leave the link unspent; public delivery must then return 200 once and 404 after consumption. No capability or profile body is printed. Unit fixture tests pass (2 tests); registry GET intent regression passes. Final real-server matrix and MCP gates are pending.
Author
Owner

Round 7c3 repair committed on job/merge-round-7c, head b49c7f145a. GET and implicit HEAD follow registry mutation intent. Credential-bearing profile and OIDC capability flows pass the ordinary route guard. Anonymous one-use profile delivery and read-only public sign-in options retain their behavior. Setup, profile creation, invite and recovery POST writes were reviewed; collaboration GET mutations share the same intent rule. No existing assertion was changed.

Real local server evidence, verbatim:

PASS profile authority: API read denied; public capability unspent and delivered once
PASS profile authority: MCP read denied; public capability unspent and delivered once
PASS generated MCP scope denial: 208 mutations; real Note read
authorization matrix: exit 0

The matrix covered 406 operations and 2,668 requests. Its new valid capability fixture proves exact 403 denial before one public delivery and subsequent 404. The registry GET/HEAD test and public-options compatibility case pass in server tests.

Rust gates, verbatim:

cargo fmt --check: exit 0
calternal-plugin-notes clippy: exit 0
calternal-plugin-notes test: exit 0
calternal-server clippy: exit 0
calternal-server test: exit 0

The full MCP campaign has a separate existing Money fixture failure under #1079; the scope assertions above passed. Final report and six macOS screenshots are on #867. Issues remain open.

Round 7c3 repair committed on job/merge-round-7c, head b49c7f145ae41e5fb7b3f89610060f742b83bcd6. GET and implicit HEAD follow registry mutation intent. Credential-bearing profile and OIDC capability flows pass the ordinary route guard. Anonymous one-use profile delivery and read-only public sign-in options retain their behavior. Setup, profile creation, invite and recovery POST writes were reviewed; collaboration GET mutations share the same intent rule. No existing assertion was changed. Real local server evidence, verbatim: ``` PASS profile authority: API read denied; public capability unspent and delivered once PASS profile authority: MCP read denied; public capability unspent and delivered once PASS generated MCP scope denial: 208 mutations; real Note read authorization matrix: exit 0 ``` The matrix covered 406 operations and 2,668 requests. Its new valid capability fixture proves exact 403 denial before one public delivery and subsequent 404. The registry GET/HEAD test and public-options compatibility case pass in server tests. Rust gates, verbatim: ``` cargo fmt --check: exit 0 calternal-plugin-notes clippy: exit 0 calternal-plugin-notes test: exit 0 calternal-server clippy: exit 0 calternal-server test: exit 0 ``` The full MCP campaign has a separate existing Money fixture failure under #1079; the scope assertions above passed. Final report and six macOS screenshots are on #867. Issues remain open.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#1076
No description provided.