Owner: Surface layer
Canonical source: ~/openclaw/mandaire_mcp/server.py, auth_provider.py, core/mandaire/a2a/
Protocol version: 1.0.14
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-30 (v1.0.17: §4.1 + §4.2: context_card family envelope fields documented (card_id, mode, persisted, outcome, allowed_summary, withheld_topics, owner_approval_required, safe_to_send, wire_version); enforce mode behavior noted (shipped #151510). §15 context_card wire gap closed.)
Review dispatch targets:
| Area | Change |
|---|---|
| §4.1 JSON Schema | Added 9 conditional top-level fields for the context_card from_kind family: card_id, mode, persisted, outcome, allowed_summary, withheld_topics, owner_approval_required, safe_to_send, wire_version. These are merged via extras at the _mandaire_envelope boundary (server.py:49003). Only present when from_kind ∈ {context_card, context_card_preview, context_card_create}. |
| §4.2 Field semantics | Corresponding rows added for all 9 context_card fields. Enforce-mode semantics: var/feature_flags/context_card_contract.json {"mode":"enforce"} live on .16 (2026-06-30 #151510). Leak-class violation → safe_version nulled, outcome=DENY, safe_to_send=False. Owner-keyed cards unaffected. |
| §15 Open Items | Context card wire fields gap closed; moved to Closed table. |
| Area | Change |
|---|---|
| §15 Open Items | Super-tool drift cron: LIVE 2026-06-28 — mcp-tool-count-check.timer (weekly Mon 05:00 Pacific, Persistent). Baseline-set drift check via live list_tools() — alerts Surface P3 by tool name on any unreviewed addition; removal logged only. Baseline: 9 tools (mandaire, mandaire_read, mandaire_write, mandaire_channel, mandaire_confirm + server_status, health, get_protocol_spec, tool_telemetry). When Surface adds a reviewed super-tool, update BASELINE_TOOLS in scripts/mcp_tool_count_check.py. |
| §15 Open Items | Chokepoint cron placement (ADR-0291): LIVE 2026-06-28. safety_chokepoint_coverage_check.py runs every 3h on mnd-app (.16) via crontab (not systemd timer — consistent with .16 infra). Reads served telemetry via MANDAIRE_BRAIN_DIR=/data/brain (required; bare canonical() under env-stripped cron resolves to OS-disk copy, not LUKS telemetry). Log: /home/david/logs/safety_chokepoint_coverage_check.log. ubu03 instance removed. Last verified: branch c_verified, total=76 calls, zero false dispatch. mcp_telemetry is per-VM-local by design (ADR-0291). |
| Area | Change |
|---|---|
| §13.3 A2A registered skills | Enrichment skills live (co-hosted 2026-06-13): Added enrichment.coverage_check, enrichment.enrich_entity, enrichment.freshness_sweep (LIVE) and enrichment.cohort_enrich, enrichment.linkedin_enrich (Stub). Handlers wired in core/mandaire/a2a/skills.py; registered in SKILLS dict. Skill count: 7 LIVE + 4 Stub = 11 total (was 5 live + 2 stub = 7 total). |
| §13.5 Per-skill wire reference (new) | Documents per-skill input (params.message.parts[].text + params.metadata fields) and response shape for each live skill. Sourced from core/mandaire/a2a/skills.py to prevent spec drift. |
| Area | Change |
|---|---|
| §10.9 Requester key (new) | SEC-070 D2: GET /.well-known/mandaire-requester-key.json (2026-06-13): Publicly-discoverable Ed25519 JWK for cross-tenant request signing (ADR-0224 prerequisite). No auth required. Returns {"kty":"OKP","crv":"Ed25519","use":"sig","kid":"...","x":"..."} or 404 when no key exists. Live at mcp.mandaire.com and mcp.mandaire.app. Cross-user verbs themselves (ADR-0224) remain gated on Architecture layer-3 authorization. |
| Area | Change |
|---|---|
| §10.4 Scope | ADR-0199 incremental scope consent wire documented: token missing the required scope is rejected with an explicit re-consent error (no silent downgrade). |
| §10.7 SDK endpoint | ADR-0233 POST /sdk/v1/signal (control-plane, from_kind-discriminated, mTLS+CN-gated, allowlist mnd-app-*, kinds=diagnostic) and POST /sdk/v1/write added alongside /sdk/v1/select. |
| §13 A2A | Skill registry synced to live wire: 5 live skills (added research.inferred_asset_query + co-hosted inference.claim_lookup). §13.4 + §15 updated: v0.4 OAuth is condition-gated on the multi-user / external-peer milestone (not date-scheduled), confirmed by Research 2026-06-13. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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). |
| 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. |
| 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 |
| 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) |
| 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 |
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=Falsemandaire()
{
"$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": []
}
_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.
| 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 |
| 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 |
| 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= |
compare_options | SENSITIVE | Tradeoff matrix for in-flight decision |
recent_decisions | SENSITIVE | Alias for topic+fields=decisions |
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
| 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 |
| 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 |
| 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 |
| from_kind | Tier | Verbs | Description |
|---|---|---|---|
ai_observation | STANDARD | INSERT, SELECT | AI-synthesized inference writeback to inference.db |
| 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 |
| 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 |
| 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. |
| 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. |
| 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. |
Schemas: domains/dev/schemas/dev_ (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. |
| from_kind | Tier | Description | |
|---|---|---|---|
lineage | STANDARD | Upstream/downstream traversal from `from_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=. 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. |
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). |
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). |
All responses use the same envelope. Error responses include the same metadata keys.
{
"$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"}
}
},
"card_id": {"type": ["string", "null"], "description": "context_card family only. Stable card ID; non-null only when from_kind=context_card_create AND persisted."},
"mode": {"type": ["string", "null"], "description": "context_card family only. dry_run | preview | persist."},
"persisted": {"type": ["boolean", "null"], "description": "context_card family only. True when card written to context_cards (context_card_create)."},
"outcome": {"type": ["string", "null"], "description": "context_card family only. PERMIT | DENY | WITHHOLD policy verdict."},
"allowed_summary": {"type": ["string", "null"], "description": "context_card family only. Prose of what the card would share; null on DENY."},
"withheld_topics": {"type": "array", "items": {"type": "string"}, "description": "context_card family only. Topics withheld per per-(R,T,C,S) policy; [] when all allowed."},
"owner_approval_required": {"type": ["boolean", "null"], "description": "context_card family only. True if safe_to_send=False OR high-regret OR outcome != PERMIT."},
"safe_to_send": {"type": ["boolean", "null"], "description": "context_card family only. Fail-closed in enforce mode — any leak-class violation sets safe_version=null, outcome=DENY."},
"wire_version": {"type": "string", "description": "context_card family only. Wire protocol version, e.g. 'context_card/v0.2'."}
}
}
| 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 |
card_id | conditional | context_card family only. Stable card ID; non-null only when from_kind=context_card_create AND persisted |
mode | conditional | context_card family only. dry_run | preview | persist |
persisted | conditional | context_card family only. True when card written to context_cards |
outcome | conditional | context_card family only. PERMIT | DENY | WITHHOLD policy verdict |
allowed_summary | conditional | context_card family only. Prose of what the card would share; null on DENY |
withheld_topics | conditional | context_card family only. Topics withheld per per-(R,T,C,S) policy; [] when all allowed |
owner_approval_required | conditional | context_card family only. True if safe_to_send=False OR high-regret OR outcome≠PERMIT |
safe_to_send | conditional | context_card family only. Fail-closed in enforce mode (live 2026-06-30 #151510) — leak-class violation → safe_version nulled, outcome=DENY |
wire_version | conditional | context_card family only. Wire protocol version, e.g. "context_card/v0.2" |
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).
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.
Every envelope carries a provenance triple via attach_provenance() from core.mandaire.transport.provenance. Fail-open: attachment failure never blocks response.
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.
{
"$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):
{
"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.dedup=trueclassifier_version = mcp_ai_observation_v0.1 stamped automaticallySELECT ai_observation:
mandaire(verb="SELECT", from_kind="ai_observation", from_match="<query>", filters={"limit": 20})
Returns array of inference rows matching the query via recall_search.
mandaire(verb="SELECT", from_kind="decision", from_id="<audit_ref_uuid>")
Output:
{
"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).
Required: from_match (or from_id / ask) containing the topic string.
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):
{
"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:
[
{"verb": "SELECT", "from_kind": "situation_brief", "from_match": "<topic>", "filters": {"detail": "full"}},
{"verb": "SELECT", "from_kind": "context", "from_match": "<topic>"}
]
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=: cap recent_decisions (default 8)filters.event_days=: upcoming_events lookahead in days (default 7)filters.threads_limit=: live_threads count (default 15)filters.mailing_lists_limit=: live_mailing_lists count (default 10)Output (result field):
{
"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).
Reached via from_kind=person with from_match=.
Input:
mandaire(verb="SELECT", from_kind="person", from_match="Bala", purpose="researching_history")
Candidate list (within result):
{
"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.
| Header | Required | Description |
|---|---|---|
Authorization | yes | Bearer at_<40-byte-urlsafe-token> |
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
| Token | Prefix | TTL |
|---|---|---|
| Access token | at_ | 24h |
| Refresh token | rt_ | 30 days |
| Auth code | ac_ | 10 minutes |
Valid scopes: mandaire (default, shim period), mandaire-v2 (post-shim). Scope bump triggers re-consent cycle for all existing clients.
Incremental scope consent (ADR-0199): a client presenting a token that is missing the current required scope is rejected with an explicit re-consent error rather than a silent downgrade. The client must re-run the consent flow to acquire the elevated scope; partial-scope tokens never silently grant reduced access.
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).
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.
Privileged full-fidelity read endpoint for trusted internal agents (Chief, Briefing, david-cli):
GET /sdk/v1/select (port 8765, tailnet-only; NOT exposed externally)
POST /sdk/v1/write (symmetric with select; same mTLS+CN gate)
POST /sdk/v1/signal (control-plane signal, ADR-0233 D1)
X-Mandaire-Agent-CN: chief ← set by nginx mTLS frontend on port 8768
/sdk/v1/signal (ADR-0233): cross-VM control-plane signal channel, from_kind-discriminated. Same mTLS + CN gate; CN allowlist defaults to mnd-app-david (env MANDAIRE_SDK_SIGNAL_CN_ALLOWLIST, or pattern ^mnd-app-[a-z0-9-]+$). Allowed kinds default to diagnostic (env MANDAIRE_SDK_SIGNAL_KINDS). Not a data-read path; carries diagnostic / control signals only.
{chief, briefing, david-cli} → 403.mandaire_read response (disclosure-axis fields stripped before response).X-Mandaire-Agent-CN set (tailnet-only; no mTLS enforcement until CA is up).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 extend without new routes.
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
| 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) |
| 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: | 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.
| 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 |
Applied at mandaire() entry, after verb validation, before any handler runs.
| 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.
person, household, relationship at depth="deep" or depth="exhaustive" → treated as SENSITIVE regardless of catalog tier.
Presence-only check. Not validated against an enum. Suggested values: preparing_for_meeting, drafting_reply, researching_history, evaluating_relationship, disambiguating, ai_writeback_from_conversation.
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):
{
"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:
{
"ok": false,
"error": "response blocked by disclosure policy",
"disclosure_applied": []
}
Endpoint: https://research.mandaire.com/a2a
Version: 0.1.0
Wire owner: Surface (absorbed from Research at activation)
| Method | Path | Auth |
|---|---|---|
| GET | /.well-known/agent-card.json | None |
| POST | /a2a | Bearer shared secret |
| GET | /healthz | None |
{
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"skill": "research.<skill_id>",
"message": {"parts": [{"kind": "text", "text": "..."}]},
"metadata": {"limit": 8, "mode": "both"}
},
"id": "<caller-generated id>"
}
Required headers: Authorization: Bearer
Optional: X-A2A-Trace-Id, X-A2A-Parent-Trace, X-A2A-Hop-Count, X-Mandaire-Caller
| Skill ID | Status | Cost class | Notes |
|---|---|---|---|
research.recall_search | Live | free | FTS + semantic over recall.db |
research.entity_lookup | Live | free | Name/email/phone lookup in people.db |
research.inferred_asset_query | Live | free | web_enrichment.db + observations.db read |
research.web_enrich | Live | claude_sonnet (~90-180s) | WebSearch + Claude enrichment per entity |
inference.claim_lookup (co-hosted) | Live | free | Active claims over inference.db; supports as_of replay |
enrichment.coverage_check (co-hosted) | Live | free | web_enrichment.db coverage stats (no text needed) |
enrichment.enrich_entity (co-hosted) | Live | claude_sonnet (~90-180s) | Full web enrichment; metadata.force=true to re-enrich |
enrichment.freshness_sweep (co-hosted) | Live | claude_sonnet × N | Batch stale-entity re-enrichment; cost gate at N>20 |
research.web_browse | Stub | N/A | |
research.synthesize | Stub | N/A | |
enrichment.cohort_enrich | Stub | N/A | Cost-gate design pending |
enrichment.linkedin_enrich | Stub | N/A | Batch-only API design pending |
v0.1 (active): Shared bearer secret at ~/openclaw/Brain/credentials/a2a_research_v01.secret. Sufficient because all live peers are internal fleet agents on the same trust boundary.
v0.4 (condition-gated, not date-scheduled): OAuth 2.1 client_credentials via Authelia (https://auth.mandaire.com/api/oidc/token). Scopes: research:read, research:write (Research-owned); inference:read (Inference-owned). The flip condition is: admit a genuinely external / untrusted peer, OR the disclosure engine reaches ENFORCEMENT for cross-agent callers. When either fires, OAuth enrollment precedes bearer decommission; no silent rotation. Confirmed by Research 2026-06-13.
All skills use the §13.2 request envelope. This section documents params.message.parts[].text (the query input) and params.metadata fields per live skill, plus the response shape. Stub skills return {"status": "not_yet_implemented", "skill": "<id>"}.
research.recall_search
limit int default=8; mode "both"|"fts"|"semantic" default="both"{"status", "query", "mode", "limit", "elapsed_s", "output": "<recall output, max 20 000 chars>", "cost_class": "free"}research.entity_lookup
{"status", "query", "match_count", "results": [{"id", "name", "type", "company", "title", "current_role", "current_company", "web_bio" (≤300 chars), "updated_at"}], "elapsed_s", "cost_class": "free"}. Returns up to 10 matches ranked by source_count.research.web_enrich
timeout_s int default=180{"status", "entity", "elapsed_s", "output": "<enrichment stdout, max 20 000 chars>", "cost_class": "claude_sonnet"}. Writes to web_enrichment.db as a side-effect.research.inferred_asset_query
asset "web_enrichment"|"observations"|"all" default="all"; limit 1-50 default=10{"status", "query", "asset", "web_enrichment": [{entity_name, summary (≤400), current_role, company, confidence, queried_at}], "observations": [{source, severity, topic_key, summary (≤400), created_at}], "elapsed_s", "cost_class": "free"}. Keys present only for requested asset(s).inference.claim_lookup
limit 1-100 default=20; as_of ISO-ts optional (valid-time replay)lookup_claims() output — active (non-superseded, non-evicted) claims for the subject. Shape: {"status", "subject", "claims": [{"id", "subject_key", "property", "value", "confidence", "source", "asserted_at", "valid_from", "evidence_refs"}], "count", "elapsed_s"}.enrichment.coverage_check
{"status": "ok", "total", "pct_with_summary", "pct_stale_90d", "pct_no_citations", "pct_with_uuid", "pct_contaminated", "elapsed_s", "cost_class": "free"}. All pct_* fields are 0–100 floats rounded to 1 decimal.enrichment.enrich_entity
force bool default=false (re-enrich even if recently enriched); timeout_s int default=180{"status", "entity", "force", "elapsed_s", "output" (max 20 000 chars), "cost_class": "claude_sonnet"}. Writes to web_enrichment.db.enrichment.freshness_sweep
limit 1-50 default=10 (max entities to re-enrich); refresh_days int default=90 (staleness threshold); confirm_cost "yes" required when limit > 20{"status", "limit", "refresh_days", "elapsed_s", "output" (max 20 000 chars), "cost_class": "claude_sonnet × ≤N"}. When limit>20 and confirm_cost != "yes": {"status": "cost_gate", "error": "...", "cost_class": "..."}.| 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 |
_VALID_FROM_KINDS for ≥90 days with deprecation notice in server_status output./spec/v.M/ .https://mandaire.org/spec/v1.0 (hosted by nginx; Surface owns keeping it in sync with this doc).
| Item | Owner | Status |
|---|---|---|
| A2A v0.4: OAuth client credentials replacing shared secret | Surface (AOR-3) | Condition-gated on multi-user / external-peer milestone (not date-scheduled); confirmed Research 2026-06-13 |
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 drift check | Surface + OS | LIVE 2026-06-28 — mcp-tool-count-check.timer weekly Mon 05:00 Pacific. Baseline-set drift (not count threshold): alerts Surface P3 by name on unreviewed addition; removal logged only. Update BASELINE_TOOLS in scripts/mcp_tool_count_check.py when adding a reviewed super-tool. |
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 |
Every call recorded to ~/openclaw/brain/mcp_telemetry.db:
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).