Skip to content

fix(fe): gate read-only access UI behind a feature flag (default off) - #4087

Merged
basvandijk merged 2 commits into
mainfrom
claude/gate-readonly-ui
Jul 3, 2026
Merged

basvandijk merged 2 commits into
mainfrom
claude/gate-readonly-ui

Conversation

@aterga

@aterga aterga commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Read-only mode (#4016) is broken end-to-end with today's agent stack. A read-only ("queries-only") delegation is a canister signature over a representation-independent hash that includes permissions = "queries". No released agent (ic-agent / ic-transport-types, checked through 0.47.3; @icp-sdk/core) has a permissions field on its Delegation wire type, and there is no #[serde(flatten)] catch-all — so the field is dropped when the delegation is re-serialized into the request envelope. The replica then recomputes the hash over {pubkey, expiration, targets} (no permissions), which the II canister never signed, and canister-signature verification fails:

invalid delegation chain: A canister signature in the delegation chain
could not be verified: sig not found in the signature tree

This surfaced as MCP connections in read-only mode failing: the read-only grant makes the MCP server mint queries-only per-app delegations, which its agent can't use. The same fails-closed applies to read-only /continue and /cli sessions.

Rather than revert the feature, gate the UI off until the agent stack preserves the delegation permissions field (agent-side field preservation is enough to un-break verification; the field doesn't need to be semantically interpreted by the agent — enforcement stays the replica's job).

Changes

  • New READ_ONLY_MODE feature flag in src/frontend/src/lib/state/featureFlags.ts, off by default everywhere (including id.ai / beta.id.ai — no domain override). Documented with the reason and the flip condition.
  • AccessLevelToggle is hidden in all three flows (ContinueView, CliAuthorizeView, McpAuthorizeView) when the flag is off.
  • Each flow computes an effectiveAccessLevel that is forced to full-access while the flag is off. This is the load-bearing part: /cli and /mcp default their (now-hidden) toggle to read-only, so simply hiding it would otherwise leave them minting the unusable queries-only delegations. With the flag off, every flow sends full access.
  • e2e assertions updated to verify the toggle is absent by default (toHaveCount(0)).

The backend capability, the candid Permissions type, and all delegation tests from #4016 are unchanged and remain in place — re-enabling read-only is a one-line flip of the flag default once agents preserve the field.

Tests

  • tsc --noEmit / svelte-check: 0 errors.
  • eslint, prettier: clean.
  • Frontend unit tests pass (18), including the toPermissionsArg mapping and the /cli + /mcp authorize-helper argument tests (unaffected — they exercise the mapping directly, below the flag).
  • e2e specs (cli, mcp, authorize) updated to assert the access-level toggle is not rendered while the flag is off.

🤖 Generated with Claude Code

https://claude.ai/code/session_018gHRjyFYqumMxAMwbPRLuN


Generated by Claude Code

A read-only ("queries-only") delegation is a canister signature over a
message that includes `permissions = "queries"`. No released agent stack
(`ic-agent` / `ic-transport-types` / `@icp-sdk/core`) round-trips that
field: it is dropped when the delegation is re-serialized into the request
envelope, so the replica recomputes a different representation-independent
hash and canister-signature verification fails with "sig not found in the
signature tree". This broke read-only MCP connections (and would break any
read-only dapp/CLI session), since the delegation can't be used by current
agents.

Gate the access-level UI behind a new `READ_ONLY_MODE` feature flag, off by
default everywhere (including id.ai / beta.id.ai) until the agent stack
preserves the delegation `permissions` field:

- `AccessLevelToggle` is hidden in all three flows (`/continue`, `/cli`,
  `/mcp`) when the flag is off.
- Each flow sends an `effectiveAccessLevel` that is forced to `full-access`
  while the flag is off — so hiding the toggle can't leave `/cli` or `/mcp`
  (which default to read-only) silently minting unusable delegations.
- e2e assertions updated to verify the toggle is absent by default.

The backend capability, candid `Permissions` type, and all delegation tests
are unchanged and stay in place; re-enabling read-only is a one-line flip of
the flag default once agents support the field.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR disables the read-only (“queries-only”) authorization UI by default via a new frontend feature flag, to avoid minting delegations that currently fail verification end-to-end because agent stacks drop the permissions="queries" field when re-serializing delegations.

Changes:

  • Added a READ_ONLY_MODE feature flag (default off) with in-code documentation explaining the current agent incompatibility and the condition for re-enabling.
  • Hid AccessLevelToggle in the /continue, /cli, and /mcp flows when the flag is off, and forced an effectiveAccessLevel of full-access in those flows to avoid minting unusable queries-only delegations.
  • Updated Playwright e2e specs to assert the access-level toggle is not rendered by default.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/frontend/src/lib/state/featureFlags.ts Introduces READ_ONLY_MODE flag (default off) and documents the rationale/flip condition.
src/frontend/src/routes/(new-styling)/authorize/views/ContinueView.svelte Hides the access-level toggle behind the flag and forces full access when disabled.
src/frontend/src/routes/(new-styling)/cli/views/CliAuthorizeView.svelte Hides the toggle behind the flag and ensures CLI authorizations are full access when disabled.
src/frontend/src/routes/(new-styling)/mcp/views/McpAuthorizeView.svelte Hides the toggle behind the flag and ensures MCP connections request full access when disabled.
src/frontend/tests/e2e-playwright/routes/authorize/index.spec.ts Asserts the read-only toggle is absent by default in the authorization flow.
src/frontend/tests/e2e-playwright/routes/cli.spec.ts Asserts the CLI “Full access” toggle is absent by default.
src/frontend/tests/e2e-playwright/routes/mcp.spec.ts Asserts the MCP “Read-only mode” toggle is absent by default.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Address Copilot review: the `accessLevel` default comments described the
read-only / full-access defaults without noting they only apply when
READ_ONLY_MODE is enabled. While the flag is off (the current default) the
toggle is hidden and `effectiveAccessLevel` forces full access, so those
defaults are unreachable. Clarified each comment accordingly. No behavior
change.
@aterga
aterga marked this pull request as ready for review July 3, 2026 17:09
@aterga
aterga requested a review from a team as a code owner July 3, 2026 17:09
@zeropath-ai

zeropath-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown

✅ No security or compliance issues detected. Reviewed everything up to 185018e.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► src/frontend/src/lib/state/featureFlags.ts
      Add READ_ONLY_MODE feature flag
► src/frontend/src/routes/(new-styling)/authorize/views/ContinueView.svelte
      Use READ_ONLY_MODE to control read-only UX and effective access level
► src/frontend/src/routes/(new-styling)/cli/views/CliAuthorizeView.svelte
      Use READ_ONLY_MODE to control read-only UX and effective access level
► src/frontend/src/routes/(new-styling)/mcp/views/McpAuthorizeView.svelte
      Use READ_ONLY_MODE to control read-only UX and effective access level
► src/frontend/tests/e2e-playwright/routes/authorize/index.spec.ts
      Update test to reflect hidden read-only toggle when READ_ONLY_MODE is off
► src/frontend/tests/e2e-playwright/routes/cli.spec.ts
      Update test to reflect hidden read-only toggle when READ_ONLY_MODE is off
► src/frontend/tests/e2e-playwright/routes/mcp.spec.ts
      Update test to reflect hidden read-only toggle when READ_ONLY_MODE is off

@aterga
aterga enabled auto-merge (squash) July 3, 2026 17:22
@basvandijk
basvandijk disabled auto-merge July 3, 2026 18:17
@aterga
aterga enabled auto-merge (squash) July 3, 2026 18:18
@aterga
aterga disabled auto-merge July 3, 2026 18:18
@basvandijk
basvandijk merged commit 92c17b4 into main Jul 3, 2026
47 checks passed
@basvandijk
basvandijk deleted the claude/gate-readonly-ui branch July 3, 2026 18:18
aterga pushed a commit that referenced this pull request Jul 4, 2026
…p-delegation-refactor-x5b6ek

Conflicts were in the two files #4087 and this branch both touched:

- McpAuthorizeView.svelte: adopt main's feature-flag gating. The access-level
  toggle now renders only under `{#if $READ_ONLY_MODE}` (default off) and
  `effectiveAccessLevel` forces "full-access" while the flag is off — so a
  connect never sends read-only ("queries") permissions until the flag is on,
  which is the intended behavior (no released agent round-trips a queries-only
  delegation yet). This supersedes this branch's prompt="full-access" tweak.
  Kept this branch's 30-day TTL preset/range (the MCP grant cap is 30 days).

- mcp.spec.ts: keep this branch's session-key-model tests ("registers the
  server's session key", plus the finish_url test) and apply #4087's flag-off
  expectation — the toggle is hidden (toHaveCount(0)) and the completion POST
  reports permissions "all". The read-only→"queries" mapping stays covered by
  the utils unit tests, which drive mcpAuthorize directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbjCCqTjsMn57fczTNGaKf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants