AGENTS.md@assistant/src/approvals · git:20260921.470901d · 2026-09-21 · sha256 e507a14ef48c850f

AGENTS.md@assistant/src/approvals git:20260921.470901dA

Immutable. This exact content never changes and is served at /api/v1/blob/e507a14ef48c850f.

# Approvals & Guardian Rules

The end-to-end lifecycle of interactive guardian requests (approvals, questions) — promotion, notification cards, reply routing, resolution — is mapped in [docs/guardian-request-flow.md](../../docs/guardian-request-flow.md). Read it before extending any decision or card-delivery path.

## Approval Flow Resilience

- **Rich delivery failures must degrade gracefully.** If delivering a rich approval prompt (e.g., Telegram inline buttons) fails, fall back to plain text with instructions (e.g., `Reply "yes" to approve`) — never auto-deny.
- **Non-rich channels** (http-api) receive plain-text approval prompts. The conversational approval engine handles free-text responses.
- **Race conditions:** Always check whether a decision has already been resolved before delivering the engine's optimistic reply. If `handleChannelDecision` returns `applied: false`, deliver an "already resolved" notice and return `stale_ignored`.
- **Unified guardian decision primitive:** All guardian decision paths (callback buttons, conversational engine, channel text) must route through `applyGuardianDecision()` in `assistant/src/approvals/guardian-decision-primitive.ts`. Do not inline decision logic (CAS resolution, resolver dispatch, grant minting) at individual callsites.

## Single-Guardian Invariant

Each assistant instance serves exactly one guardian. Multi-guardian is not supported and will never be. Only that guardian's principal decides guardian requests: `applyGuardianDecision()` requires it exactly. Do not introduce guardian-keyed maps, per-guardian routing logic, or multi-guardian multiplexing; they add complexity without a real use case and create the false impression that cross-guardian isolation is required.

One guardian does not mean one person connected. The event stream (`GET /v1/events`) admits any actor principal with `chat.read`, so a non-guardian actor can hold a valid connection. Routes that must be guardian-only set `requireGuardian: true`, which `routes/http-adapter.ts` enforces with `requireBoundGuardian` (`runtime/auth/require-bound-guardian.ts`). Content meant only for the guardian is scoped to the guardian's principal at delivery (`targetActorPrincipalId` on the event hub), never assumed safe because it reached a connection.

## Guardian Verification Invariant

Guardian verification consumption must be identity-bound to the expected recipient identity. Every outbound verification session stores the expected identity (phone E.164, Telegram user/chat ID), and the consume path rejects attempts where the responding actor's identity does not match.

Conversational guardian verification control-plane invocation is guardian-only. Non-guardian and unverified-channel actors cannot invoke channel verification endpoints (`/v1/channel-verification-sessions/*`) conversationally via tools. Enforcement is a deterministic gate in the tool execution layer (`assistant/src/tools/executor.ts`) using actor-role context — only `guardian` and `undefined` (desktop/trusted) actor roles pass. The policy module is at `assistant/src/tools/verification-control-plane-policy.ts`.

## Memory Provenance Invariant

Memory access follows the acting actor's capabilities: `resolveCapabilities(trustClass).canAccessMemory`, which only `guardian` holds. An actor without it neither reads nor writes the guardian's memory. The memory plugin's `injectors.ts` admits personal-memory content only for a turn whose trust context passes `isPersonalMemoryAllowed`; the `recall`, `remember`, and `delete_memory_page` tools refuse such a turn (`plugins/defaults/memory/tools.ts`); and extraction from conversation messages runs only for guardian-provenance rows, or legacy rows with none (`plugins/defaults/memory/indexer.ts`).

## Guardian Privilege Isolation Invariant

Untrusted actors (`non-guardian`, `unverified_channel`) must never receive privileged host/tool capabilities or privileged conversation context directly.

- Tool execution gate: untrusted actors cannot execute host-target tools or side-effect tools in-band. These actions require guardian-mediated approval flow. Enforcement lives in `assistant/src/tools/tool-approval-handler.ts`.
- History view gate: when loading conversation history for untrusted actors, only untrusted-provenance messages are included and compacted summaries are suppressed. This prevents replay of guardian-era context after trust downgrades. Enforcement lives in `assistant/src/daemon/conversation-lifecycle.ts` and actor-scoped reload wiring in `assistant/src/daemon/conversation.ts`.