# Mandaire MCP Wire Contract v1.0.15

**Owner:** Surface layer  
**Canonical source:** `~/openclaw/mandaire_mcp/server.py`, `auth_provider.py`, `core/mandaire/a2a/`  
**Protocol version:** 1.0.15  
**Spec URL:** https://mandaire.org/spec/v1.0  
**Produced:** 2026-05-20 (Surface first-session deliverable)  
**Supersedes:** v0.1 (2026-05-18)  
**Revised:** 2026-06-13 (v1.0.15 — A2A multi-layer catalog expanded)

**Review dispatch targets:**
- Product: semantic correctness of per-kind output shapes
- Inference: compute contract (confidence scoring, expectedness, calibration gaps)
- Judgment: pre-flight hook shape, safety tier gate enforcement

---

## Change log: v1.0.14 → v1.0.15

| Area | Change |
|------|--------|
| §13 A2A Protocol | **Multi-layer A2A catalog (2026-06-13):** §13.3 expanded from 5 Research-only skills to the complete 12-layer registered catalog (5 LIVE · 23 STUB · 3 CANDIDATE · 1 PLANNED across 10 namespaces). Added §13.1.1 co-hosting model: Enrichment co-hosted at `/a2a/enrichment/` subpath; Inference co-hosted at `/a2a` via `skill` prefix routing (`inference.*`). Added §13.5 Agent Card (LIVE skills only). Added §13.6 Telemetry schema. `research.inferred_asset_query` (LIVE 2026-06-06) and `inference.claim_lookup` (LIVE 2026-06-10) documented. App / Chief / OS recorded as intentional non-participants with rationale. Wave 4 (org, infra) pending registration. |

## Change log: v1.0.13 → v1.0.14

| Area | Change |
|------|--------|
| §10.9 Requester key (new) | **SEC-070 D2 — `GET /.well-known/mandaire-requester-key.json` (2026-06-13):** Publicly-discoverable Ed25519 JWK that Mandaire uses to sign cross-tenant request payloads (ADR-0224 cross-user verb prerequisite). No auth required. Response is a JSON Web Key with `kty="OKP"`, `crv="Ed25519"`, `use="sig"`. Key material is generated and rotated by OS (D1); Surface serves the current key file from `var/requester_keys/mandaire-requester-key.json`. 404 when no key file exists (expected until OS D1 generates one). Live at `mcp.mandaire.com` and `mcp.mandaire.app`. |

## Change log: v1.0.12 → v1.0.13

| Area | Change |
|------|--------|
| §10.4 Scope | **ADR-0199 granular scope consent wire (Surface D1/D5/Q5, 2026-06-10):** `valid_scopes` expanded from 3 to 9. Five granular read scopes added: `career:read`, `finances:read`, `general:read`, `health:read`, `observations:read`. These map 1:1 to FROM_KIND_TO_TOPIC topic groups via `SCOPE_TO_TOPIC_MCP`. Legacy full-read scopes (`mandaire`, `mandaire-v2`, `mandaire_health`) still accepted via `_LEGACY_FULL_READ_SCOPES`. Write scope: `write:internal`. Enforcement gate (Execution side) ships shadow-first per §5.4. |
| §10.8 SDK signal endpoint (new) | **ADR-0233 D1 — `/sdk/v1/signal` control-plane endpoint (2026-06-13):** `POST /sdk/v1/signal` accepts cross-VM SDK-side diagnostic signals (health probes, feedback events) from `mnd-app-*` principals. mTLS CN-gated — CN must match `mnd-app-[a-z0-9][a-z0-9-]*` pattern (or explicit `MANDAIRE_SDK_SIGNAL_CN_ALLOWLIST` env list). `from_kind` field discriminates signal class; only `diagnostic` allowed by default (expandable via `MANDAIRE_SDK_SIGNAL_KINDS` env). Returns `{"status": "recorded", "from_kind": "diagnostic"}`. Shares port 8765 (internal tailnet). |
| §10.3 Token TTLs | **Operator tier auto-promotion for owner devices (2026-06-13):** Owner devices (recognized via `_is_owner_device()` in `authorize()`) now auto-promote from DCR default `read_only` tier to `operator` tier. `operator` tier unlocks `mandaire_write` (write-capable tool). `_TIER_TOOLS["operator"]` = `{mandaire, mandaire_read, mandaire_write, mandaire_channel}`. Fix: prior DCR auto-seed left owner devices at `read_only`, silently blocking write tools. |

## Change log: v1.0.11 → v1.0.12

| Area | Change |
|------|--------|
| §2 MCP Tools | **ADR-0021 CRUD split live:** Tool table updated — 8 tools now registered (was 5). Added `mandaire_read` (read-only SELECT, `_meta` envelope), `mandaire_write` (INSERT/UPDATE/DELETE), `mandaire_channel` (channel-aware SELECT). `mandaire` remains but transitions to META/discovery after shim retirement. |
| §2 super-tool watermark | Updated from >5 to >8 (CRUD split adds 3 permanent tools). |
| §10.4 Scope | Added `mandaire-v2` scope (planned; CURRENT_SCOPE still "mandaire" — shim retirement pending). |
| §10.5 M2M / machine token (new) | Added ADR-0139 B3 endpoint: `POST /auth/machine/token` (grant_type=client_credentials) → ES256 JWT. |
| §10.6 ES256 JWKS (new) | JWKS endpoint: `GET /.well-known/jwks.json`. JWT issuer: `https://mcp.mandaire.app`. |
| §10.7 SDK endpoint (new) | ADR-0141 S-O5: `GET /sdk/v1/select` on port 8765 (internal). Agent-auth via `X-Mandaire-Agent-CN` header. CN must be one of `{chief, briefing, david-cli}`. Returns full-fidelity `mandaire_read` response. Port 8768 nginx mTLS frontend pending Mandaire CA bootstrap. |
| §14.3 spec URL note | Corrected — spec is live at mandaire.org/spec/v1.0. |

## Change log: v1.0.10 → v1.0.11

