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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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