ππ Agent
Chapter 18 / 21

Settings, Trust, Telemetry & Output Guard

Project trust gate & TUI output integrity

output-guard owns stdout so the model never corrupts the TUI
No simulation scenario available for this chapter

Deep Dive

~7 min read·7016 chars

output-guard takes over stdout so the model never breaks the TUI

Learning Objectives

After this chapter you will be able to:

  • Explain the two-layer settings (global + project) merge and hot-reload mechanism
  • Describe the complete project trust gate flow and its security boundaries
  • Explain how output-guard cooperates with the render cycle to guarantee frame integrity
  • Understand the telemetry opt-out mechanism and privacy boundaries

Key Source Files

FileLinesResponsibility
packages/coding-agent/src/core/settings-manager.ts1234Two-layer settings merge, hot-reload, migrations
packages/coding-agent/src/core/trust-manager.ts~300Trust state management
packages/coding-agent/src/core/project-trust.ts~200resolveProjectTrusted implementation
packages/coding-agent/src/core/telemetry.ts~250Telemetry reporting and opt-out
packages/coding-agent/src/core/output-guard.ts~150stdout takeover and buffering
packages/coding-agent/src/core/http-dispatcher.ts~100undici dispatcher + proxy configuration

Design Motivation: Why Are Settings So Complex?

Problem: pi's configuration comes from multiple dimensions:

  • User global preferences (~/.pi/agent/settings.json)
  • Project-specific config (.pi/settings.json)
  • One-shot CLI overrides (--model, --thinking)
  • Runtime hot-modification (/settings dialog)

If there were only one layer: Either global config pollutes projects ("I set vim keybindings globally, but this project needs emacs"), or project config cannot override global.

Analogy: Like Git's three-layer config — system → global → local, where closer means higher priority. pi uses global → project → CLI.

Design goals: Merge deterministically, reload without restart, migrate without data loss.

Two-Layer Settings Hot-Reload

class SettingsManager {
  private global: Settings   // ~/.pi/agent/settings.json
  private project: Settings  // .pi/settings.json

  get merged(): Settings {
    return deepMerge(this.global, this.project)  // project overrides global
  }

  // Hot-reload: automatically reload on file change
  watch() {
    fsWatch("~/.pi/agent/settings.json", () => this.reload())
    fsWatch(".pi/settings.json", () => this.reload())
  }

  // Migration: old-version settings auto-upgrade
  migrate() {
    if (this.global.version < CURRENT_VERSION) {
      migrations[this.global.version](this.global)
      warn("settings auto-migrated to v" + CURRENT_VERSION)
    }
  }
}

Why hot-reload matters: Users never restart pi after changing settings. Edit the file → next frame picks it up. The /settings dialog internally writes the file → triggers hot-reload.

Project Trust Gate

resolveProjectTrusted(cwd):
  ① cwd found in ~/.pi/agent/trust.json? → trusted ✓
  ② .pi/trust.json exists?
     ├── User confirms → write trust.json → trusted ✓
     └── User declines → untrusted ✗
  ③ Default policy (defaultProjectTrust):
     ├── "ask" (default) → prompt dialog
     ├── "trust" → auto-trust (dangerous!)
     └── "deny" → auto-deny (safest)

Restrictions when untrusted:

  • .pi/extensions/ not loaded (→ Chapter 13)
  • .pi/skills/ not loaded (→ Chapter 12)
  • .pi/SYSTEM.md not applied (→ Chapter 12)
  • But global extensions and AGENTS.md still load (not controlled by the project)

Why do global extensions load before trust? They are user-installed (pi install), not project-controlled — they may participate in trust event handling (e.g., enterprise compliance policy injection).

output-guard and the Render Cycle

// output-guard.ts
class OutputGuard {
  private buffer: string[] = []

  install() {
    const original = process.stdout.write.bind(process.stdout)
    process.stdout.write = (chunk: any, ...args: any[]) => {
      this.buffer.push(chunk.toString())  // buffer, don't write directly
      return true
    }
  }

  // Called by the render loop at a safe point
  flush(write: (s: string) => void) {
    const content = this.buffer.join("")
    this.buffer = []
    if (content) write(content)  // controlled write-out
  }
}

Render cycle cooperation (→ Chapter 17): ① Collect events → ② Update components → ③ output-guard.flush() writes subprocess output into message panel → ④ Differential render wrapped in DECSET 2026. Subprocess output is "recruited" as TUI content at step ③, never tearing the frame.

Telemetry and Privacy

// telemetry.ts
class Telemetry {
  enabled = process.env.PI_TELEMETRY !== "0"

  reportInstall() {
    if (!this.enabled) return
    fetch("https://pi.dev/api/report-install", {
      method: "POST",
      body: JSON.stringify({ version: PI_VERSION, platform: process.platform })
    })
  }
}

Privacy boundaries: PI_TELEMETRY=0 disables completely. Only reports install event (version + platform), never conversation content. Attribution header (x-pi-version) tells providers the request came from pi.

Engineering Insights

  1. settings-manager.ts is 1234 lines: Mostly schema validation (TypeBox) + migration scripts. Every settings format change adds a migration — users never lose config due to upgrades.

  2. output-guard is only ~150 lines: Minimal but critical. It is the secret to "6046 lines of interactive-mode staying stable" — without it, any subprocess console.log is a ticking time bomb.

  3. http-dispatcher ~100 lines: Globally configures undici connection pooling, proxy (http_proxy/https_proxy), and timeouts. All HTTP requests (model API, OAuth, telemetry) share this single dispatcher.

Cross-Chapter Links

DirectionChapterRelationship
PrerequisiteCh10 CLI ModesTrust gate executed in main.ts
PrerequisiteCh16 Interactive TUIoutput-guard protects TUI frames
PrerequisiteCh17 TUI EngineRender cycle cooperates with output-guard
RelatedCh13 ExtensionsTrust determines whether extensions load
RelatedCh12 System PromptTrust determines whether .pi/SYSTEM.md applies
RelatedCh08 Auth & OAuthhttp-dispatcher carries OAuth requests

Check Your Understanding

  1. If settings did not support hot-reload, how would the user experience degrade?
  2. If output-guard flushed at the wrong point in the render cycle, what problems would arise?
  3. Why is "global extensions loading before trust" safe, but "project extensions loading before trust" dangerous?

Takeaways

  • Two-layer settings (global + project) hot-reload — changes take effect immediately, no restart
  • Project trust gate: untrusted → no project extensions/skills/SYSTEM.md loaded
  • output-guard takes over stdout — subprocess output recruited as TUI content
  • Telemetry fully disableable (PI_TELEMETRY=0), only reports install event

This chapter is based on the main-branch source of earendil-works/pi.