| Area | Change |
|------|--------|
| §3.2 catalog count | **Publish gate check (App #70296):** Corrected from "91 kinds" to "96 catalog entries (95 in `_VALID_FROM_KINDS`; `ai_observation` routes via alias map, not tuple — per App N2)." Prior hand-maintained count had drifted. Verified: `_VALID_FROM_KINDS` = 96 entries / 95 unique (`free` duplicated); spec documents all 95 + `ai_observation` = 96. Delta vs live = 0 undocumented kinds. |
| §3.2 count gate | **Gate PASS:** 0 undocumented SENSITIVE or RESTRICTED kinds in the delta. All 11 RESTRICTED and 42 SENSITIVE kinds fully documented. |
| §3.2 disclosure_demo | **Fast-follow (App #70296):** Added M1 convergence cross-ref pointer to §4.1 from the catalog row — discoverable at point of use for renderers hitting the list shape. |

## Change log: v1.0.9 → v1.0.10

| Area | Change |
|------|--------|
| §4.3 multi-claim aggregation | **Inference #70282 (commit 187de03 co-doc):** Documented the multi-claim provenance aggregation rule for `inference_claim` (N claims → single envelope provenance). Rule: MOST CONSERVATIVE source_kind wins. Ranking: `llm_inferred` < `derived_index` < `observed`/`user_authored`. One `llm_inferred` in the set pulls the whole envelope to −0.15. Per-claim source_kind preserved in `result.claims[].source_kind` for granular renderer hedging. count=0 → provenance=None (0.00; no found=false on this handler so no −0.30). |
| §4.3 handler status | `derived_index=neutral` is FINAL — not pending. Handlers live at commit 187de03. |

## Change log: v1.0.8 → v1.0.9

| Area | Change |
|------|--------|
| §3.2 Finances section | **B1 (BLOCKING, App #70277):** Added `tax_event` (RESTRICTED) — was live in `_VALID_FROM_KINDS` @7461 + RESTRICTED @7510 but entirely absent from spec. Compliance weight: a shipped RESTRICTED financial-PII surface must be in the audited surface contract. |
| §12.1 RESTRICTED tier | **B1:** Added `tax_event` to RESTRICTED enumeration. Live RESTRICTED set = 11 kinds (10 documented in v1.0.8 + `tax_event`). |
| §3.2 catalog count | **H2 (HIGH, App #70277):** Corrected to 91 kinds (was 90; +tax_event). Live `_VALID_FROM_KINDS` = 95 unique (96 entries — `free` duplicated in tuple @7372 and @7401; de-dup Surface-side pending). Count SSOT note added: generate from live tuple at publish time, not hand-maintained. |
| §5 ai_observation INSERT response | **H3 (HIGH, App #70277):** Added 4 live fields missing from documented response: `claim_source_source`, `claim_type_source`, `verification_state`, `claim_signature`. All present in live handler @15049. |
| §4.1 disclosure_applied | **M1 (MEDIUM, App #70277):** Added live-shape reconciliation note. Three shapes exist in live code: (1) `[]` — engine not engaged; (2) dict via `_disclosure_state()` — Judgment engine ran (canonical shape defined in v1.0.8); (3) list-of-`{field, withheld, reason\|policy_tuple}` — disclosure_demo/compose path. Shapes (2) and (3) must converge to ONE before v0.1 PRODUCTION. Reserved: `[]` = engine not engaged; object = Judgment ran. |

## Change log: v1.0.7 → v1.0.8

| Area | Change |
|------|--------|
| §12.1 RESTRICTED tier | **F1 (HIGH, Judgment #70273):** Added `personality_profile` to RESTRICTED list — was in §3.2 catalog but missing from §12.1, causing gate divergence. Added SSOT note: gate MUST derive classification from §3.2 tier column, not this table. |
| §4.6 Safety shim | **F2 (HIGH, Judgment #70273):** S2/S3 HOLD suppression rule added — when Judgment disclosure-policy HOLD fires on S2/S3 topics, external response must equal NO_INFO_RESPONSE template byte-for-byte; `blocked_reason` goes to audit log only. |
| §12.4 New section | **F3 (MEDIUM, Judgment #70273):** Judgment disclosure-policy gate documented (pre-fetch path, distinct from safety.review() post-handler). `block_class` enum: `safety_content \| disclosure_policy \| safety_tier_gate`. |
| §11.2 error codes | **F3 (MEDIUM, Judgment #70273):** Added `block_class` field note for Judgment-integrated error envelopes. |
| §4.1 disclosure_applied | **F4 (MEDIUM, Judgment #70273):** Sub-schema defined — object form (Judgment ran, fields enumerated) vs `[]` (stub, Judgment did not run). Distinction reserved explicitly. |
| §4.1 judgment_applied | **F6 (LOW, Judgment #70273):** Added v1.1-planned field for AOR-2 / GDPR Article 30 evidence that Judgment ran. Currently `disclosure_applied: []` is indistinguishable from Judgment not running. |
| §3.1 caller.disclosure_mode | **F5 (LOW, Judgment #70273):** Added enum for valid values per project_policy_schema_v0_1.md. Unrecognized values default to hold. |
| §15 open items | Added `judgment_applied` wiring + Judgment HOLD error shape as v0.1 PRODUCTION items. |
| Section reference note | Judgment dispatch referenced §11/§7.4/§8 (older draft structure); current draft: §12/§4.6/§4.1. All sections located by content; noting for spec-publishing audit. |

## Change log: v1.0.5 → v1.0.6

| Area | Change |
|------|--------|
| §4.3 confidence scoring | F1: Reverted erroneous v1.0.1 change. `derived_index` is NOT in the +0.20 bucket — live code (server.py:8616-8622) does not include it. `derived_index` = NEUTRAL (0.0 delta). The v1.0.1 changelog entry was wrong; live code's omission is more correct than the prior spec. |
| §4.3 confidence scoring | F2: Documented `source_kind` (inference.db column, 4-value CHECK constraint) and `provenance` (envelope scorer key, emitted by handlers) as DISTINCT namespaces. Added intent mapping table. Pending Inference handler changes at server.py:5695/5739 (co-bump after those ship). |
| §4.3 SSOT note | F3: Corrected `derived_index` prevalence from 99.8% to 92.8% (live: 503 rows — derived_index 467/92.8%, llm_inferred 23/4.6%, observed 13/2.6%, user_authored 0). |
| §4.1 envelope | F4: Marked `_valid_until` and `_prior_id` as v1.1-planned. Not emitted live (_get_inference_claims does not select valid_until; prior_id is per-claim only). Wire to envelope-level deferred pending handler stabilization. |
| §4.4 compression | F5: Dropped `origin` from compression-exempt list. Not a from_kind in §3.2 catalog (orphan entry). |
| §15 confidence_breakdown[] | F6: Calibration gate note updated — GT-B coverage ~3% (far short of per-recipient/topic CI threshold). Stays v1.1 candidate; Inference will notify when calibration crosses usable threshold. |

## Change log: v1.0.4 → v1.0.5

| Area | Change |
|------|--------|
| §3.2 dev_architecture_decision | MAJOR BUMP v1.0 → v2.0 per Dev #64082 + App #64067 meaning-doc. Wire changes: PK renamed `id` → `architecture_decision_id`; `title`+`chosen` merged into `decision_statement`; 4 new required fields (evaluation_matrix, consequences, accepted_tradeoffs, exit_criteria); status enum 3-value → 5-value (drafted\|ratified\|revisited\|superseded\|rejected); made_by enum added (agent\|agent_with_buyer_ratification); special_class_flags conditional-required sub-fields for schema_migration and payment. |
| §3.2 auto-mirror shape | Auto-mirror INSERT from dev_decision_ledger now produces v2.0 schema rows. alternatives_evaluated auto-constructed from ledger's alternatives_considered; evaluation_matrix/consequences/accepted_tradeoffs/exit_criteria are null in auto-mirror (caller populates at ratification). Response includes note field flagging null required fields. |
| §3.2 UPDATE mutable set | New mutable fields: supersedes_architecture_decision_id (renamed from superseded_by), ratified_by, ratified_at, ratification_notes, revisited_at, superseded_at, made_by. Auto-timestamp logic: ratified_at set automatically on status=ratified if not already set. |
| v1→v2 migration | Handler detects v1 table (missing evaluation_matrix column) and DROP+recreates with v2 schema. Safe: no live data in dev_architecture_decision (bootstrapped this session). |
| Schema file | `domains/dev/schemas/dev_architecture_decision.v2.json` (v1.json retained with deprecation marker). |
| §3.2 consent_management SELECT | Fixed per-integration iteration logic (App #64084). Previously collapsed all integration_auth grants to a single row (last-write-wins on consent_type key — incorrect for multi-integration). Now one row per (consent_type, integration_name) pair — each integration gets its own `active_grants` entry. |
| §3.2 consent_management INSERT | Added `applies_to_integration_name` field (REQUIRED when `applies_to_consent_type=integration_auth`, optional otherwise). Stored as `integration_name` on the withdrawal event row. Included in INSERT response and Privacy erasure dispatch. |

## Change log: v1.0.3 → v1.0.4

| Area | Change |
|------|--------|
| §3.2 dev namespace | Handlers SHIPPED for `dev_intent_brief`, `dev_decision_ledger`, `dev_architecture_decision` per Dev #64027. Catalog text updated to final semantic descriptions. Added UPDATE verb to `dev_intent_brief` + `dev_architecture_decision`. `dev_tech_debt` remains gated (v0.2 wave). |
| §3.2 dev descriptions | Replaced placeholder ETA-text with final descriptions from Dev's schema comments. Auto-mirror trigger from `dev_decision_ledger` → `dev_architecture_decision` documented (IRREVERSIBLE class or tag in promo-trigger set of 8). |
| §5.29 correction | Replaced §5.20.2 cites (audit-loop discipline, wrong section) with canonical §5.29 (reversibility enum: REVERSIBLE\|IRREVERSIBLE\|PARTIAL). Affects `reversibility_class` notes in §3.2 dev section. |
| Catalog count | 90 kinds (unchanged); dev_tech_debt count held pending v0.2. |

## Change log: v1.0.2 → v1.0.3

| Area | Change |
|------|--------|
| §3.2 status | Corrected: App rigorous per-kind grep confirmed all 87 existing catalog entries are SHIPPED. Prior "17 PLANNED" estimate was a regex false-negative on App's side. Preamble updated accordingly. |
| §3.2 new kinds | Added `lineage` (STANDARD, Enrichment/graph), `strategic_context` (STANDARD, alias → context), `list_topics` (STANDARD, alias → topic). Catalog grows to 90. |
| §3.2.1 new section | Aliases and legacy migration pointers: `flag_inference` → `ai_observation`, `correct_profile` → `correction` verb=INSERT, `events` → `event`, `photos` → `photo`, `strategic_context` → `context`, `list_topics` → `topic`. |
| §3.2.2 new section | Deprecated kinds: `synthesis` (90-day window), `list_state` (immediate — never had a from_kind handler), `file_store_status` (immediate — routes via `from_kind=file`). |
| §3.2 consent | Added `consent_management` (SENSITIVE) — Privacy AOR-2 / SOC 2 P2. SELECT: active consent grants. INSERT: withdrawal + Privacy erasure dispatch. |
| §3.2 dev namespace | Added `dev_*` gated namespace: `dev_intent_brief`, `dev_decision_ledger`, `dev_architecture_decision`, `dev_tech_debt` (all SENSITIVE, gated pending Dev schemas). |

## Change log: v1.0.1 → v1.0.2

| Area | Change |
|------|--------|
| §4.1 expectedness | HIGH: fixed field/value mismatch — `score` corrected from string enum to float [0.0–1.0]; `surface_recommendation` now carries the 5-value enum (SUPPRESS, SUPPRESS_OBVIOUS_SURFACE_NOVEL, SURFACE_NOVEL, SURFACE_INTERESTING_IF_TRUE, SURFACE_AS_IS); `framing_hint` added (optional). Prior schema rejected every valid response. |
| §4.3 confidence | ~~HIGH: `derived_index` added to +0.20 bucket.~~ **RETRACTED in v1.0.6.** Live code (server.py:8616-8622) never included derived_index in +0.20. SSOT note retained; percentage + mapping corrected in v1.0.6. |
| §4.1 cognitive_mode | MEDIUM: 6 missing fields added under `properties` (not `required`) for minor-bump compatibility: `length`, `structure`, `decisions`, `max_words`, `policy_source`, `user_mode_axes`. Will be promoted to `required` in v1.1. |
| §4.1 envelope | MEDIUM: 3 optional inference-origin fields added: `_inference_origin` (field-level provenance list), `_valid_until` (v1.1-planned — not emitted live), `_prior_id` (v1.1-planned — not emitted live). |
| §4.2 semantics | Updated expectedness description to match corrected schema. |
| §15 confidence_breakdown[] | CONFIRMED v1.1: gated on AOR-3 calibration data quality (FP rate currently unmeasurable; GT coverage 0%). Anticipated shape documented in §15 for planning. |

## Change log: v1.0 → v1.0.1

| Area | Change |
|------|--------|
| §3.2 catalog | Added `status` column (SHIPPED/DRAFT/PLANNED/DEPRECATED) to all tables; deprecated kinds listed in §3.2 footer |
| §3.2 tiers | `personality_profile` escalated SENSITIVE → RESTRICTED (App #63870 recommendation; Big Five inferred from inbound text; no disclosure-engine rule yet) |
| §3.2 footer | Added genetic-data review gate |
| §5 entity_refs | F1: added `uuid` field; clarified uuid wins over integer entity_id; stable alias (email/phone) recommended for cross-rebuild durability |
| §5 schema | F2: `purpose` moved to request-envelope-level note (not payload field) |
| §5 claim_tier | F3: closed enum added (factual/behavioral/preference/preference_negative/boundary) |
| §7 key_people | F1 applied: `entity_id` → `uuid` (preferred) + `entity_id` (best-effort) |
| §9 resolution | F4: step 7 added — contact-tagged entities win ties at every preceding step |
| §3.2 identity | Identity contract (uuid stable, entity_id not invariant) lifted to §3.2 preamble |

## Change log: v0.1 → v1.0

| Area | Change |
|------|--------|
| From_kind catalog | 87 active kinds (was ~60); see §3.2 for full list |
| ai_observation | New INSERT path with complete payload schema (§5) |
| decision | New SELECT path: audit click-through via from_id (§6) |
| situation_brief | Promoted to v0.2: Analysis situation_lookup() primary path live; structured output schema documented (§7) |
| catch_me_up | v0.2.2 output schema documented (§8) |
| entity_lookup | Output schema documented (§9) |
| Provenance | Article 50 provenance triple attached to every envelope (§4.5) |
| Safety tier gate | R2 enforcement live: RESTRICTED / SENSITIVE / STANDARD tiers (§11) |
| Health kinds | 7 health from_kinds added (RESTRICTED tier) (§3.2) |
| free_access | free_access_token + free_access_audit from_kinds (§3.2) |
| disclosure_demo | New demo wedge from_kind (§3.2) |
| Versioning policy | Formal policy codified (§12) |

---

## 1. Transport

| Setting | Value |
|---------|-------|
| Protocol | MCP (Model Context Protocol) via FastMCP |
| Default transport | `streamable-http` |
| Alt transport | `stdio` (Claude Desktop; `MCP_TRANSPORT=stdio`) |
| Bind | `127.0.0.1:8765` (nginx-proxied externally) |
| Mount path | `/mcp` |
| Session model | **Stateless** (`stateless_http=True`) |
| Public URLs | `https://mcp.mandaire.com` (primary), `https://mcp.mandaire.app` (alias) |
| SSE | Deferred to v0.2 streaming path |

---

## 2. Registered MCP Tools

**8 tools** registered (ADR-0021 CRUD split live as of 2026-06-06).

| Tool | Annotations | Purpose | Tier |
|------|-------------|---------|------|
| `mandaire` | `readOnlyHint=True` | Primary read entry point; also META/discovery (transitions after shim retirement) | All tiers |
| `mandaire_read` | `readOnlyHint=True` | READ-ONLY SELECT with `_meta` envelope (`{result, _meta}`) | read_only, write, channel, viewer_read |
| `mandaire_write` | `readOnlyHint=False` | INSERT/UPDATE/DELETE/UPSERT/CORRECT verbs | channel (personal) only |
| `mandaire_channel` | `readOnlyHint=True` | Channel-aware SELECT (injects `disclosure_mode=channel`) | channel (personal) only |
| `health` | `readOnlyHint=True` | HTTP `/health` probe (no auth required) | All |
| `server_status` | `readOnlyHint=True` | DB counts + valid_from_kinds catalog | All |
| `get_protocol_spec` | `readOnlyHint=True` | Machine-readable protocol surface | All |
| `tool_telemetry` | `readOnlyHint=True` | MCP call telemetry from `mcp_telemetry.db` | All |

**Super-tool watermark:** >8 registered MCP tools = regression. PR-time check enforces this.

**Grant tier → tool access** (enforced at `tools/list` + server-side dispatch):

| Tier | Allowed tools |
|------|--------------|
| `read_only` | mandaire, mandaire_read |
| `viewer_read` | mandaire, mandaire_read |
| `write` | mandaire, mandaire_read, mandaire_write |
| `channel` | mandaire, mandaire_read, mandaire_write, mandaire_channel |
| `None` (no active grant row) | [] — fail-closed |

**Scope (current vs planned):**
- `CURRENT_SCOPE = "mandaire"` — shim period. Refresh tokens scoped "mandaire" still work.
- `CURRENT_SCOPE = "mandaire-v2"` — post-shim. Refresh tokens must be re-authorized to get "mandaire-v2". Bumping CURRENT_SCOPE starts the 24h window before the SELECT shim in `mandaire()` can be retired.

Annotation presets:
- `_RO`: `readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=False`
- `_WRITE_INTERNAL`: `readOnlyHint=False, destructiveHint=False, idempotentHint=False, openWorldHint=False`

---

## 3. Request Envelope — `mandaire()`

### 3.1 Full JSON Schema

```json
{
  "$schema": "https://json-schema.org/draft/2020-12",
  "title": "MandaireRequest",
  "type": "object",
  "properties": {
    "verb": {
      "type": "string",
      "enum": ["SELECT", "INSERT", "UPDATE", "DELETE", "READ", "GET", "QUERY", "CREATE", "ADD", "PATCH", "REMOVE"],
      "default": "SELECT",
      "description": "Operation verb. SELECT/READ/GET/QUERY are aliases. INSERT/CREATE/ADD are aliases. UPDATE/PATCH are aliases. DELETE/REMOVE are aliases."
    },
    "from_kind": {
      "type": ["string", "null"],
      "description": "The data domain to query. See §3.2 for full catalog."
    },
    "from_id": {
      "type": ["string", "null"],
      "description": "Canonical ID for direct-lookup path. UUID or integer string depending on from_kind."
    },
    "from_match": {
      "type": ["string", "null"],
      "description": "Free-text match string (SELECT only). Used for fuzzy name/topic matching."
    },
    "fields": {
      "type": ["array", "null"],
      "items": {"type": "string"},
      "description": "Field projections within from_kind. Behavior is from_kind-specific."
    },
    "payload": {
      "oneOf": [
        {"type": "object"},
        {"type": "array"},
        {"type": "null"}
      ],
      "description": "Write payload (INSERT/UPDATE/DELETE). Shape is from_kind-specific. See §5 for ai_observation schema."
    },
    "since": {
      "type": ["string", "null"],
      "format": "date-time",
      "description": "ISO 8601 UTC lower bound (inclusive). e.g. '2026-01-01T00:00:00Z'"
    },
    "until": {
      "type": ["string", "null"],
      "format": "date-time",
      "description": "ISO 8601 UTC upper bound (exclusive)."
    },
    "sources": {
      "type": ["array", "null"],
      "items": {"type": "string"},
      "description": "Source filter. Restricts which data sources contribute."
    },
    "filters": {
      "type": ["object", "null"],
      "description": "Pre-computation filters (SQL WHERE equivalent). Shape is from_kind-specific."
    },
    "having": {
      "type": ["object", "null"],
      "description": "Post-computation filters (SQL HAVING equivalent)."
    },
    "purpose": {
      "type": ["string", "null"],
      "description": "Intent of the call. Required for SENSITIVE and RESTRICTED from_kinds. Suggested values: preparing_for_meeting | drafting_reply | researching_history | evaluating_relationship | disambiguating | ai_writeback_from_conversation"
    },
    "context_note": {
      "type": ["string", "null"],
      "description": "Working hypothesis for retrieval bias. Steers semantic search."
    },
    "caller": {
      "type": ["object", "null"],
      "properties": {
        "who": {"type": "string"},
        "channel": {"type": "string"},
        "disclosure_mode": {
          "type": "string",
          "enum": ["direct", "relay", "delegated", "federated", "professional_update", "investment_discussion", "casual_chat", "medical_consultation", "legal_proceeding"],
          "description": "Maps to `context` axis of the 5-axis disclosure tuple. Unrecognized values default to hold (no policy match → hold per project_policy_schema_v0_1.md §Evaluation Order)."
        }
      },
      "description": "Caller context. Default: self/mirror mode (David's own room)."
    },
    "depth": {
      "type": "string",
      "enum": ["minimal", "standard", "deep", "exhaustive"],
      "default": "standard",
      "description": "Composition depth. deep/exhaustive on person/household/relationship escalates to SENSITIVE tier."
    },
    "format_hint": {
      "type": "string",
      "enum": ["auto", "prose", "structured", "both", "ui"],
      "default": "auto",
      "description": "Output format preference."
    },
    "max_chars": {
      "type": ["integer", "null"],
      "minimum": 1,
      "description": "Cap on rendered_text length."
    },
    "ask": {
      "type": ["string", "null"],
      "description": "Free-text question. No from_kind → recall fallback."
    }
  },
  "required": []
}
```

### 3.2 From_kind Catalog (v1.0.11 — 96 catalog entries: 95 in `_VALID_FROM_KINDS` + `ai_observation` via alias map)

**Identity contract:** `uuid` is the stable entity identity across ER rebuilds. Integer `entity_id` is NOT invariant — it may rotate when entities are merged. Email/phone aliases are stable across rebuilds and recommended for cross-rebuild durability. Any cross-reference that must survive an ER merge must use `uuid`, not `entity_id`.

**Status taxonomy:** All 91 kinds in this catalog are SHIPPED (confirmed live handlers in `mandaire_mcp/server.py`). Deprecated kinds are documented in §3.2.2. Gated kinds (namespace reserved, handler pending) are marked with a GATED note in their catalog entry.

**Live count note (App H2 #70277):** Live `_VALID_FROM_KINDS` = 95 unique entries (96 in tuple — `free` duplicated at positions @7372 and @7401; Surface de-dup pending). The 4-kind gap between documented (91) and live (95) is aliases and potential undocumented kinds. SSOT rule: generate the advertised count from `len(set(_VALID_FROM_KINDS))` at publish time, not hand-maintained.

**Genetic/health data gate:** Any from_kind returning genetic, genomic, or familial disease-risk data requires RESTRICTED tier review + explicit purpose declaration before deployment. No such kinds are currently active; this gate applies to future additions.

#### Core life-graph

| from_kind | Tier | Verb support | Description |
|-----------|------|-------------|-------------|
| `person` | STANDARD (SENSITIVE at depth≥deep) | SELECT | People lookup: entity profile, aliases, interaction counts |
| `household` | STANDARD (SENSITIVE at depth≥deep) | SELECT | Household membership + shared context |
| `relationship` | STANDARD (SENSITIVE at depth≥deep) | SELECT | Entity-pair relationship: tie strength, history |
| `email` | STANDARD | SELECT | Email messages (threat model B: is_retrieved_content=true) |
| `message` | STANDARD | SELECT | iMessage/WhatsApp messages (threat model B) |
| `photo` | STANDARD | SELECT | Photo timeline for entity |
| `event` | STANDARD | SELECT | Calendar events |
| `trip` | STANDARD | SELECT | Travel + trip records |
| `note` | STANDARD | SELECT, INSERT | Notes (threat model B on SELECT) |
| `task` | STANDARD | SELECT, INSERT | Tasks from tasks.db |
| `state` | STANDARD | SELECT, INSERT, UPDATE | Ephemeral key-value state store |
| `inference` | STANDARD | SELECT, UPDATE, DELETE | Inference claims from inference.db |
| `correction` | STANDARD | INSERT | Entity field corrections |
| `topic` | STANDARD | SELECT | Topic corpus: drifts / decisions / threads |
| `context` | STANDARD | SELECT | Context docs (topic_corpus) |
| `area` | STANDARD | SELECT | Life areas listing |
| `recall` | STANDARD | SELECT | FTS + semantic search across recall.db |
| `free` | STANDARD | SELECT | Free-form question (recall + optional synthesis) |
| `file` | STANDARD | SELECT | Files from corpus (threat model B) |
| `attachment` | STANDARD | SELECT | Email attachments |
| `system` | STANDARD | SELECT | Server metadata: DB counts, valid_from_kinds |
| `server_status` | STANDARD | SELECT | Alias for from_kind=system |

#### Operating profile

| from_kind | Tier | Description |
|-----------|------|-------------|
| `profile` / `user_profile` | SENSITIVE | User profile prose |
| `communication_guide` | SENSITIVE | Communication style guide |
| `assistant_identity` | SENSITIVE | Assistant identity doc |
| `operating_instructions` | SENSITIVE | Current operating instructions |
| `trust_vector` | SENSITIVE | Trust calibration state |
| `correction_history` | RESTRICTED | Full aggregate of field corrections |
| `briefing_style` | SENSITIVE | Preferred briefing format |
| `operating_profile` | SENSITIVE | Full operating profile composite |

#### Decisions + standing state

| from_kind | Tier | Description |
|-----------|------|-------------|
| `decisions` | SENSITIVE | Standing decisions list |
| `disclosure_policy` | RESTRICTED | Per-(person, topic, context) disclosure policy graph |
| `decision` | SENSITIVE | Single decision: from_id=<audit_ref> for click-through |
| `compare_options` | SENSITIVE | Tradeoff matrix for in-flight decision |
| `recent_decisions` | SENSITIVE | Alias for topic+fields=decisions |

#### Composed surfaces

| from_kind | Tier | Description |
|-----------|------|-------------|
| `catch_me_up` | SENSITIVE | Full catch-up composite: tasks, threads, drifts, decisions, events, commitments |
| `situation_brief` | SENSITIVE | Structured brief for a named ongoing situation (from_match required) |
| `user_context` | SENSITIVE | Identity + user_profile + active_situations |
| `situation` | SENSITIVE | active_situations.db umbrella |

#### Communication surfaces

| from_kind | Tier | Description |
|-----------|------|-------------|
| `communication_style` | SENSITIVE | Per-recipient style + David's register |
| `draft_reply` | SENSITIVE | Composed: inbound thread + recipient style + similar prior exchanges |
| `draft_email` | SENSITIVE | Composed: new outbound with recipient style + last contact |
| `outbound_style` | SENSITIVE | Composed: L1 generic + L2 channel + L3 mode + L4 recipient |

#### Activity / silence / commitments

| from_kind | Tier | Description |
|-----------|------|-------------|
| `outbound_silence` | SENSITIVE | David sent last, awaiting reply (cadence-aware) |
| `followup_due` | SENSITIVE | David received last, owes reply (cadence-aware) |
| `unread_inbound_important` | SENSITIVE | Recent inbound from importance-class senders |
| `open_commitments` | SENSITIVE | Life commitments (sent + inbound) with temporal anchor |
| `active_threads` | SENSITIVE | Alias for topic+fields=threads |
| `recent_drifts` | SENSITIVE | Alias for topic+fields=drifts |

#### Pattern detection

| from_kind | Tier | Description |
|-----------|------|-------------|
| `trend` | SENSITIVE | Time-series direction for any cross-source metric |
| `anomaly` | SENSITIVE | Cross-source change detector: recent vs baseline z-score |
| `ratio_shift` | SENSITIVE | Pair-metric ratio changes |
| `drift_alert` | SENSITIVE | Composite of trend + anomaly + ratio_shift |
| `anomaly_scan` | SENSITIVE | Find contact_keys that went silent |
| `recurring_meeting` | SENSITIVE | Detect weekly/biweekly/monthly cadence + breaks |

#### Pipeline / review

| from_kind | Tier | Description |
|-----------|------|-------------|
| `pipeline_stage` | STANDARD | Per-phase pipeline.db progress + watermarks |
| `pipeline_approval` | STANDARD | Cloud-batch review-gate state |
| `roadmap` | STANDARD | tasks.db WHERE tags LIKE '%roadmap%' |
| `mcp_finding` | STANDARD | INSERT: proactive agent improvement signals |
| `open_writeback_slots` | STANDARD | Open nil-result slots for principal (feedback loop) |

#### Dimension / pipeline graph

| from_kind | Tier | Description |
|-----------|------|-------------|
| `dimension` | STANDARD | List 5-W dimensions + leaf counts |
| `dimension_node` / `node` | STANDARD | Get one leaf (WHO/WHAT/WHEN/HOW/WHERE) |
| `timeline` | STANDARD | Cross-dim query: turns matching WHO+WHAT+WHEN+HOW+WHERE |

#### Enrichment + graph

| from_kind | Tier | Description |
|-----------|------|-------------|
| `enrich` | STANDARD | Web-enrichment overlay on entity: company/title/role/news |
| `social_graph` | SENSITIVE | Multi-entity edge traversal from people.db |
| `cohort` | STANDARD | Institutional/temporal/topical entity groupings |
| `entity_state` | STANDARD | Tie strength + CI + tie_trend + interaction_frequency from inference.db |
| `inference_claim` | STANDARD | Canonical inferences: claim_text + value + CI + evidence_refs |
| `personality_profile` | RESTRICTED | Big Five inferred from entity's inbound text |
| `surfacing_score` | SENSITIVE | Rank candidates by time-criticality × importance × user-mode-match |

#### Cognitive / input modeling

| from_kind | Tier | Description |
|-----------|------|-------------|
| `user_mode` | SENSITIVE | 6-axis input-mode detection: WHAT/WHERE/WHEN/HOW/WHY/HOW_MUCH |
| `conflict` | STANDARD | Scheduling-conflict detection: overlapping calendar events |
| `calendar` | STANDARD | Calendar discovery: list calendars with owner + source + event_count |
| `agent_status` | STANDARD | Per-agent heartbeat + current in-flight queue item + last outcome |

#### AI writeback

| from_kind | Tier | Verbs | Description |
|-----------|------|-------|-------------|
| `ai_observation` | STANDARD | INSERT, SELECT | AI-synthesized inference writeback to inference.db |

#### Free access (bearer secret links)

| from_kind | Tier | Verbs | Description |
|-----------|------|-------|-------------|
| `free_access_token` | STANDARD | INSERT, DELETE, SELECT | Create/revoke/list bearer-secret free-access links |
| `free_access_audit` | STANDARD | SELECT | Audit log of free-user queries |

#### Health (RESTRICTED tier — audience_tier_max=1)

| from_kind | Tier | Description |
|-----------|------|-------------|
| `latest_vitals` | RESTRICTED | Most recent HR / RR / SpO2 / BP / weight |
| `vitals_trend` | RESTRICTED | Time-series of an HKQuantityType over N days |
| `lab_results` | RESTRICTED | FHIR Observation lab values |
| `medications_active` | RESTRICTED | MedicationRequest where status=active |
| `immunizations` | RESTRICTED | Vaccination history |
| `ecg_summary` | RESTRICTED | ECG classifications over time |
| `conditions` | RESTRICTED | Active medical conditions |

#### Finances (RESTRICTED tier — audience_tier_max=1)

| from_kind | Tier | Verb | Description |
|-----------|------|------|-------------|
| `tax_event` | RESTRICTED | SELECT | Tax document data: `result={tax_returns[], w2_forms[], count}`. Extras: `audience_tier_max:1`, `pii_class:"financial"`, `privacy_policy:"raw_text/source_file withheld"`, `filter_hint`. `raw_text` and `source_file` deliberately excluded from output. Filters: `year` (YYYY format), `limit` (default 50). `purpose` required. |

#### Disclosure

| from_kind | Tier | Description |
|-----------|------|-------------|
| `disclosure_demo` | SENSITIVE | Per-(recipient, topic, context) disclosure wedge; synthetic data + hardcoded policy. **See §4.1 M1 note:** this handler emits `disclosure_applied` as a list-of-`{field, withheld, reason\|policy_tuple}` — diverges from the canonical object form; must converge before v0.1 PRODUCTION. |

#### Consent management

| from_kind | Tier | Verbs | Description |
|-----------|------|-------|-------------|
| `consent_management` | SENSITIVE | SELECT, INSERT | SELECT: active consent grants for principal. `active_grants[]` is one row per active grant — `integration_auth` grants produce one row per integration (`integration_name` field, e.g. `"gmail"`, `"icloud_mail"`). `revoked_grants[]` same shape with `revoked=true`, `revoked_at`, `revocation_event_id`. INSERT `consent_type=consent_withdrawal`: records withdrawal, dispatches to Privacy for DSAR erasure within 30 days. Art. 17(3)(b): consent log retained regardless. `applies_to_integration_name` required (and included in response) when `applies_to_consent_type=integration_auth`. |

#### mandaire.dev namespace (v1.0 SHIPPED; dev_architecture_decision bumped v2.0 per Dev #64082)

Schemas: `domains/dev/schemas/dev_<kind>.v1.json` (dev_architecture_decision: `dev_architecture_decision.v2.json`). Storage: `brain/dev.db`. All `reversibility_class` values use canonical OS.md §5.29 enum (REVERSIBLE\|IRREVERSIBLE\|PARTIAL). `dev_tech_debt` is the only remaining gated kind (v0.2 wave).

| from_kind | Tier | Verbs | Description |
|-----------|------|-------|-------------|
| `dev_intent_brief` | SENSITIVE | SELECT, INSERT, UPDATE | Pre-execution document the AI-CTO produces BEFORE any non-trivial build step. Names what success looks like, what could go wrong (pre-mortem), what is explicitly NOT in scope. Buyer ratifies before code is written. The single artifact that distinguishes Mandaire .dev from Cursor/Devin/Cowork/Operator per PRODUCT.md competitive table. 1 per build cycle. Schema at `domains/dev/schemas/dev_intent_brief.v1.json`. HARD_RULE #14 floor: handler surfaces `_hr14_block` warning on SELECT when `scope_of_effort.reversibility_class=IRREVERSIBLE` AND `status` NOT IN (`ratified`, `edited_then_ratified`). UPDATE mutable fields: `status`, `ratified_at`, `ratified_by`, `scope_amendments[]` (append-only). |
| `dev_decision_ledger` | SENSITIVE | SELECT, INSERT | Per-build-decision record that accumulates across all builds in a project. Every code-touching choice gets a time-stamped row with rationale + alternatives + `reversibility_class`. High-volume (5-50 per build; 1k-10k lifetime per project). Ledger never starts over (CR1 immutability: supersession = new row + `parent_decision_id`, not mutation). `claim_signature` dedup: sha256(decision_text+affects) prevents agent-loop redundancy. Auto-mirror trigger: INSERT with `reversibility_class=IRREVERSIBLE` OR `tags` ∩ {auth, data_substrate, multi_tenancy, language_choice, deployment_substrate, payment, schema_migration, data_loss_risk} → atomic mirror INSERT to `dev_architecture_decision` (v2.0 schema). Schema: `domains/dev/schemas/dev_decision_ledger.v1.json` (v1.1 — extended promo-trigger tag set per App #64039). |
| `dev_architecture_decision` | SENSITIVE | SELECT, INSERT, UPDATE | **v2.0** (MAJOR BUMP per Dev #64082 + App #64067 meaning-doc). Load-bearing framework-level choices (database engine, auth model, language, deployment substrate, multi-tenancy boundary) that constrain all subsequent builds. Low-volume, high-weight. Created directly OR auto-mirrored from `dev_decision_ledger` promo trigger. PK field: `architecture_decision_id` (renamed from v1's `id`). SELECT default status filter: `ratified`. INSERT required fields: `architecture_decision_id`, `project_id`, `decision_statement` (≤500 chars, falsifiable), `alternatives_evaluated[]` (minItems:1, each `{name, disqualifier}`), `evaluation_matrix` (5 axes: cost/reversibility/scaling_ceiling/complexity/risk — each maps alternative→{text,score}), `rationale` (≤500 chars), `consequences[]` (3-7 deterministic implications), `accepted_tradeoffs[]` (2-5 items), `exit_criteria[]` (1+ revisit conditions), `reversibility_class`, `status`, `made_by`, `drafted_at`, `schema_version`. Status enum (5 values): `drafted\|ratified\|revisited\|superseded\|rejected`. `made_by` enum: `agent\|agent_with_buyer_ratification` (flips to latter on ratify). `special_class_flags`: conditional-required sub-fields when `schema_migration=true` (migration_plan + rollback_strategy) or `payment=true` (compliance_check_pci_dss + billing_state_migration_plan). UPDATE mutable fields: `status`, `supersedes_architecture_decision_id`, `linked_decision_ledger_ids[]` (append-only), `ratified_by`, `ratified_at`, `ratification_notes`, `revisited_at`, `superseded_at`, `made_by`. UPDATE ratify rule: `status=ratified` requires `ratified_by`; auto-sets `made_by=agent_with_buyer_ratification`. Schema: `domains/dev/schemas/dev_architecture_decision.v2.json`. |
| `dev_tech_debt` | SENSITIVE | SELECT, INSERT | GATED — v0.2 wave. Tech-debt ledger. P1 per App #63923 demotion. Meaning-doc + schema land in v0.2 alongside taste_memory extraction. Handler returns 'schema not yet landed' until v0.2 ships. |

#### New in v1.0.3

| from_kind | Tier | Description |
|-----------|------|-------------|
| `lineage` | STANDARD | Upstream/downstream traversal from `from_id=<claim_id\|entity_uuid>`. Traverses inference.db prior_id chain + people.db merge graph. `filters.node_kind`: `claim` (default) or `entity`. |
| `strategic_context` | STANDARD | Alias for `from_kind=context` with `from_match=<topic>`. Legacy `search_context` / `get_strategic_context` migration target. Routes to context handler unchanged. |
| `list_topics` | STANDARD | Alias for `from_kind=topic` with no from_match. Lists topic corpus with optional `filters.area`, `filters.type`, `filters.domain`, `filters.limit`. |

---

### 3.2.1 Aliases and Legacy Migration Pointers

These are accepted from_kind values that route to a canonical handler. Callers SHOULD use the canonical form. Aliases are supported indefinitely unless explicitly deprecated with a 90-day window per §14.1.

| Alias | Canonical | Notes |
|-------|-----------|-------|
| `flag_inference` | `ai_observation` | Legacy tool name from pre-super-tool era. Both SELECT and INSERT route identically. |
| `correct_profile` | `correction` with `verb=INSERT` | Legacy tool name. Use `mandaire(verb="INSERT", from_kind="correction", ...)` directly. |
| `events` | `event` | Plural form. |
| `photos` | `photo` | Plural form. |
| `strategic_context` | `context` | See §3.2 new entries. |
| `list_topics` | `topic` | See §3.2 new entries. |
| `commitment` | `open_commitments` | "open commitments" synonym. |
| `cross_source_recall` | `catch_me_up` | Multi-source synthesis synonym. |
| `document` | `recall` | Document search → recall with ask. |
| `server_status` | `system` | Status alias — returns same `system` payload. |
| `user_profile` | `profile` | Profile alias. |
| `dimension_node` | `node` | Dimension node alias (canonical is `node`). |

### 3.2.2 Deprecated Kinds

Deprecated from_kinds return a migration hint but remain in `_VALID_FROM_KINDS` until the deprecation window closes per §14.1 (90 days minimum).

| from_kind | Status | Migration path | Deprecated since |
|-----------|--------|----------------|-----------------|
| `synthesis` | DEPRECATED — 90-day window | `mandaire(from_kind="system", fields=["synthesis"])` | 2026-05-20 (v1.0.3). Window closes 2026-08-18. |
| `list_state` | DEPRECATED — immediate | `mandaire(from_kind="state", filters={"scope": "..."})` — never had a real from_kind handler; was documentation error in migration help-text. | 2026-05-20 (v1.0.3). |
| `file_store_status` | DEPRECATED — immediate | `mandaire(from_kind="file", ask="store_status")` — same; was documentation error in migration help-text. | 2026-05-20 (v1.0.3). |

---

## 4. Response Envelope

All responses use the same envelope. Error responses include the same metadata keys.

### 4.1 Full JSON Schema

```json
{
  "$schema": "https://json-schema.org/draft/2020-12",
  "title": "MandaireResponse",
  "type": "object",
  "required": ["ok", "verb", "result", "rendered_text", "structured",
               "approval_required", "sources_used", "disclosure_applied",
               "absent_knowledge_caveats", "suggestions", "confidence",
               "signals", "cognitive_mode", "expectedness"],
  "properties": {
    "ok":                      {"type": "boolean"},
    "verb":                    {"type": "string"},
    "result":                  {"oneOf": [{"type": "object"}, {"type": "array"}, {"type": "null"}]},
    "rendered_text":           {"type": "string"},
    "structured":              {"oneOf": [{"type": "object"}, {"type": "array"}, {"type": "null"}]},
    "artifact_id":             {"type": ["string", "null"]},
    "approval_required":       {"type": "boolean"},
    "sources_used":            {"type": "array", "items": {"type": "string"}},
    "disclosure_applied": {
      "oneOf": [
        {
          "type": "array",
          "maxItems": 0,
          "description": "[] — engine not engaged: Judgment did not run OR nothing withheld. Reserved meaning; do NOT use for populated results."
        },
        {
          "type": "object",
          "description": "Judgment ran. Object form = full Judgment engine evaluation result. CONVERGENCE NOTE (App M1 #70277): live code produces three shapes — (1) [] via _disclosure_state()-fallback paths, (2) this dict via _disclosure_state() @server.py:1581, (3) list-of-{field,withheld,reason|policy_tuple} via disclosure_demo/compose path @16885. Shapes (2) and (3) must converge to this object form before v0.1 PRODUCTION. Until then, renderers should handle all three gracefully.",
          "required": ["mode", "policy_version", "receiver_kind", "context"],
          "properties": {
            "mode":                {"type": "string", "enum": ["external", "internal", "relay", "direct"]},
            "policy_version":      {"type": "string", "description": "Disclosure policy schema version, e.g. '0.1'"},
            "receiver_kind":       {"type": "string", "enum": ["family", "friend", "manager", "report", "investor", "client", "public", "internal"]},
            "context":             {"type": "string", "description": "Interaction context from caller.disclosure_mode"},
            "gating_reason":       {"type": ["string", "null"]},
            "redacted_fields":     {"type": "array", "items": {"type": "string"}},
            "pii_classes_blocked": {"type": "array", "items": {"type": "string"}},
            "rule_id":             {"type": ["string", "null"]},
            "spillover_risk":      {"type": "number", "minimum": 0.0, "maximum": 1.0}
          }
        }
      ]
    },
    "absent_knowledge_caveats":{"type": "array", "items": {"type": "string"}},
    "suggestions":             {"type": "array"},
    "confidence":              {"type": "number", "minimum": 0.0, "maximum": 1.0},
    "signals":                 {"type": "array", "items": {"type": "string"}},
    "cognitive_mode": {
      "type": "object",
      "required": ["cycle", "tone", "preferred_kinds", "avoid", "hour_pt", "weekday", "iso_pt", "rendering_hint"],
      "properties": {
        "cycle":           {"type": "string"},
        "tone":            {"type": "string"},
        "preferred_kinds": {"type": "array", "items": {"type": "string"}},
        "avoid":           {"type": "array", "items": {"type": "string"}},
        "hour_pt":         {"type": "integer"},
        "weekday":         {"type": "string"},
        "iso_pt":          {"type": "string"},
        "rendering_hint":  {"type": "string"},
        "length":          {"type": "string", "description": "Rendering length policy compiled from briefing_style_by_mode.md (v1.0.1 — optional, promoted to required in v1.1)"},
        "structure":       {"type": "string", "description": "Structure policy (prose | bullets | mixed)"},
        "decisions":       {"type": "string", "description": "Decision-surfacing policy for this mode"},
        "max_words":       {"type": "integer", "description": "Hard cap on rendered output word count"},
        "policy_source":   {"type": "string", "description": "Artifact path for the briefing_style_by_mode.md that produced this policy"},
        "user_mode_axes":  {"type": "object", "description": "6-axis input mode (WHAT/WHERE/WHEN/HOW/WHY/HOW_MUCH). WHEN populated from clock; other axes nullable hooks for from_kind=user_mode to fill."}
      }
    },
    "expectedness": {
      "type": "object",
      "required": ["score", "rationale", "surface_recommendation"],
      "properties": {
        "score":                  {"type": "number", "minimum": 0.0, "maximum": 1.0, "description": "Continuous novelty score; lower = more expected/routine"},
        "rationale":              {"type": "string"},
        "surface_recommendation": {"type": "string", "enum": [
          "SUPPRESS",
          "SUPPRESS_OBVIOUS_SURFACE_NOVEL",
          "SURFACE_NOVEL",
          "SURFACE_INTERESTING_IF_TRUE",
          "SURFACE_AS_IS"
        ]},
        "framing_hint":           {"type": "string", "description": "Optional renderer framing suggestion"}
      }
    },
    "_inference_origin":       {"type": "array", "items": {"type": "string"}, "description": "Optional. Field names in result whose value originated from inference.db (vs deterministic count or raw observed). Lets renderers distinguish LLM-derived fields from algorithmic ones. e.g. ['tie_strength', 'tie_trend', 'info_quality']"},
    "_valid_until":            {"type": ["string", "null"], "format": "date-time", "description": "v1.1-planned. For from_kinds returning inference claims: the validity window of the primary claim (from inference.db valid_until). NOT emitted live — _get_inference_claims does not SELECT valid_until. Null until handler ships."},
    "_prior_id":               {"type": ["integer", "null"], "description": "v1.1-planned. For inference SELECT envelopes: the prior_id chain pointer if this claim supersedes a previous one. NOT emitted at envelope level live — prior_id is per-claim only inside claims[]. Non-null enables renderers to display supersession history."},
    "judgment_applied": {
      "type": "object",
      "description": "v1.1-planned. Evidence that Judgment ran (AOR-2 audit trail / GDPR Article 30). NOT emitted live — Judgment integration not yet wired. When absent, disclosure_applied: [] is indistinguishable from Judgment not running; judgment_applied closes that gap.",
      "properties": {
        "version":        {"type": "string"},
        "decision":       {"type": "string", "enum": ["ALLOW", "HOLD", "WARN"]},
        "policy_version": {"type": "string"},
        "rule_id":        {"type": ["string", "null"], "description": "Matched rule_id from policy, or null if default-hold (no rule match)"}
      }
    },
    "error":                   {"type": "string", "description": "Present when ok=false only"},
    "hint":                    {"type": "string", "description": "Present with error, not on success"},
    "is_retrieved_content":    {"type": "boolean", "description": "true when from_kind ∈ {email, message, note, recall, file, image}"},
    "safety_warning":          {"type": "string", "description": "Present when safety.review() returns WARN"},
    "provenance": {
      "type": "object",
      "description": "Article 50 provenance triple",
      "properties": {
        "producer":  {"type": "string"},
        "channel":   {"type": "string"},
        "audience":  {"type": "string"},
        "ts":        {"type": "string", "format": "date-time"}
      }
    },
    "_compression": {
      "type": "object",
      "description": "Present when envelope was compressed by local Qwen 14B (>24,000 chars)",
      "properties": {
        "applied":          {"type": "boolean"},
        "original_chars":   {"type": "integer"},
        "compressed_chars": {"type": "integer"},
        "model":            {"type": "string"},
        "provider_used":    {"type": "string"},
        "fallback_used":    {"type": "boolean"}
      }
    }
  }
}
```

### 4.2 Field semantics

| Field | Mandatory | Notes |
|-------|-----------|-------|
| `ok` | yes | true = inner call succeeded |
| `verb` | yes | Echo of requested verb |
| `result` | yes | Raw inner output. `{}` on error. On compression: `{"compressed_summary": str, "_original_result_was_compressed": true}` |
| `rendered_text` | yes | Human-readable prose; respect `max_chars`. Empty on error |
| `structured` | yes | Machine-readable payload; preserved unmodified even on compression |
| `artifact_id` | no | Present when a durable artifact was created |
| `approval_required` | yes | True when result contains user-confirmable write |
| `sources_used` | yes | List of source identifiers contributing to result |
| `disclosure_applied` | yes | Current disclosure state |
| `absent_knowledge_caveats` | yes | Hedges Mandaire flagged. **Surface these to the user.** |
| `suggestions` | yes | Advisory follow-up call shapes. Hints only — do not auto-fire |
| `confidence` | yes | Envelope-level [0.0–1.0]. Drives hedging in rendered output |
| `signals` | yes | Evidence trail for `confidence` score |
| `cognitive_mode` | yes | Always present. Renderer MUST adapt output shape to this |
| `expectedness` | yes | Object: `score` (float 0–1), `surface_recommendation` (SUPPRESS / SUPPRESS_OBVIOUS_SURFACE_NOVEL / SURFACE_NOVEL / SURFACE_INTERESTING_IF_TRUE / SURFACE_AS_IS), `rationale`, optional `framing_hint` |
| `error` | conditional | Present when `ok=false` |
| `hint` | conditional | Present with error |
| `is_retrieved_content` | conditional | true when from_kind ∈ {email, message, note, recall, file, image}. Threat model B marker — body items also receive sentinel wrapping |
| `safety_warning` | conditional | Defamation disclaimer when safety.review()=WARN |
| `provenance` | conditional | Article 50 triple (producer, channel, audience, ts) |
| `_compression` | conditional | Present when local Qwen compressed the envelope |

### 4.3 Confidence scoring

Additive/subtractive from 0.40 base:

| Driver | Delta |
|--------|-------|
| Base (success) | +0.40 |
| Per source_used (up to 3) | +0.15 each |
| `result.found == True` | +0.10 |
| `result.provenance` in `{user_authored, user_confirmed, observed}` | +0.20 |
| `result.sample_size >= 10` | +0.05 |
| `result.provenance` in `{ai_inference, auto_derived, llm_inferred}` | −0.15 |
| `result.provenance == derived_index` | 0.00 (neutral — deterministic index output, not user-confirmed truth) |
| `result.found == False` | −0.30 |
| Per absent_knowledge_caveat (up to 3) | −0.10 each |
| Error response | 0.0, signals=["error_response"] |

Inner result may carry its own `confidence` float; envelope takes `max(computed, inner)`.

**Two distinct namespaces (v1.0.6):** `source_kind` and `provenance` are NOT the same field.

- `source_kind` — `inference.db.inferences` column. CHECK constraint: exactly `{observed, derived_index, llm_inferred, user_authored}`. Dominant value: `derived_index` (92.8% of 503 live rows — personality_v0.1_deterministic, probabilistic_pair_match.v0.1); llm_inferred 4.6%, observed 2.6%, user_authored 0. Values `user_confirmed`, `ai_inference`, `auto_derived` do not appear in inference.db and are blocked by constraint.

- `provenance` — envelope field emitted by each from_kind handler. The scorer reads `result.get("provenance")` (server.py:8616). Current handlers (5695, 5739) hardcode `"ai_inference"` regardless of underlying source_kind. **Pending change (Inference #70261):** handlers will emit per-source_kind provenance per the intent mapping below. Until that ships, every inference_claim/entity_state read receives −0.15.

**Intent mapping (post-handler-fix, Inference + Surface co-ratified):**

| source_kind in inference.db | handler emits provenance | scorer delta |
|---|---|---|
| `observed` / `user_authored` | `observed` / `user_authored` | +0.20 |
| `derived_index` | `derived_index` | 0.00 (neutral) |
| `llm_inferred` | `llm_inferred` | −0.15 |

Dead wire values (`user_confirmed`, `ai_inference`, `auto_derived`): valid in scorer table for non-inference callers; handlers must never emit them for inference.db-sourced reads.

**Multi-claim provenance aggregation rule (`inference_claim` from_kind, shipped commit 187de03):**

`inference_claim` returns N claims, each carrying its own `source_kind`. The envelope-level `provenance` is a single value — a coarse trust signal. The handler resolves it by taking the **most conservative** (weakest) source_kind present across all returned claims:

| Ranking (weakest → strongest) | Delta |
|---|---|
| `llm_inferred` | −0.15 |
| `derived_index` | 0.00 |
| `observed` / `user_authored` | +0.20 |

One `llm_inferred` claim anywhere in the set pulls the entire envelope to −0.15. Rationale: the envelope confidence must never over-promise — conservative aggregation prevents a mostly-observed result from being reported at +0.20 when it contains a single LLM-derived claim.

Per-claim `source_kind` is preserved in `result.claims[].source_kind` for granular per-claim hedging by renderers that need finer resolution.

`count=0` → `provenance=None` → 0.00 delta (neutral; no `found=false` signal on this handler, so no −0.30).

### 4.4 Compression

Large envelopes (>24,000 chars) are compressed by local Qwen 14B before returning. Exempt from_kinds (renderer needs IDs for follow-up calls): `person`, `relationship`, `household`, `social_graph`, `entity_state`, `personality_profile`, `inference_claim`, `email`, `event`, `decision`, `compare_options`, `topic`, `context`, `file`, `outbound_style`, `user_mode`, `surfacing_score`.

### 4.5 Provenance (Article 50)

Every envelope carries a provenance triple via `attach_provenance()` from `core.mandaire.transport.provenance`. Fail-open: attachment failure never blocks response.

### 4.6 Safety shim

Every external MCP response runs through `safety.review()` at `_instrumented_call_tool`.

| Result | Wire behavior |
|--------|--------------|
| PASS | Result transmitted unchanged |
| WARN | `safety_warning` field added; result transmitted |
| HOLD | Result replaced with `{"error": "response blocked by safety review", "blocked_reason": "...", "disclosure_applied": []}` |

**S2/S3 HOLD suppression (Judgment integration, v0.1 PRODUCTION):** When Judgment's disclosure-policy gate fires a HOLD on S2/S3 sensitivity topics (SENSITIVE/RESTRICTED from_kinds), the external response MUST equal the `NO_INFO_RESPONSE` template byte-for-byte — identical to a GENUINE_UNKNOWN response. `blocked_reason` and `block_class` MUST NOT appear in the external response for S2/S3 HOLD (they go to the audit log only). This preserves the 8-property pooling checklist Property 1: S2/S3 topic existence must be non-distinguishable from the wire.

The existing safety content HOLD (STANDARD/S1 topics, fired by safety.review()) MAY retain `blocked_reason` in the external response — the suppression requirement applies only to S2/S3 HOLD paths. See §12.4.

---

## 5. ai_observation — INSERT Payload Schema

```json
{
  "$schema": "https://json-schema.org/draft/2020-12",
  "title": "AiObservationInsertPayload",
  "type": "object",
  "required": ["claim", "confidence", "trigger_reason"],
  "properties": {
    "claim": {
      "type": "string",
      "description": "The observation text. PII-detected: SSN/payment_card/password/api_key/otp patterns are blocked."
    },
    "confidence": {
      "type": "number",
      "minimum": 0.0,
      "maximum": 1.0,
      "description": "Confidence in the claim."
    },
    "trigger_reason": {
      "type": "string",
      "enum": ["user_direct_statement", "ai_synthesis", "nil_result_writeback", "user_confirmed_prior"],
      "description": "Why this observation is being written."
    },
    "claim_tier": {
      "type": "string",
      "enum": ["factual", "behavioral", "preference", "preference_negative", "boundary"],
      "description": "Claim classification tier."
    },
    "entity_refs": {
      "type": "array",
      "items": {"type": "object"},
      "description": "Entity references for cross-linking to people.db. Each item: {name?, email?, phone?, uuid?, entity_id?}. If both uuid and entity_id supplied, uuid wins. Integer entity_id is best-effort resolution-time and may rotate across ER rebuilds. Email/phone aliases are stable across rebuilds and recommended for cross-rebuild durability."
    },
    "evidence_refs": {
      "type": "array",
      "items": {"type": "string"},
      "description": "Source references supporting the claim."
    },
    "writeback_slot_id": {
      "type": ["string", "null"],
      "description": "If filling a nil-result slot, the slot ID from open_writeback_slots."
    },
    "property": {
      "type": ["string", "null"],
      "description": "Property name the claim is about (e.g. 'relationship_quality', 'preference_food')."
    },
    "subject_kind": {
      "type": ["string", "null"],
      "description": "Subject type: person | relationship | self | topic"
    },
    "subject_key": {
      "type": ["string", "null"],
      "description": "UUID of the subject entity (for person/relationship subjects)."
    }
  }
}
```

**INSERT response (ai_observation):**

```json
{
  "ok": true,
  "verb": "INSERT",
  "result": {
    "id": "<integer inference.db row id>",
    "kind_stored": "AI_OBSERVATION",
    "tier": "high | medium | low",
    "user_visible": "<boolean: confidence >= 0.65>",
    "claim_tier": "<string>",
    "claim_source": "<string>",
    "source_attribution": "<string>",
    "pending_review_until": "<ISO 8601 or null>",
    "entity_resolution_status": "<object>",
    "dedup": "<boolean: true if this is a duplicate of existing active row>",
    "claim_source_source": "<string — classifier that produced claim_source>",
    "claim_type_source": "<string — classifier that produced the claim type/tier>",
    "verification_state": "<string — e.g. 'pending_review' | 'verified' | 'rejected'>",
    "claim_signature": "<string — dedup hash: sha256(claim_text + affects); present on both new and dedup rows>"
  },
  "artifact_id": "<inference.db row id as string>"
}
```

**Constraints:**
- `purpose` required (e.g. `ai_writeback_from_conversation`). Declared at the request envelope level (`mandaire(..., purpose="...")`), not inside the INSERT payload — the server reads it from the outer call args.
- Cross-user writeback blocked in v1.0 (single-tenant)
- Double-write guard: same claim_signature + active = dedup, returns existing_id with `dedup=true`
- `classifier_version` = `mcp_ai_observation_v0.1` stamped automatically

**SELECT ai_observation:**

```json
mandaire(verb="SELECT", from_kind="ai_observation", from_match="<query>", filters={"limit": 20})
```

Returns array of inference rows matching the query via recall_search.

---

## 6. decision — SELECT Schema (Audit Click-Through)

```json
mandaire(verb="SELECT", from_kind="decision", from_id="<audit_ref_uuid>")
```

**Output:**

```json
{
  "ok": true,
  "verb": "SELECT",
  "result": {
    "from_kind": "decision",
    "audit_ref": "<uuid>",
    "found": true,
    "evaluations": [
      {
        "decision": "<text>",
        "recipient": "<name>",
        "decision_date": "<ISO 8601>",
        "confidence": 0.85,
        "source_kind": "user_authored",
        "evidence_refs": []
      }
    ]
  }
}
```

If `from_id` not provided, returns recent decisions list (same as `from_kind=recent_decisions`).

---

## 7. situation_brief — SELECT Schema

**Required:** `from_match` (or `from_id` / `ask`) containing the topic string.

```json
mandaire(
  verb="SELECT",
  from_kind="situation_brief",
  from_match="tesla roof leak",
  filters={"detail": "standard"}
)
```

**`filters.detail` values:**

| Value | Token target | Char cap |
|-------|-------------|----------|
| `brief` | 400 | 1,600 |
| `standard` (default) | 800 | 3,200 |
| `full` | 1,500 | 6,000 |

**Output (`result` field):**

```json
{
  "title": "<situation title>",
  "status": "active_unresolved | resolved | not_found | unknown",
  "confidence": 0.85,
  "signals": ["source: analysis_situation_lookup (2026-05-19)"],
  "key_facts": [
    {
      "claim": "<fact text, max 200 chars>",
      "confidence": 0.9,
      "source_kind": "user_authored | observed | ai_inference",
      "source_refs": ["<source identifier>"]
    }
  ],
  "open_items": [
    {"item": "<text>", "priority": "high | medium | low"}
  ],
  "recent_events": [
    {"ts": "<ISO 8601>", "event": "<text, max 100 chars>", "source": "<source>"}
  ],
  "key_people": [
    {"name": "<name>", "role": "<role>", "uuid": "<stable uuid or null>", "entity_id": "<integer, best-effort, may rotate across ER rebuilds>"}
  ],
  "suggested_next_action": "<text>",
  "absent_knowledge_caveats": ["<caveat>"],
  "disclosure_applied": [],
  "_detail_level": "standard",
  "_version": "v0.2"
}
```

**Primary path:** Analysis `situation_lookup()` (situations.db, ~200ms warm). Falls back to pre-built context doc (topic_corpus), then recall_search (may be 8-15s cold if SentenceTransformer not warm).

**Sources:** `analysis_situation_lookup` > `topic_corpus` > `recall`

**Suggestions always included:**
```json
[
  {"verb": "SELECT", "from_kind": "situation_brief", "from_match": "<topic>", "filters": {"detail": "full"}},
  {"verb": "SELECT", "from_kind": "context", "from_match": "<topic>"}
]
```

---

## 8. catch_me_up — SELECT Schema

```json
mandaire(verb="SELECT", from_kind="catch_me_up")
```

Optional filters:
- `filters.include_silence=true`: include outbound_silence + followup_due (adds ~100-300ms)
- `filters.decisions_limit=<int>`: cap recent_decisions (default 8)
- `filters.event_days=<int>`: upcoming_events lookahead in days (default 7)
- `filters.threads_limit=<int>`: live_threads count (default 15)
- `filters.mailing_lists_limit=<int>`: live_mailing_lists count (default 10)

**Output (`result` field):**

```json
{
  "ok": true,
  "version": "v0.2.2-2026-05-15-commitment-resolution",
  "live_tasks": [
    {"title": "<task>", "status": "<status>", "tags": [], "deadline": "<ISO 8601 or null>"}
  ],
  "filtered_out_stale_tasks": {
    "count": 2,
    "reason": "deadline_passed or likely_resolved (corpus evidence)",
    "items": []
  },
  "recent_drifts": [],
  "active_threads": {},
  "recent_decisions": [],
  "upcoming_events": [],
  "outbound_silence": [],
  "followup_due": [],
  "open_commitments_outbound": [],
  "open_commitments_inbound": [],
  "recently_resolved_commitments": [],
  "live_threads": [
    {
      "entity_id": "<uuid>",
      "name": "<name>",
      "channels": [],
      "decay_score": 0.85,
      "last_contact": "<ISO 8601>"
    }
  ],
  "live_mailing_lists": [],
  "stale_filter_metric": {
    "total_tasks_in_pending": 10,
    "live_count": 8,
    "stale_count": 2
  },
  "cache_hit": false
}
```

**Cache:** 5-minute TTL (write-invalidation stamp). `cache_hit=true` when cache served.

**Sources:** `pending_tasks`, `comms`, `topic_corpus`, `calendar`, `task_resolution_v0_2`, `commitments_v0_1`, `active_threads_decay_v0_2`, `people_aliases`

**Note for renderer:** Narrate with confidence markers (CONFIRMED / OBSERVED / STALE / GAP).

---

## 9. entity_lookup (via from_kind=person) — Output Schema

Reached via `from_kind=person` with `from_match=<name|email|phone>`.

**Input:**

```json
mandaire(verb="SELECT", from_kind="person", from_match="Bala", purpose="researching_history")
```

**Candidate list (within result):**

```json
{
  "found": true,
  "candidates": [
    {
      "entity_id": 10522,
      "name": "Balaji T Kuppuswamy",
      "uuid": "<uuid>",
      "type": "person | org",
      "has_profile": true,
      "interactions": 548,
      "sources": ["gmail", "imessage"]
    }
  ],
  "interaction_total": 548
}
```

**Resolution precedence:**
1. Exact email match (aliases.alias_type='email')
2. Exact phone match (aliases.alias_type='phone', normalized)
3. Exact name match
4. Multi-token LIKE match
5. Prefix LIKE match (single token ≥3 chars)
6. name_variant alias match
7. Contact-tagged entities win ties at every preceding step — an entity with a contact tag ranks above an untagged entity at the same precedence level.

**Merged-entity guard:** Rows with `name.startswith("[MERGED")` or `notes` containing `"merged_into"` are excluded from candidates.

**Identity contract:** `uuid` is the stable identity; `entity_id` (integer) is NOT invariant across merges.

---

## 10. Auth

### 10.1 Required headers (MCP/HTTP)

| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | yes | `Bearer at_<40-byte-urlsafe-token>` |

### 10.2 OAuth 2.1 + PKCE flow

```
1. Discovery:  GET {issuer}/.well-known/openid-configuration
2. DCR:        POST {issuer}/oauth/register → {client_id, client_secret}
3. Authorize:  GET {issuer}/oauth/authorize?...&code_challenge=...&code_challenge_method=S256
               (Authelia SSO gates this; code prefix: "ac_", TTL: 600s)
4. Exchange:   POST {issuer}/oauth/token (grant_type=authorization_code)
               → {"access_token": "at_...", "expires_in": 86400, "refresh_token": "rt_..."}
5. Refresh:    POST {issuer}/oauth/token (grant_type=refresh_token)
6. Revoke:     POST {issuer}/oauth/revoke
```

### 10.3 Token TTLs

| Token | Prefix | TTL |
|-------|--------|-----|
| Access token | `at_` | 24h |
| Refresh token | `rt_` | 30 days |
| Auth code | `ac_` | 10 minutes |

### 10.4 Scope

**v1.0.13 — 9 supported scopes (ADR-0199):**

| Scope | Class | Description |
|-------|-------|-------------|
| `mandaire` | legacy full-read | Default (shim period); equivalent to all read scopes |
| `mandaire-v2` | legacy full-read | Post-shim target scope |
| `mandaire_health` | legacy full-read | Health-only legacy; maps to `health:read` semantics |
| `career:read` | granular read | Career, employment, professional kinds |
| `finances:read` | granular read | Financial, tax, investment kinds |
| `general:read` | granular read | Relationships, contacts, calendar, general kinds |
| `health:read` | granular read | Health, medical, biometric kinds (SENSITIVE/RESTRICTED) |
| `observations:read` | granular read | AI observations, inferred claims |
| `write:internal` | write | Correction inserts, observation writes (`mandaire_write` tool) |

Legacy scopes (`mandaire`, `mandaire-v2`, `mandaire_health`) continue to be accepted and treated as full-read during the shim period. Granular read scopes enable per-call minimum-privilege requests; Execution enforcement gate ships shadow-first (§5.4). The `scope_for_topic(topic)` helper returns the correct `WWW-Authenticate` scope value for a given topic group.

### 10.5 Machine token (M2M — ADR-0139 B3)

For headless agents needing a JWT without a user OAuth flow:

```
POST /auth/machine/token
Content-Type: application/json
{"grant_type": "client_credentials", "client_id": "<id>", "client_secret": "<secret>"}

→ {"access_token": "<ES256 JWT>", "token_type": "Bearer", "expires_in": 3600}
```

JWT issuer: `https://mcp.mandaire.app`. Algorithm: ES256. Valid for 1h (not 24h).

### 10.6 JWKS endpoint (ADR-0139 B1)

Public key for JWT validation:

```
GET /.well-known/jwks.json
→ {"keys": [{"kty": "EC", "crv": "P-256", "kid": "...", "x": "...", "y": "..."}]}
```

Callers SHOULD cache the JWKS for the JWT's TTL; re-fetch on `kid` mismatch.

### 10.7 SDK endpoint — agent-auth (ADR-0141 S-O5, internal only)

Privileged full-fidelity read endpoint for trusted internal agents (Chief, Briefing, david-cli):

```
GET /sdk/v1/select       (port 8765, tailnet-only; NOT exposed externally)
X-Mandaire-Agent-CN: chief    ← set by nginx mTLS frontend on port 8768
```

- CN absent → 403. CN not in `{chief, briefing, david-cli}` → 403.
- Returns full-fidelity `mandaire_read` response (disclosure-axis fields stripped before response).
- Port 8768 nginx mTLS frontend: pending Mandaire CA bootstrap (David at terminal on mnd-infra). Per-agent leaf certs required: chief, briefing, david-cli.
- Until port 8768 is live, internal agents may call port 8765 directly with `X-Mandaire-Agent-CN` set (tailnet-only; no mTLS enforcement until CA is up).

### 10.8 SDK signal endpoint — control-plane signals (ADR-0233 D1, internal only)

Cross-VM diagnostic and feedback endpoint for mnd-app-side principals (SDK agents):

```
POST /sdk/v1/signal       (port 8765, tailnet-only; NOT exposed externally)
Content-Type: application/json
X-Mandaire-Agent-CN: mnd-app-david    ← set by nginx mTLS frontend

Body: {"from_kind": "diagnostic", "payload": {...}}

→ {"status": "recorded", "from_kind": "diagnostic"}
```

**CN gate:** Must match pattern `mnd-app-[a-z0-9][a-z0-9-]*` (or explicit `MANDAIRE_SDK_SIGNAL_CN_ALLOWLIST` env list). CN absent or non-matching → 403.

**`from_kind` discriminator:** Only `diagnostic` permitted by default. Expandable via `MANDAIRE_SDK_SIGNAL_KINDS` env (comma-separated). Unknown kind → 400.

**Rationale (ADR-0233):** Separates cross-VM control-plane signals (health probes, telemetry) from the data plane (`/sdk/v1/select`). Avoids overloading the SELECT endpoint with write/signal semantics. Single endpoint, `from_kind`-discriminated, so future signal kinds (feedback, healthcheck, alert) extend without new routes.

### 10.9 Requester key discovery (SEC-070 D2)

Publicly-discoverable Ed25519 signing key for cross-tenant request authentication (ADR-0224 prerequisite):

```
GET /.well-known/mandaire-requester-key.json    (no auth, public)

→ HTTP 200 + Cache-Control: public, max-age=3600
{
  "kty": "OKP",
  "crv": "Ed25519",
  "use": "sig",
  "kid": "<key-id>",
  "x": "<base64url-encoded public key>"
}

→ HTTP 404 when no key has been generated yet (OS D1 prerequisite not met)
```

**Purpose:** Cross-tenant peers resolve this endpoint to verify that incoming request payloads were signed by this Mandaire instance. Used by the five ADR-0224 cross-user verbs (`request_context`, `offer_context`, `evaluate_disclosure`, `claim_context`, `acknowledge_disclosure`) — those verbs are **not yet implemented** (gated on Architecture layer-3 authorization).

**Key lifecycle:** OS layer generates and rotates key material; Surface reads and serves the current JWK file. `kid` is derived from a SHA-256 fingerprint of the public key (first 16 hex chars). Cache-Control of 1h prevents stale serving during rotation.

**Live:** `https://mcp.mandaire.com/.well-known/mandaire-requester-key.json` and `https://mcp.mandaire.app/.well-known/mandaire-requester-key.json`

---

## 11. Error Codes

### 11.1 MCP-level errors (HTTP status)

| Status | Meaning |
|--------|---------|
| 401 | Missing or expired Bearer token |
| 403 | Valid token but insufficient scope |
| 400 | Malformed JSON / missing required field |
| 500 | Server error (see error envelope) |

### 11.2 Application-level errors (ok=false in envelope)

| error value | Trigger |
|-------------|---------|
| `"from_kind required"` | verb=SELECT and no from_kind or ask |
| `"from_match required for from_kind=situation_brief"` | situation_brief without topic |
| `"ask required for from_kind=recall"` | recall without ask |
| `"purpose required for SENSITIVE from_kind"` | SENSITIVE kind, purpose absent |
| `"purpose required for RESTRICTED from_kind"` | RESTRICTED kind, purpose absent |
| `"unknown from_kind: <value>"` | from_kind not in _VALID_FROM_KINDS catalog |
| `"payload.claim required"` | ai_observation INSERT, claim absent |
| `"payload.confidence required (0.0-1.0)"` | ai_observation INSERT, confidence absent |
| `"payload.trigger_reason required"` | ai_observation INSERT, trigger_reason absent |
| `"CROSS_USER_WRITEBACK_BLOCKED"` | ai_observation INSERT with mismatched user targets |
| `"pii_pattern_rejection"` | ai_observation INSERT, PII detected in claim |
| `"response blocked by safety review"` | safety.review()=HOLD (STANDARD/S1 topics — `blocked_reason` returned) |
| `"response blocked by disclosure policy"` | Judgment gate HOLD on S2/S3 topics — external response = NO_INFO_RESPONSE template; `blocked_reason` suppressed (see §12.4 + §4.6) |

**block_class field (Judgment integration, v0.1 PRODUCTION):** Error envelopes include `block_class` (string) when Judgment is wired. Values: `"safety_content" | "disclosure_policy" | "safety_tier_gate"`. For S2/S3 `disclosure_policy` blocks, `block_class` goes to audit log only — NOT returned to the external caller.

### 11.3 A2A error codes

| Code | Meaning |
|------|---------|
| -32001 | Auth failure (bad/missing shared secret) |
| -32002 | Hop limit exceeded |
| -32003 | Rate limit exceeded |
| -32601 | Method not implemented / unknown skill |
| -32602 | Missing required param |
| -32603 | Skill execution failed |

---

## 12. Safety Tier Gate (R2)

Applied at `mandaire()` entry, after verb validation, before any handler runs.

### 12.1 Tiers

| Tier | from_kinds | Behavior |
|------|-----------|----------|
| RESTRICTED | `disclosure_policy`, `correction_history`, `personality_profile`, `tax_event`, all 7 health kinds (`latest_vitals`, `vitals_trend`, `lab_results`, `medications_active`, `immunizations`, `ecg_summary`, `conditions`) | `purpose` required. Audit-logged (log.warning). Safety authorization callback deferred. (11 kinds total as of v1.0.9) |
| SENSITIVE | ~30 kinds (see §3.2 tier column) | `purpose` required. Error envelope if absent. |
| STANDARD | ~55 kinds | No gate. |

**SSOT gate discipline:** The gate implementation MUST derive RESTRICTED/SENSITIVE/STANDARD tier from the `tier` column in §3.2, not from a hardcoded enumeration of the table above. §3.2 is authoritative; §12.1 is a human-readable summary. Any divergence between them is a spec-publishing error — §3.2 wins.

### 12.2 Depth-conditional escalation

`person`, `household`, `relationship` at `depth="deep"` or `depth="exhaustive"` → treated as SENSITIVE regardless of catalog tier.

### 12.3 Purpose check

Presence-only check. Not validated against an enum. Suggested values: `preparing_for_meeting`, `drafting_reply`, `researching_history`, `evaluating_relationship`, `disambiguating`, `ai_writeback_from_conversation`.

### 12.4 Judgment disclosure-policy gate (v0.1 PRODUCTION — forward design)

Judgment's gate fires **pre-fetch** (before any handler runs). This is distinct from the safety.review() shim (§4.6) which fires post-handler. These are different failure modes with different caller remediation paths.

**block_class enum** — appears in error envelopes when Judgment is wired:

| block_class | Trigger | Caller remediation |
|---|---|---|
| `safety_content` | safety.review() HOLD (§4.6, post-handler) | No recourse |
| `disclosure_policy` | Judgment gate HOLD (pre-fetch, S2/S3 topic) | May provide additional purpose, context, or authorization token |
| `safety_tier_gate` | Tier gate enforcement: SENSITIVE/RESTRICTED without `purpose` | Include `purpose` field in request |

**S2/S3 HOLD external response:** byte-equal to `NO_INFO_RESPONSE` template (see §4.6 S2/S3 suppression note). `block_class` and `blocked_reason` go to audit log only — NOT returned to the external caller. Existence of S2/S3 content must be non-distinguishable from GENUINE_UNKNOWN on the wire.

**Error envelope shape for Judgment-integrated blocks:**

Non-S2/S3 blocks (`safety_tier_gate` + non-sensitive `disclosure_policy`):
```json
{
  "ok": false,
  "error": "response blocked by disclosure policy",
  "block_class": "disclosure_policy",
  "hint": "Provide additional purpose or authorization context.",
  "disclosure_applied": []
}
```

S2/S3 `disclosure_policy` HOLD — `block_class` and `blocked_reason` OMITTED from external response (audit log only); caller cannot distinguish from GENUINE_UNKNOWN:
```json
{
  "ok": false,
  "error": "response blocked by disclosure policy",
  "disclosure_applied": []
}
```

---

## 13. A2A Protocol

**Wire owner:** Surface (AOR-2). Catalog SSOT: `domains/surface/catalog/a2a_skills.md`.  
**Version:** 0.1.0  
**Base URL:** `https://research.mandaire.com/a2a`

### 13.1 Endpoints

| Method | Path | Auth | Notes |
|--------|------|------|-------|
| GET | `/.well-known/agent-card.json` | None | Agent discovery |
| POST | `/a2a` | Bearer shared secret | Research + Inference skills |
| POST | `/a2a/enrichment/` | Bearer shared secret | Enrichment skills (co-hosted) |
| GET | `/healthz` | None | Liveness probe |

#### 13.1.1 Co-hosting model

Three skill namespaces share one Research server (port 8095). Routing by sub-path and `skill` prefix:

| Namespace | Path | Route key |
|-----------|------|-----------|
| Research | `/a2a` | `skill` prefix `research.*` |
| Inference | `/a2a` | `skill` prefix `inference.*` (co-hosted 2026-06-10) |
| Enrichment | `/a2a/enrichment/` | sub-path; handlers pending Enrichment layer wiring |

Future layers (Data, Analysis, Production, Graph, Security, Tools) will each host their own A2A server when skills promote from STUB. Until then, skills in those namespaces are registered as STUB with no live server.

### 13.2 Request envelope

```json
{
  "jsonrpc": "2.0",
  "method": "message/send",
  "params": {
    "skill": "<namespace>.<skill_id>",
    "message": {"parts": [{"kind": "text", "text": "..."}]},
    "metadata": {}
  },
  "id": "<caller-generated id>"
}
```

**Required headers:** `Authorization: Bearer <shared_secret>`  
**Optional:** `X-A2A-Trace-Id`, `X-A2A-Parent-Trace`, `X-A2A-Hop-Count`, `X-Mandaire-Caller`

**Response (`result` field on success):**
```json
{"kind": "message", "parts": [{"kind": "text", "text": "<structured response>"}]}
```

Error responses follow JSON-RPC 2.0 — `{"code": <int>, "message": "<str>"}` in the `error` field. See §11.3 for codes.

### 13.3 Registered skill catalog

**Status taxonomy:** LIVE = handler active; STUB = registered, no live handler; CANDIDATE = design-ready, gated on prerequisite; PLANNED = gated on auth upgrade (v0.4 OAuth required).

**Totals (2026-06-13):** 5 LIVE · 23 STUB · 3 CANDIDATE · 1 PLANNED across 10 namespaces (32 registered skills).

#### Research — `/a2a` (skill prefix `research.*`)

| Skill | Status | Cost class | Notes |
|-------|--------|------------|-------|
| `research.recall_search` | LIVE | free | FTS + semantic search over recall.db |
| `research.entity_lookup` | LIVE | free | people.db + relationship_graph profile |
| `research.inferred_asset_query` | LIVE (2026-06-06) | free | web_enrichment.db, entity profiles, surprising_facts, observations.db per-source |
| `research.web_enrich` | LIVE | claude_sonnet (~90s) | WebSearch + WebFetch → web_enrichment.db |
| `research.web_browse` | STUB | — | |
| `research.synthesize` | STUB | — | |

#### Inference — `/a2a` (skill prefix `inference.*`, co-hosted)

| Skill | Status | Notes |
|-------|--------|-------|
| `inference.claim_lookup` | LIVE (2026-06-10) | Query inference.db claims by subject/property; returns claim_text + value + CI + evidence_refs |
| `inference.calibration_check` | CANDIDATE | Gate: ≥5% GT-A coverage in inference.db |
| `inference.claim_store_health` | CANDIDATE | Activate together with `inference.claim_lookup` |

#### Enrichment — `/a2a/enrichment/` (co-hosted, handlers pending)

| Skill | Status | Notes |
|-------|--------|-------|
| `enrichment.enrich_entity` | STUB | Pending Enrichment layer server wiring |
| `enrichment.linkedin_enrich` | STUB | |
| `enrichment.cohort_enrich` | STUB | Cost gate required (caller must authorize spend) |
| `enrichment.freshness_sweep` | STUB | |
| `enrichment.coverage_check` | STUB | |

#### Data — no server yet

| Skill | Status |
|-------|--------|
| `data.source_freshness` | STUB |
| `data.canonical_schema` | STUB |
| `data.schema_coherence_audit` | STUB |
| `data.sdk_adoption_status` | STUB |
| `data.canonical_store_health` | STUB |
| `data.migration_guard_check` | STUB |

#### Analysis — no server yet

| Skill | Status |
|-------|--------|
| `analysis.contamination_scan` | STUB |
| `analysis.cohort_build` | STUB |

#### Entities — no server yet

| Skill | Status | Notes |
|-------|--------|-------|
| `entities.coverage_check` | CANDIDATE | Deferred pending ER rebuild stability |

#### Production — no server yet

| Skill | Status |
|-------|--------|
| `production.ops_health` | STUB |
| `production.cron_audit` | STUB |

#### Graph — no server yet

| Skill | Status | Notes |
|-------|--------|-------|
| `graph.relationship_context` | PLANNED | Gated on v0.4 OAuth (contains personal relationship data requiring caller auth) |
| `graph.coverage_check` | STUB | |

#### Security + Tools — no server yet

| Skill | Status |
|-------|--------|
| `security.pre_ship_review` | STUB |
| `security.risk_register` | STUB |
| `tools.oss_first_audit` | STUB |
| `tools.catalog_maintenance` | STUB |
| `tools.tool_ergonomics_review` | STUB |

#### Intentional non-participants

| Layer | Decision | Rationale |
|-------|----------|-----------|
| App | No A2A skills | Read surfaces already on `mandaire()` MCP wire; disclosure gate not live for external callers; re-survey at multi-user milestone |
| Chief | No A2A skills | All skills are David-personal or disclosure-gated; external A2A safe only post-Judgment enforcement |
| OS | No A2A skills | Internal fleet substrate; no external caller AOR |

**Wave 4 pending:** org, infra registration dispatches not yet sent.

### 13.4 Auth

| Version | Mechanism | Credential | Gate |
|---------|-----------|------------|------|
| v0.1 (active) | Shared bearer secret | `~/openclaw/Brain/credentials/a2a_research_v01.secret` | — |
| v0.4 (planned) | OAuth 2.1 client_credentials | TBD | Admit external/untrusted peer OR disclosure engine at ENFORCEMENT for cross-agent callers |

### 13.5 Agent Card

```json
GET https://research.mandaire.com/a2a/.well-known/agent-card.json

{
  "name": "Mandaire Research Agent",
  "version": "0.1.0",
  "description": "Mandaire personal AI — recall search, entity lookup, inferred asset queries, web enrichment, and inference claims",
  "url": "https://research.mandaire.com/a2a",
  "provider": {"organization": "Mandaire", "url": "https://mandaire.com"},
  "capabilities": {"streaming": false, "pushNotifications": false, "stateTransitionHistory": false},
  "authentication": {"schemes": ["Bearer"]},
  "skills": [
    {"id": "research.recall_search", "name": "Recall Search", "description": "FTS + semantic search across personal knowledge corpus", "inputModes": ["text"], "outputModes": ["text"], "tags": ["search", "recall", "semantic"]},
    {"id": "research.entity_lookup", "name": "Entity Lookup", "description": "Entity profile + relationship context from people.db + relationship_graph.db", "inputModes": ["text"], "outputModes": ["text"], "tags": ["entity", "relationship", "people"]},
    {"id": "research.inferred_asset_query", "name": "Inferred Asset Query", "description": "Derived and enriched asset lookup: web_enrichment.db, surprising_facts, observations.db", "inputModes": ["text"], "outputModes": ["text"], "tags": ["inference", "enrichment", "derived"]},
    {"id": "research.web_enrich", "name": "Web Enrichment", "description": "Live WebSearch + WebFetch enrichment for a named entity → web_enrichment.db", "inputModes": ["text"], "outputModes": ["text"], "tags": ["web", "enrichment", "live"]},
    {"id": "inference.claim_lookup", "name": "Claim Lookup", "description": "Query inference.db claims by subject/property with confidence + evidence_refs", "inputModes": ["text"], "outputModes": ["text"], "tags": ["inference", "knowledge", "claims"]}
  ]
}
```

### 13.6 Telemetry

```sql
-- ~/openclaw/brain/a2a_telemetry.db
a2a_calls(
  id           INTEGER PRIMARY KEY,
  ts           TEXT,     -- ISO 8601 UTC
  skill_name   TEXT,     -- e.g. "research.recall_search"
  caller_layer TEXT,     -- e.g. "chief"
  latency_ms   INTEGER,
  status       TEXT,     -- 'ok' | 'error' | 'stub'
  request_id   TEXT
)
```

---

## 14. Versioning Policy

### 14.1 Version bump rules

| Change type | Version bump |
|-------------|-------------|
| New from_kind added | Minor (v1.X) |
| New optional envelope field | Minor (v1.X) |
| New required envelope field | Major (vX.0) |
| from_kind renamed or removed | Major (vX.0) |
| Envelope field renamed or removed | Major (vX.0) |
| Auth flow change | Major (vX.0) |
| Tier gate escalation (STANDARD→SENSITIVE) | Minor with notice |
| Tier gate escalation (SENSITIVE→RESTRICTED) | Major with notice |

### 14.2 Guarantees

- No silent breaking changes. All wire changes require a spec version bump.
- Deprecated from_kinds remain in `_VALID_FROM_KINDS` for ≥90 days with deprecation notice in `server_status` output.
- Spec URL always points to the current stable version. Previous versions archived at `/spec/v<N>.M/`.

### 14.3 Current spec URL

`https://mandaire.org/spec/v1.0` (hosted by nginx; Surface owns keeping it in sync with this doc).

---

## 15. Open Items (Forward Design)

| Item | Owner | Status |
|------|-------|--------|
| A2A v0.4: OAuth client credentials replacing shared secret | Surface (AOR-3) | Planned, not scheduled |
| `research.web_browse` + `research.synthesize` stubs | Research | Not implemented |
| `result.confidence_breakdown[]` per-recipient CI | Inference | v1.1 candidate when AOR-3 calibration data available |
| mandaire.org/spec page at `/wire/v1.0` | Surface (AOR-4) | **LIVE** at https://mandaire.org/spec/v1.0 |
| ADR-0021 scope bump (mandaire → mandaire-v2) | Surface | Ready; timing gate — David must be present to re-auth (queued for post-Costa-Rica) |
| ADR-0141 B2 nginx mTLS port 8768 | Infra | Pending Mandaire CA bootstrap (David at terminal on mnd-infra) |
| MCP-as-ingest / self-client pattern | Surface (AOR-5) | SDK endpoint live (/sdk/v1/select, port 8765); mTLS frontend pending |
| Weekly super-tool count check (>8 = alert) | Surface | Cron dispatch to OS pending |
| Judgment HOLD error shape wiring (§12.4 `block_class`, S2/S3 suppression) | Judgment + Surface | v0.1 PRODUCTION gate — spec-settled in v1.0.8; code wiring pending Judgment activation |
| `judgment_applied` envelope field emission | Judgment + Surface | v1.1 candidate — closes AOR-2 GDPR Article 30 audit gap (currently `disclosure_applied: []` indistinguishable from Judgment not running) |
| Judgment disclosure-policy gate (pre-fetch) wiring | Judgment + Surface | v0.1 PRODUCTION gate — requires Judgment policy engine live on MCP path |

---

## Appendix A: Telemetry Schema

Every call recorded to `~/openclaw/brain/mcp_telemetry.db`:

```sql
tool_calls(
  id                           INTEGER PRIMARY KEY,
  ts_utc                       TEXT,
  request_id                   TEXT,
  client_id                    TEXT,
  principal                    TEXT,
  tool_name                    TEXT,
  args_json                    TEXT,      -- truncated at 2000 chars
  args_size_bytes              INTEGER,
  latency_ms                   INTEGER,
  status                       TEXT,      -- 'ok' | 'error' | 'empty'
  error_class                  TEXT,
  error_message                TEXT,      -- truncated at 500 chars
  result_size_bytes            INTEGER,
  result_summary               TEXT,
  disclosure_applied_populated INTEGER,   -- 1=non-stub, 0=stub, NULL=pre-migration
  safety_review_latency_ms     INTEGER,
  cache_hit                    INTEGER,
  source_kind                  TEXT,
  confidence_score             REAL,
  inference_ids_json           TEXT
)
```

Safety review SLO: p99 < 10ms.

---

*Spec produced by Surface (Mandaire) 2026-05-20. Dispatch targets: Product (§7 semantic shapes), Inference (§4.3 confidence contract + §15 confidence_breakdown), Judgment (§11 safety tier gate + §12 pre-flight hook shape).*
