Permissions and Sandboxing Patterns for Desktop AI Apps (TypeScript)
securitydesktoppermissions

Permissions and Sandboxing Patterns for Desktop AI Apps (TypeScript)

UUnknown
2026-03-03
11 min read
Advertisement

Audit-style guide: safely grant desktop AI apps access to files, mic, GPU, and enforce least privilege with TypeScript.

Hook: Why granting desktop AI apps access is an audit, not a checkbox

Desktop AI apps in 2026 can do amazing things: index your files, transcribe meetings, and run models on your GPU. But every additional access—files, microphone, GPU—expands the attack surface and multiplies compliance and privacy risks. If you're a developer or security engineer shipping a desktop AI product, this is the practical audit you need: how to grant exactly what the app needs, when it needs it, and how to enforce least privilege using TypeScript architectures.

Executive summary (what you should do first)

  • Adopt process isolation: push sensitive ops (file I/O, microphone capture, GPU inference) into narrowly scoped privileged processes.
  • Use typed capability tokens in TypeScript so only code with an explicit capability can call sensitive APIs.
  • Make grants time-limited and UI-driven: ask users per-task and revoke after use.
  • Validate everything at the boundary with runtime guards and schema checks (zod/TypeBox) and mirror those guarantees in TypeScript types.
  • Audit and instrument every privilege grant and failed attempt for post-incident review.

Since late 2024–2025, desktop AI trends accelerated: more local LLMs and multimodal models, wider WebGPU support across platforms, and OS-level privacy controls that treat microphones and file access as first-class. In early 2026 we've seen applications like Anthropic's Cowork (Jan 2026) push agents that directly manipulate files and solicit deep system access. That makes least-privilege architecture and auditable grants essential. Tooling has matured too: runtimes like Deno and modern bundlers have more expressive permission flags; WebGPU is widely supported; and popular desktop frameworks (Tauri, Electron) provide stronger sandboxing and IPC primitives.

High-level architecture: isolate privilege in narrow processes

The easiest way to enforce least privilege is to follow the principle of process separation. Treat your app as a small coordinator process (UI) and a set of specialized worker processes that hold high privileges.

  1. UI / Renderer - no direct access to disk, mic, or GPU. Communicates via typed IPC only.
  2. File Access Worker - a single-process service that implements file reads/writes under a sandboxed root and exposes typed operations via IPC.
  3. Audio Capture Worker - captures microphone frames and streams them over an encrypted channel; enforces consent and session tokens.
  4. GPU Inference Worker - a dedicated process with GPU access and constrained resources (memory/threads); model execution happens here.

Why process separation helps

  • Limits blast radius: compromise of the UI doesn't expose raw access tokens or device handles.
  • Enables OS-level sandboxing per-process (AppContainer, Mac App Sandbox, seccomp, firejail).
  • Makes auditing straightforward: log and correlate grants per process.

Pattern: Typed capability tokens in TypeScript

Use TypeScript types to represent capabilities so the compiler helps you maintain least privilege. The runtime can mint opaque tokens; TypeScript ensures only code paths that receive a capability can call the sensitive API.

Core idea (code)

// capability.ts
export type Brand = K & { __brand?: T }

// define capability brands
export type FileReadCap = Brand<{ readonly __cap: 'file-read' }, 'FileReadCap'>
export type FileWriteCap = Brand<{ readonly __cap: 'file-write' }, 'FileWriteCap'>

// opaque creator only in privileged module
export const createFileReadCap = (): FileReadCap => ({ } as FileReadCap)

In your UI code, you cannot manufacture a FileReadCap; only the privileged worker can mint it and hand it over via IPC. This prevents accidental API use and documents allowed capabilities in types.

Typed API surface for IPC

// ipc-api.ts
export interface FileApi {
  readFile(path: string, cap: FileReadCap): Promise<Uint8Array>
  writeFile(path: string, data: Uint8Array, cap: FileWriteCap): Promise<void>
}

Use a thin runtime guard at the privileged worker boundary to validate the token and log the grant. Keep tokens short-lived and store them in ephemeral memory only.

File access: granular, auditable, and path-sanitized

Grant file access in the smallest unit possible: a single file or a scoped directory. Avoid global filesystem access. Prefer one of these approaches:

  • Descriptor-passing (Unix): the privileged process opens the file and passes the file descriptor to the worker, eliminating need to pass paths that could be exploited.
  • Scoped root: define a sandbox root and refuse paths that escape it (reject ../ traversals and symlink attacks).
  • Intent-based grants: create grants like "read-attachment-123.pdf until T+10min" and associate with a session token.

TypeScript pattern: FileGrant object

// file-grant.ts
export type FileGrant = {
  id: string
  expiresAt: number // epoch ms
  allowedOps: ('read' | 'write')[]
}

export const isGrantValid = (g: FileGrant) => Date.now() <= g.expiresAt

The File Access Worker verifies isGrantValid(grant) and the allowedOps before performing any I/O. Log grant usage with file hash and result code for audits.

Microphone access should be explicitly requested and clearly explained to users. For many AI features you don't need raw audio stored—prefer streaming inference with ephemeral buffers and serverless privacy transforms.

Practical rules

  • Always show a prompt that explains the feature and the exact scope.
  • Use session-scoped tokens and revoke on pause/stop.
  • Apply client-side transformations (voice fingerprint removal, obfuscation) before persisting or uploading.
  • Keep the capture worker distinct from playback/renderer to limit access to captured frames.

TypeScript sketch: audio stream capability

// audio-cap.ts
export type AudioCap = Brand<{ readonly __cap: 'audio-capture' }, 'AudioCap'>

export interface AudioApi {
  startCapture(cap: AudioCap, opts: { sampleRate: number }): Promise<string /* sessionId */>
  stopCapture(sessionId: string, cap: AudioCap): Promise<void>
}

The Audio Worker validates consent UI state before creating AudioCap and streaming frames. You should also rate-limit and sample audio when sending to models, and never store raw streams long-term unless the user explicitly opts in.

GPU access: isolate model execution and control resource quotas

GPU access is valuable for performance but risky: buggy kernels, malicious models, or resource exhaustion can degrade host systems. Treat the GPU as a privileged resource and expose an abstracted, typed inference API to the rest of your app.

Practical GPU rules

  • Run models in a dedicated GPU worker process under an OS sandbox when possible.
  • Enforce resource quotas: VRAM caps, per-inference timeouts, and concurrency limits.
  • Validate model artifacts (signatures/hashes) before loading on the GPU.
  • Prefer WebGPU or vendor runtimes with explicit permission surfaces; avoid exposing raw CUDA handles.

TypeScript API: typed inference surface

// inference.ts
export type GpuCap = Brand<{ readonly __cap: 'gpu' }, 'GpuCap'>

export interface InferenceApi {
  runModel(modelId: string, inputs: unknown, cap: GpuCap): Promise<unknown>
  getGpuStats(cap: GpuCap): Promise<{ vramUsage: number, tempC: number }>
}

The GPU Worker should check modelId against an allowlist and perform hash validation. Apply preflight guards: if a model tries to allocate above a threshold, abort and log an alert for audit.

Sandboxing options (OS & runtime)

Choose sandboxing according to platform needs. Here are reliable options in 2026:

  • macOS: Mac App Sandbox + hardened runtime; use App Sandbox entitlements for camera/microphone and scoped file access.
  • Windows: AppContainer and Protected Processes; prefer secure Desktop Bridge packaging for tighter policies.
  • Linux: namespaces, seccomp, and user namespaces with tools like firejail or systemd-run to limit capabilities.
  • Tauri: combines Rust backend with webview and encourages minimal privileges; good default for least-privilege designs.
  • Electron: still viable but requires strict contextIsolation, no remote module, and careful preload scripts with typed bridges.
  • WASM/WASI: run untrusted models or plugins in a WASM sandbox with controlled host functions for I/O.

Practical auditing checklist (TypeScript-friendly)

  1. Inventory every API that touches files, mic, or GPU and map them to a capability type in TypeScript.
  2. Ensure all sensitive APIs live in privileged modules; UIs only hold capability references.
  3. Require runtime validation for every grant: check time, scope, origin, and user consent before executing.
  4. Log grants, expiry, and usage with correlation IDs; keep logs tamper-evident (append-only or signed).
  5. Run fuzz and stress tests on GPU worker and audio capture worker to reveal resource-exhaustion paths.
  6. Verify model provenance and signer checks before allowing GPU load. Reject unknown or unsigned artifacts by default.

Troubleshooting: common errors and how to resolve them

Permission denied at runtime

Symptom: API call from renderer returns a permission error. Cause: capability token missing or expired, or OS privacy block.

Fixes:

  • Confirm capability was minted and sent over IPC correctly; add structured logs when creating the token.
  • Check OS privacy prompts (macOS/Windows) and verify entitlements/manifest flags.
  • When testing, ensure the granting UI actually sets the session state and that the grant expiration is long enough for the debug session.

