Skip to content

Align web tool permission prompts - #242

Merged
anandh8x merged 1 commit into
mainfrom
permission-approval-ui-parity
Jun 18, 2026
Merged

Align web tool permission prompts#242
anandh8x merged 1 commit into
mainfrom
permission-approval-ui-parity

Conversation

@anandh8x

@anandh8x anandh8x commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • treat hosted web_search as an allowed read-only network discovery tool so normal searches do not stop for approval
  • add host-scoped persistent grants for web_fetch URLs so always-allow is bounded to the requested host
  • update permission prompt/transcript rendering to show network targets and preserve scope metadata through caching and session hydration

Tests

  • GOCACHE=/tmp/zero-go-cache go test ./...

Summary by CodeRabbit

Release Notes

  • New Features

    • Host-scoped permissions for network operations enable granular per-host access control
    • Enhanced permission prompts now display specific targets and offer "always for this host" options
    • Permission scope information is now preserved across transcripts and session resumption
  • Bug Fixes

    • Improved permission scope handling and matching for network tools
  • Documentation

    • Clarified permission metadata and scope behavior descriptions

Treat hosted web_search as an allowed read-only network discovery tool so normal searches do not stop on a permission prompt, while retaining sandbox network enforcement when tool-network policy is enabled.

Add host-scoped persistent grants for web_fetch URLs so an always-allow decision is bounded to the requested host instead of becoming a blanket tool-wide fetch grant.

Update permission prompt and transcript rendering to show network targets, scoped always labels, and preserved scope metadata across live rows, render caching, and session hydration.

Tested: GOCACHE=/tmp/zero-go-cache go test ./...
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: b85264eb-e6e0-4b82-af09-748aeae636a0

📥 Commits

Reviewing files that changed from the base of the PR and between 4cb1f10 and 7b95dcb.

📒 Files selected for processing (19)
  • internal/agent/loop.go
  • internal/agent/loop_test.go
  • internal/agent/permission_scope_test.go
  • internal/sandbox/engine.go
  • internal/sandbox/engine_test.go
  • internal/sandbox/grant_scope.go
  • internal/sandbox/grant_scope_test.go
  • internal/sandbox/grants.go
  • internal/tools/registry_test.go
  • internal/tools/types.go
  • internal/tools/web_search.go
  • internal/tools/web_search_test.go
  • internal/tui/model_test.go
  • internal/tui/permission_prompt_test.go
  • internal/tui/render_cache.go
  • internal/tui/rendering.go
  • internal/tui/session.go
  • internal/tui/session_test.go
  • internal/tui/transcript.go

Walkthrough

Adds a ScopeHost grant kind so web_fetch permission grants are scoped to a normalized hostname (exact match, no subdomain bleed). Changes web_search from PermissionPrompt to PermissionAllow (no per-search prompt). Propagates Scope through TUI permission events and adds network-aware target: vs scope: label helpers in the permission rendering pipeline.

Changes

Host-scoped web_fetch grants, web_search allow model, and TUI scope rendering

Layer / File(s) Summary
ScopeHost kind, DeriveScope extension, and host normalization
internal/sandbox/grant_scope.go, internal/sandbox/grants.go
Defines ScopeHost constant, extends DeriveScope to extract a normalized host from web_fetch's url argument, and adds deriveHostScope, normalizeHostScope, and resolveScopeForKind helpers. Updates Grant/GrantInput doc comments to include host semantics.
grantCovers, grants.go reconciliation, and engine.go wiring
internal/sandbox/grant_scope.go, internal/sandbox/grants.go, internal/sandbox/engine.go
Updates grantCovers to compare ScopeHost grants using normalized host strings; adds ScopeHost to normalizeScopeKind and reconcileScope; rewires Engine.Evaluate and Engine.Grant to use resolveScopeForKind instead of resolveScopeAbs.
Sandbox host-scope tests
internal/sandbox/grant_scope_test.go, internal/sandbox/engine_test.go, internal/agent/permission_scope_test.go
Extends TestDeriveScope and TestGrantCovers with ScopeHost cases; adds TestEngineGrantScopesWebFetchToHost for engine-level host matching; updates TestEvaluateExemptsNetworkToolsFromDenyByDefault for PermissionAllow; adds TestPersistPermissionGrantScopesWebFetchToHost in the agent package.
web_search permission model change
internal/tools/web_search.go, internal/tools/types.go, internal/tools/registry_test.go, internal/tools/web_search_test.go, internal/agent/loop_test.go, internal/agent/loop.go
Changes web_search Safety() from PermissionPrompt+AdvertiseInAuto to PermissionAllow+SideEffectNetwork only. Updates registry and agent tests to assert differentiated postures for web_fetch vs web_search. Revises AdvertiseInAuto doc comment.
Scope propagation through TUI permission events and render cache
internal/tui/transcript.go, internal/tui/session.go, internal/tui/render_cache.go, internal/tui/session_test.go
Populates Scope on PermissionEvent from request.Scope in transcript.go and from the payload "scope" key in session.go; includes event.Scope in permissionCacheFingerprint. Test fixtures and assertions updated accordingly.
TUI network-aware label helpers and rendering
internal/tui/rendering.go, internal/tui/permission_prompt_test.go, internal/tui/model_test.go
Adds permissionScopeLine, permissionOptionLabel, and permissionEventScopeLabel to switch between target:/scope: labels and "always for this host"/"always for this scope" wording. Updates renderPermissionRow and renderFocusedPermissionPrompt to use them. New and updated rendering tests assert network-specific label output.

Sequence Diagram(s)

sequenceDiagram
    participant Agent
    participant Engine
    participant GrantScope as grant_scope.go
    participant GrantStore as grants.go
    participant TUI

    Agent->>Engine: Evaluate(web_fetch, {url: "https://example.com/..."})
    Engine->>GrantScope: DeriveScope("web_fetch", args)
    GrantScope-->>Engine: ("example.com", ScopeHost)
    Engine->>GrantScope: resolveScopeForKind("example.com", ScopeHost, root)
    GrantScope-->>Engine: "example.com"
    Engine->>GrantStore: Lookup(reqScope="example.com")
    GrantStore-->>Engine: ActionAllow (if host grant exists)
    Engine-->>Agent: ActionAllow / ActionPrompt

    Agent->>TUI: PermissionEvent{Scope: "example.com", SideEffect: "network"}
    TUI->>TUI: permissionEventScopeLabel → "target"
    TUI->>TUI: permissionOptionLabel → "always for this host"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@anandh8x
anandh8x marked this pull request as ready for review June 18, 2026 07:12
@github-actions

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 7b95dcb1ce0b
Changed files (19): internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/permission_scope_test.go, internal/sandbox/engine.go, internal/sandbox/engine_test.go, internal/sandbox/grant_scope.go, internal/sandbox/grant_scope_test.go, internal/sandbox/grants.go, internal/tools/registry_test.go, internal/tools/types.go, internal/tools/web_search.go, internal/tools/web_search_test.go, and 7 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review: web tool permission alignment

Reviewed the security-sensitive parts closely (grant scoping, the web_search auto-allow, scope-through-cache/hydration) and ran build/test/staticcheck on the branch. On net this tightens the sandbox, and both headline changes hold up. Approving.

Security — verified sound

web_search auto-allow does not bypass the network policy. web_search.go flips to Permission: PermissionAllow, but in-process network tools were never subject to the sandboxed-shell's Network: deny gate in the first place — that gate is for the shell (engine.go effectiveNetworkMode; see the NetworkHostAllowed comment). The opt-in EnforceToolNetwork path still holds web_search to the policy: RunWithSandbox still calls enforceScopedNetworkPolicy(engine, host), and sameHostRedirectPolicy still fails closed on cross-host redirects. The egress target is operator-fixed — web_search's schema exposes only query/limit/domains (no host/URL), so the model can't redirect it. Net effect: removes a prompt, keeps enforcement.

Host-scoped web_fetch grants are matched exactly — and this narrows the previously tool-wide grant. grantCovers's ScopeHost case is normalizeHostScope(reqScope) == normalizeHostScope(grant.Scope) — exact equality, no prefix/suffix. I exercised the confusion vectors: a grant for example.com correctly refuses api.example.com (subdomain), example.com.attacker.net (sibling suffix), and the userinfo trick https://example.com@evil.com (url.Hostname()evil.com, not covered). Case-/port-/trailing-dot-/IPv6-bracket-normalized. No over-broad match.

No scope confusion through render cache or session hydration. The render-cache fingerprint keys on ToolCallID + the new Scope + SideEffect, and the session round-trips Scope/SideEffect, so a cached or rehydrated card can't apply host A's grant to host B.

Nits (optional, non-blocking)

  1. Hostless/malformed URL falls back to a tool-wide web_fetch grant. If web_fetch gets a URL that parses but yields Host == "" (e.g. file:///x), DeriveScope returns ScopeToolWide, so "always allow" persists a tool-wide grant. This matches pre-PR behavior (and the "always for this host" label won't render without a host), so it's not a regression — but for a network tool, fail-closed (re-prompt) would be safer than persisting tool-wide. Worth a follow-up.
  2. A couple of negative tests would lock in the security crux. Host scoping is well-covered for subdomains/case; I'd add the userinfo-confusion URL (https://a@evil.com resolves to evil.com and is refused), the hostless-URL → tool-wide fallback (documents the intent), and an IPv6/punycode negative match.

Overlap with another in-flight branch

This modifies internal/agent/loop.go, internal/tools/types.go, internal/agent/types.go, and internal/tui/model.go, which a separate open branch (specialist delegation, #243) also touches. #242's edits to those files are localized (mostly doc comments + reuse), so a textual conflict is unlikely, but types.go/loop.go will want a 3-way merge — whichever lands second should re-run the sandbox/agent/tui -race suite. No semantic conflict; the two touch different concerns.

Tooling

go build ./..., go vet ./..., and go test ./internal/sandbox/... ./internal/tools/... ./internal/agent/... ./internal/tui/... -race all pass; staticcheck is clean for the files this PR touches (the few findings are pre-existing in untouched files).

Careful, well-tested change — approving.

@anandh8x
anandh8x merged commit a1b9bbb into main Jun 18, 2026
8 checks passed
@Vasanthdev2004
Vasanthdev2004 deleted the permission-approval-ui-parity branch June 28, 2026 08:27
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.

3 participants