Skip to content

Model Directory Permissions via User-Defined Trust Tiers #163

Description

@jeonghun-jj-lee

type: spec
date: 2026-08-10
status: draft
priority: p2
platform: opencode
tags: [spec, permissions, privacy, settings-ui, multi-provider]
linked_plan: null

Model Directory Permissions via User-Defined Trust Tiers

Important

Decision Surface

Problem: All LLM providers currently share identical file access permissions. Users who work with multiple providers (Anthropic, OpenAI, Meta, etc.) have no way to restrict what directories or actions a specific model can access — even though providers have different data-handling policies and some models within the same provider (e.g., Meta Muse vs Meta Llama) have different privacy tiers.

Approach: Add a Provider Permission Layer with user-defined trust tiers. Users create named tiers (e.g., "Trusted", "Limited", "Untrusted"), assign models to tiers, and configure directory × action permission matrices per tier. Ships with 3 presets + a default "Unassigned" tier. Enforced at tool-call time AND context assembly — strict privacy filtering including history redaction on model switch.

Approaches Considered:

  • (A) Provider Permission Layer with trust tiers (selected) — clean separation, wraps existing permission system, maps well to the UI, keeps single-session model
  • (B) Extended Rule Schema — extends existing Permission.Rule with an optional provider field; minimal new abstractions but harder to present in UI and rule evaluation becomes complex
  • (C) Sandboxed Provider Sessions — strongest isolation (virtual filesystem scoping per provider) but heaviest implementation, breaks single-conversation model

Scope: New "Permissions" tab in settings dialog. Schema + config changes. Enforcement hook in permission system. Source-path tagging on tool results. Context filtering + history redaction on model switch.

Assumptions:

  • Permissions are global (not per-project)
  • Provider permission grants suppress normal ask prompts (authoritative layer)
  • A model belongs to exactly one tier at a time
  • The "Unassigned" default tier cannot be deleted