GPU worker crashes or is unresponsive

Symptom: model load fails, driver errors, or app hangs.

Troubleshooting:

  • Collect GPU worker stdout/stderr and vendor logs (NVIDIA/AMD) and correlate by timestamp with your app logs.
  • Enable per-inference timeouts and return a typed error to the UI to avoid blocking the event loop.
  • If model allocations exceed VRAM, fall back to CPU inference or quantized models and log an OOM event for audit.

Microphone capture fails or captures silence

Symptom: startCapture returns a session but frames are null or device unavailable.

Fixes:

  • Check OS microphone privacy settings and confirm the app has been authorized; provide a recoverable UI flow that directs users to OS settings.
  • Detect device busy states and surface a clear error (device-in-use) rather than generic permission denied.
  • Instrument packet loss and latency metrics for streaming inference to diagnose network or buffer issues.

Case study: short audit walkthrough

Imagine a desktop app that auto-summarizes the user's project files and records meeting notes. Audit the privilege flow:

  1. List features: project indexing (files), live transcription (mic), local summarization (GPU).
  2. Assign capabilities: FileReadCap (scoped to project root), AudioCap (session-scoped), GpuCap with vram limit.
  3. Implement workers: file-worker, audio-worker, gpu-worker. Each worker validates grants and emits detailed logs to the audit trail.
  4. UI requests a one-time file grant for the selected project folder and shows exact list of operations that will be performed (read-only traversal, no uploads).
  5. If a user enables transcription, the audio-worker creates an AudioCap only after the OS consent and the UI's explicit start button; tokens expire on stop.
  6. All model artifacts must be signed; the gpu-worker refuses unsigned models or models whose hash changes mid-execution.

Advanced TypeScript patterns for enforcement

These patterns help make privilege boundaries explicit and harder to bypass:

  • Branded capability types (shown above) to prevent forging.
  • Private constructors + factory functions: model classes that can only be instantiated by a privileged module that holds an internal symbol.
  • Exhaustive discriminated unions for permission state (granted | denied | expired). Use switches that force handling every state at compile time.
  • Schema-verified IPC: validate payloads with zod and export these schemas as types—so runtime checks and TypeScript types stay synchronized.
  • Capability narrowing: functions accept a broad PermissionContext but return narrowed contexts after checks (functional style) to reduce branching and make allowed operations explicit.

Logging, retention, and compliance

Audit logs should include who requested the grant (UI session ID), what was requested, when it was granted, and the final result. For compliance:

  • Keep logs signed and immutable for a retention period aligned to your policy.
  • Mask sensitive contents (file paths or audio snippets) in logs; store only hashes or metadata unless explicit user consent exists.
  • Provide an export feature for users that shows what was accessed—this helps with transparency and regulatory requests.

Quick reference: dev checklist before shipping

  1. Map every sensitive operation to a TypeScript capability.
  2. Verify each capability is minted only by privileged code paths.
  3. Sandbox worker processes with OS controls and resource quotas.
  4. Implement runtime validation and signed logs for all grants.
  5. Test revocation paths and simulate stale token use. Ensure safe failure modes.
  6. Document model provenance and require signatures for GPU loads.
"Assume that any module that can access a raw handle (file descriptor, audio device, or GPU context) will be compromised. Design as if that compromise is inevitable." — audit guidance

Final notes and predictions (2026+)

Desktop AI will continue to push boundaries in 2026: more multimodal local agents, improved WebGPU support, and OS vendors adding finer-grained runtime permissions. That means the bar for least privilege is rising: you must design for auditable, short-lived grants and typed architectures that let you reason about privileges at compile time.

Actionable takeaways

  • Start with process separation today: split UI and privileged workers.
  • Model permissions as branded TypeScript types and enforce at the IPC boundary.
  • Grant only what you need: single-file or scoped-root grants, session-scoped audio, and signed models for GPU.
  • Log everything, mask sensitive data, and provide revocation and audited trails.

Call to action

Run an internal audit this week: map sensitive APIs to capability types, implement one worker process (file or audio) with runtime guards, and add structured logging. If you want a template for capability-based IPC in TypeScript or a starter repo for sandboxed GPU workers, share your platform (macOS/Windows/Linux) and I’ll draft a focused example. Ship safe, smallest-possible grants first—then expand with auditable confidence.

Advertisement

Related Topics

#security#desktop#permissions
U

Unknown

Contributor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

Advertisement
2026-03-03T01:48:07.810Z