Acceptance Criteria

  • A new "Permissions" tab appears in the settings dialog (6th tab)
  • Users can create, rename, reorder, and delete trust tiers (except "Unassigned")
  • 3 preset tiers ship by default: Trusted (full access), Limited (read-only), Untrusted (no access), plus a non-deletable "Unassigned" (ask-all) default
  • Each tier has a directory × action matrix with per-cell allow/deny/ask toggles
  • Action groups: Read (read, glob, grep), Write (write, edit), Execute (bash), Network (webfetch, websearch)
  • Models are assigned to tiers via a multi-select picker inside each tier card
  • A model can only belong to one tier; reassignment removes it from the previous tier
  • Unassigned models fall through to the "Unassigned" default tier
  • At tool-call time, the active model's tier permissions are resolved BEFORE the existing global permission system
  • A tier "allow" for an action+directory suppresses the normal permission ask prompt
  • A tier "deny" blocks the tool call immediately (no prompt, no fallthrough)
  • On model switch mid-session, permissions re-evaluate immediately against the new model's tier
  • Context filtering: file contents from directories denied by the active tier are never included in the system prompt or auto-context
  • History redaction: on model switch, file contents in message history from now-denied directories are replaced with [Content from {path} filtered — trust tier "{tierLabel}" does not have read access]
  • Source-path metadata is preserved on all tool results that return file content
  • Dangerous actions (Execute, Network) are visually highlighted with warning styling when set to "allow"
  • Each tier card shows a computed summary badge: "Full Access" (⚠️), "Read Only", "No Access", "Ask Everything", or "Custom"
  • Config persists to ~/.config/opencode/opencode.jsonc under providerPermissions AND interactive "always" grants persist to SQLite (keyed by tier)
  • Directory rules support glob patterns (e.g., ~/secrets/**, src/private/**)

Key Decisions

# Decision Rationale
1 Trust tiers (not per-provider or per-model directly) Handles intra-provider variance (Meta Muse ≠ Meta Llama); fewer configs than N models; conceptually familiar (RBAC-like)
2 4 action groups, not 8+ individual toggles Cognitively manageable in a checkbox matrix; covers the real privacy boundaries (read vs mutate vs execute vs exfiltrate)
3 Strict context filtering + history redaction The privacy guarantee must be real — filtering only tool execution while leaking content in context defeats the purpose
4 Global scope only (not per-project) Simpler mental model; users set trust once. Per-project tightening can be a follow-up
5 Provider grant is authoritative (suppresses ask) Avoids double-prompting; if you've explicitly trusted a tier with read access, the per-tool ask is redundant noise
6 Source-path tagging on tool results Required for redaction; also enables future audit/lineage features

Data Contract

// Schema: packages/schema/src/provider-permission.ts

type Effect = "allow" | "deny" | "ask"

type DirectoryPermissions = {
  read: Effect     // covers: read, glob, grep
  write: Effect    // covers: write, edit
  execute: Effect  // covers: bash
  network: Effect  // covers: webfetch, websearch
}

type TrustTier = {
  id: string
  label: string
  directories: Record<string, DirectoryPermissions>  // glob → permissions
}

type ProviderPermissionsConfig = {
  defaultTier: string                    // tier ID for unassigned models
  tiers: TrustTier[]
  assignments: Record<string, string>    // modelId → tierId
}
// Example: ~/.config/opencode/opencode.jsonc
{
  "providerPermissions": {
    "defaultTier": "unassigned",
    "tiers": [
      {
        "id": "trusted",
        "label": "Trusted",
        "directories": {
          "**": { "read": "allow", "write": "allow", "execute": "allow", "network": "allow" }
        }
      },
      {
        "id": "limited",
        "label": "Limited",
        "directories": {
          "**": { "read": "allow", "write": "deny", "execute": "deny", "network": "deny" },
          "~/secrets/**": { "read": "deny", "write": "deny", "execute": "deny", "network": "deny" }
        }
      },
      {
        "id": "untrusted",
        "label": "Untrusted",
        "directories": {
          "**": { "read": "deny", "write": "deny", "execute": "deny", "network": "deny" }
        }
      },
      {
        "id": "unassigned",
        "label": "Unassigned",
        "directories": {
          "**": { "read": "ask", "write": "ask", "execute": "ask", "network": "ask" }
        }
      }
    ],
    "assignments": {
      "anthropic/claude-opus-4": "trusted",
      "openai/gpt-4o": "limited",
      "meta/muse": "untrusted"
    }
  }
}

Constraints & Invariants

  • A model belongs to exactly one tier (enforced at assignment time)
  • The "Unassigned" tier always exists and cannot be deleted or have its ID changed
  • Directory rules resolve most-specific-glob-first (longer/more-specific pattern wins)
  • Tier resolution must be O(1) per tool call (map lookup, not list scan)
  • History redaction must not mutate the stored message history — it filters at send-time only (the original messages remain for re-evaluation if the model switches back)
  • Source-path metadata adds zero runtime cost when no model switch occurs

Prior Art

  • Existing Permission system in packages/core/src/permission.ts — action × resource × effect rules with assert() + SQLite persistence
  • LocationMutation in packages/core/src/location-mutation.ts — external directory detection and boundary enforcement
  • Per-agent permission rulesets in packages/schema/src/agent.ts — precedent for scoped permission evaluation
  • packages/app/src/components/settings-v2/ — existing settings dialog with 5 tabs (General, Shortcuts, Servers, Providers, Models)

Vertical Slices

  1. Schema + config parsing — types, validation, default tier presets
  2. Tier resolution engine — permission lookup: model → tier → directory → action
  3. Source-path tagging — annotate file-content tool results with path metadata
  4. Enforcement hook — wire resolveProviderPermission() into permission.ts before existing assert()
  5. History redactionredactHistory() on model switch, filter denied content at send-time
  6. Context filtering — suppress denied auto-context (instructions, system prompt sources)
  7. UI: Permissions tab — tier cards with checkbox matrix + badges + danger highlights
  8. UI: Model assignment — multi-select picker inside tier cards
  9. UI: Tier management — add/rename/reorder/delete tiers, add directory rules

Source

Brainstorming session — model directory permissions for privacy-conscious multi-provider usage.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions