ai-memory 0.7.0

AI-agnostic persistent memory system — MCP server, HTTP API, and CLI for any AI platform
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
// Copyright 2026 AlphaOne LLC
// SPDX-License-Identifier: Apache-2.0

//! MCP namespace standard-policy handlers and governance helpers.

use crate::identity::sentinels;
use crate::mcp::param_names;
use crate::mcp::registry::McpTool;
use crate::models::GovernancePolicy;
use crate::models::field_names;
use crate::{db, validate};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{Value, json};

// --- D1.4 (#985): per-tool McpTool impls for the three namespace-
// standard governance tools ---

/// v0.7.0 #972 D1.4 (#985) — request body for `memory_namespace_set_standard`.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct NamespaceSetStandardRequest {
    /// Namespace.
    pub namespace: String,

    /// Standard memory id.
    pub id: String,

    /// Inherit-from namespace.
    #[serde(default)]
    pub parent: Option<String>,

    /// Task 1.8 policy in metadata.governance.
    #[serde(default)]
    pub governance: Option<serde_json::Value>,
}

/// v0.7.0 #972 D1.4 (#985) — `McpTool` impl for `memory_namespace_set_standard`.
#[allow(dead_code)]
pub struct NamespaceSetStandardTool;

impl McpTool for NamespaceSetStandardTool {
    fn name() -> &'static str {
        crate::mcp::registry::tool_names::MEMORY_NAMESPACE_SET_STANDARD
    }
    fn description() -> &'static str {
        "Set a memory as the standard/policy for a namespace."
    }
    fn docs() -> &'static str {
        "Standard memory auto-prepended to recall + session_start. Rule layering: global '*' + parent chain + namespace. Task 1.8: governance policy merged into metadata. P4/G1: inherit flag."
    }
    fn input_schema() -> Value {
        crate::mcp::registry::input_schema_for::<NamespaceSetStandardRequest>()
    }
    fn family() -> &'static str {
        crate::profile::Family::Governance.name()
    }
}

/// v0.7.0 #972 D1.4 (#985) — request body for `memory_namespace_get_standard`.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct NamespaceGetStandardRequest {
    /// Namespace.
    pub namespace: String,

    /// Task 1.6: return full inheritance chain.
    #[serde(default)]
    pub inherit: Option<bool>,
}

/// v0.7.0 #972 D1.4 (#985) — `McpTool` impl for `memory_namespace_get_standard`.
#[allow(dead_code)]
pub struct NamespaceGetStandardTool;

impl McpTool for NamespaceGetStandardTool {
    fn name() -> &'static str {
        crate::mcp::registry::tool_names::MEMORY_NAMESPACE_GET_STANDARD
    }
    fn description() -> &'static str {
        "Get the standard/policy memory for a namespace."
    }
    fn docs() -> &'static str {
        "Returns the standard. inherit=true (Task 1.6) returns the resolved chain (global '*' -> ancestors -> namespace)."
    }
    fn input_schema() -> Value {
        crate::mcp::registry::input_schema_for::<NamespaceGetStandardRequest>()
    }
    fn family() -> &'static str {
        crate::profile::Family::Governance.name()
    }
}

/// v0.7.0 #972 D1.4 (#985) — request body for `memory_namespace_clear_standard`.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct NamespaceClearStandardRequest {
    /// Namespace.
    pub namespace: String,
}

/// v0.7.0 #972 D1.4 (#985) — `McpTool` impl for `memory_namespace_clear_standard`.
#[allow(dead_code)]
pub struct NamespaceClearStandardTool;

impl McpTool for NamespaceClearStandardTool {
    fn name() -> &'static str {
        crate::mcp::registry::tool_names::MEMORY_NAMESPACE_CLEAR_STANDARD
    }
    fn description() -> &'static str {
        "Clear the standard/policy for a namespace."
    }
    fn docs() -> &'static str {
        "Clear the namespace standard."
    }
    fn input_schema() -> Value {
        crate::mcp::registry::input_schema_for::<NamespaceClearStandardRequest>()
    }
    fn family() -> &'static str {
        crate::profile::Family::Governance.name()
    }
}

pub fn handle_namespace_set_standard(
    conn: &rusqlite::Connection,
    params: &Value,
) -> Result<Value, String> {
    let namespace = params["namespace"]
        .as_str()
        .ok_or(crate::errors::msg::NAMESPACE_REQUIRED)?;
    validate::validate_namespace(namespace).map_err(|e| e.to_string())?;
    let id = params["id"]
        .as_str()
        .ok_or(crate::errors::msg::ID_REQUIRED)?;
    validate::validate_id(id).map_err(|e| e.to_string())?;
    let parent = params["parent"].as_str();
    if let Some(p) = parent {
        validate::validate_namespace(p).map_err(|e| e.to_string())?;
    }

    // #913 (security-medium / SOC2, 2026-05-19) — admin governance
    // audit. Namespace-standard mutations gate every downstream write;
    // the forensic-chain row MUST land before the storage write so the
    // audit trail captures intent even on validate/storage failure
    // downstream. MCP callers resolve via `identity::resolve_agent_id`.
    let caller = crate::identity::resolve_agent_id(params["agent_id"].as_str(), None)
        .unwrap_or_else(|_| sentinels::ANONYMOUS_INVALID.to_string());
    crate::governance::audit::record_decision(
        &caller,
        "allow",
        "namespace_set_standard",
        "",
        serde_json::json!({
            "namespace": namespace,
            (field_names::STANDARD_ID): id,
            "parent": parent,
            "has_governance": params.get(param_names::GOVERNANCE).is_some_and(|v| !v.is_null()),
        }),
    );

    // #929 SECURITY-high (Track A P6, 2026-05-20) — ownership gate on
    // the MCP entry. Mirrors the HTTP handler gate at
    // `handlers/hook_subscribers.rs::set_namespace_standard_inner`.
    // Without this gate any MCP caller could overwrite any namespace's
    // governance policy by passing the existing standard's id —
    // sibling vector to the HTTP path.
    //
    // Gate scope: ONLY applies when the caller has explicitly claimed
    // identity via `params["agent_id"]`. When agent_id is absent the
    // caller is identifying as the daemon process itself (the
    // historical "no claim, no gate" semantic — used by integration
    // tests that invoke this MCP function as a library call without
    // setting up a full identity chain, and by daemon-internal
    // bootstrap paths). The HTTP entry is the load-bearing gate for
    // external callers because it ALWAYS resolves caller via
    // X-Agent-Id (anonymous fallback included) and threads the
    // resolved caller into the MCP params before calling here (see
    // `set_namespace_standard_inner` line 715-720). Direct MCP
    // callers that DO claim identity (via stdio JSON-RPC params)
    // remain gated by this check; daemon-internal callers and tests
    // that don't claim identity get the legacy posture.
    let identity_claimed = params
        .get(param_names::AGENT_ID)
        .and_then(|v| v.as_str())
        .is_some_and(|s| !s.is_empty());
    if identity_claimed && let Ok(Some(existing_mem)) = db::get(conn, id) {
        let recorded_owner = existing_mem
            .metadata
            .get(param_names::AGENT_ID)
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let is_unowned = recorded_owner.is_empty() || recorded_owner == "system";
        if !is_unowned && recorded_owner != caller && caller != sentinels::DAEMON_PRINCIPAL {
            return Err(format!(
                "caller does not own this namespace standard (caller={caller}, owner={recorded_owner})"
            ));
        }
        // Unowned-legacy claim — rewrite metadata.agent_id to caller
        // so subsequent calls are gated. No-op when caller is the
        // anonymous fallback (don't anchor ownership to anonymous).
        if is_unowned && !caller.is_empty() && caller != sentinels::ANONYMOUS_INVALID {
            let mut new_meta = if existing_mem.metadata.is_object() {
                existing_mem.metadata.clone()
            } else {
                serde_json::json!({})
            };
            if let Some(obj) = new_meta.as_object_mut() {
                obj.insert(
                    "agent_id".to_string(),
                    serde_json::Value::String(caller.clone()),
                );
                obj.entry("scope".to_string())
                    .or_insert_with(|| serde_json::Value::String("shared".to_string()));
            }
            // Best-effort: don't fail the set-standard call if the
            // claim rewrite fails — the ownership gate will refire on
            // the next call once metadata.agent_id is non-empty.
            if let Err(e) = db::update(
                conn,
                id,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                Some(&new_meta),
            ) {
                tracing::warn!(
                    "namespace_standard (MCP): ownership-claim metadata update failed: {e}"
                );
            }
        }
    }

    // Task 1.8: optional governance policy merged into the standard memory's
    // metadata.governance. Policy is deserialized + validated before write.
    //
    // v0.7.0 G-PHASE-E-2 (#707) — DO NOT strip "extra" fields from the
    // governance blob. The pre-#707 path round-tripped the incoming
    // governance JSON through the typed `GovernancePolicy` struct, which
    // only carries the whitelist (write/promote/delete/approver/inherit/
    // max_reflection_depth). Any other key — most notably
    // `require_approval_above_depth`, which is a free-function look-up
    // (`storage::resolve_require_approval_above_depth`) outside the
    // typed struct — was silently dropped on re-serialisation. Operators
    // who set `require_approval_above_depth` on a memory and later
    // touched `memory_namespace_set_standard` for any reason lost that
    // gate without any error or log.
    //
    // The fix: take the existing standard memory's `metadata.governance`
    // (if any) as the base, layer the incoming `g` on top key-by-key,
    // validate the merged blob's typed shape, and write the FULL merged
    // JSON back — so unknown-to-the-struct fields on either side survive
    // the round-trip.
    let governance_val = params.get(param_names::GOVERNANCE).filter(|v| !v.is_null());
    if let Some(g) = governance_val {
        // Load the standard memory first so we can read its existing
        // governance blob and merge.
        let mut mem = db::get(conn, id)
            .map_err(|e| e.to_string())?
            .ok_or_else(|| crate::errors::msg::memory_not_found(id))?;
        // Compute the merged governance JSON: existing fields preserved,
        // incoming overrides applied per-key.
        let merged = merge_governance_fields(mem.metadata.get(param_names::GOVERNANCE), g);
        // Validate the typed shape of the result. Deserialising drops
        // unknown fields but the typed sub-set must still parse + pass
        // policy validation — this catches operator typos in known
        // fields without rejecting extras like
        // `require_approval_above_depth`.
        let policy: crate::models::GovernancePolicy = serde_json::from_value(merged.clone())
            .map_err(|e| crate::errors::msg::invalid(param_names::GOVERNANCE, e))?;
        validate::validate_governance_policy(&policy).map_err(|e| e.to_string())?;

        let mut metadata = if mem.metadata.is_object() {
            mem.metadata.clone()
        } else {
            serde_json::json!({})
        };
        if let Some(obj) = metadata.as_object_mut() {
            obj.insert(crate::META_KEY_GOVERNANCE.to_string(), merged);
        }
        let (found, _) = db::update(
            conn,
            &mem.id,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            Some(&metadata),
        )
        .map_err(|e| e.to_string())?;
        if !found {
            return Err(format!("memory not found during governance merge: {id}"));
        }
        mem.metadata = metadata;
    }

    db::set_namespace_standard(conn, namespace, id, parent).map_err(|e| e.to_string())?;
    let mut resp = json!({"set": true, "namespace": namespace, (field_names::STANDARD_ID): id});
    if let Some(p) = parent {
        resp["parent"] = json!(p);
    }
    if let Some(g) = governance_val {
        resp[field_names::GOVERNANCE] = g.clone();
    }
    Ok(resp)
}

pub fn handle_namespace_get_standard(
    conn: &rusqlite::Connection,
    params: &Value,
) -> Result<Value, String> {
    let namespace = params["namespace"]
        .as_str()
        .ok_or(crate::errors::msg::NAMESPACE_REQUIRED)?;
    validate::validate_namespace(namespace).map_err(|e| e.to_string())?;

    // Task 1.6: --inherit returns the full resolved chain, most-general-first.
    let inherit = params["inherit"].as_bool().unwrap_or(false);
    if inherit {
        let chain = super::build_namespace_chain(conn, namespace);
        let mut standards: Vec<Value> = Vec::new();
        for link in &chain {
            if let Some(std) = super::lookup_namespace_standard(conn, link) {
                let gov = extract_governance(&std);
                let entry = json!({
                    "namespace": link,
                    (field_names::STANDARD_ID): std["id"].clone(),
                    "title": std["title"].clone(),
                    "content": std["content"].clone(),
                    "priority": std["priority"].clone(),
                    (field_names::GOVERNANCE): gov,
                });
                standards.push(entry);
            }
        }
        return Ok(json!({
            "namespace": namespace,
            "chain": chain,
            "standards": standards,
            "count": standards.len(),
        }));
    }

    let standard_id = db::get_namespace_standard(conn, namespace).map_err(|e| e.to_string())?;
    match standard_id {
        Some(id) => {
            let mem = db::get(conn, &id).map_err(|e| e.to_string())?;
            match mem {
                Some(m) => {
                    // v0.7.0 #1326 — surface the FULL `metadata.governance`
                    // blob, not just the typed `GovernancePolicy` whitelist.
                    // The typed struct omits caller-supplied free fields
                    // like `require_approval_above_depth` (read by
                    // `storage::resolve_require_approval_above_depth`)
                    // which means set_standard would silently round-trip
                    // them into the DB while get_standard returned a
                    // policy blob that didn't surface them. The fix:
                    // start from the typed policy as the base (so default
                    // fields like `write`/`promote`/`delete`/`approver`/
                    // `inherit` populate), then layer the raw blob keys
                    // back on top — preserving every field the operator
                    // sent on set, including off-struct fields.
                    let gov = merge_governance_for_response(&m.metadata);
                    Ok(json!({
                        "namespace": namespace,
                        (field_names::STANDARD_ID): id,
                        "title": m.title,
                        "content": m.content,
                        "priority": m.priority,
                        (field_names::GOVERNANCE): gov,
                    }))
                }
                None => Ok(
                    json!({"namespace": namespace, (field_names::STANDARD_ID): id, "warning": "standard memory not found — may have been deleted"}),
                ),
            }
        }
        None => Ok(json!({"namespace": namespace, (field_names::STANDARD_ID): null})),
    }
}

/// v0.7.0 #1326 — build the governance blob returned by the
/// `memory_namespace_get_standard` MCP response.
///
/// Composes the typed [`GovernancePolicy`] default-or-resolved
/// serialisation as the base (so well-known fields like `write`,
/// `promote`, `delete`, `approver`, `inherit` always populate even
/// when omitted by the operator), then overlays the raw
/// `metadata.governance` JSON keys on top so caller-supplied
/// off-struct fields (`require_approval_above_depth`,
/// `skill_promotion_min_depth`, future extension keys) survive the
/// round-trip.
///
/// The pre-#1326 path returned only the typed-struct serialisation,
/// dropping every off-struct field on the floor. The L1-8 approval
/// gate (`storage::resolve_require_approval_above_depth`) read the
/// field directly from `metadata.governance`, so the gate kept firing
/// correctly — but operators inspecting the get-standard surface saw
/// an incomplete policy blob and could not confirm their gate was
/// stored.
fn merge_governance_for_response(metadata: &Value) -> Value {
    // Step 1: typed-struct base. Resolves to defaults when no
    // governance metadata is present (matches pre-#1326 behaviour
    // for the well-known-fields surface).
    //
    // #1384 — observability for stored-corruption on the GET path.
    // `from_metadata` returns three shapes: `None` (no governance
    // metadata, fall through to default), `Some(Ok(policy))` (typed
    // parse succeeded), or `Some(Err(parse_err))` (governance JSON
    // present but malformed — e.g. an unknown enum variant landed via
    // direct SQL update or a stale binary). Pre-#1384 the
    // `.map(Result::unwrap_or_default)` collapsed the `Err` arm
    // silently into `GovernancePolicy::default()`, so the get-standard
    // response showed `write: "any"` (the default) even when the
    // stored row carried `write: "approval"` (an invalid variant the
    // operator typo'd). Surface the drift via a tracing WARN so the
    // silent fallback is observable in operator logs; preserve the
    // backward-compat default-on-failure shape because the response
    // body must always include a typed policy block.
    let base = match GovernancePolicy::from_metadata(metadata) {
        None => GovernancePolicy::default(),
        Some(Ok(p)) => p,
        Some(Err(parse_err)) => {
            tracing::warn!(
                target: "ai_memory::governance::policy_read",
                error = %parse_err,
                "stored metadata.governance failed typed deserialise on \
                 the get-standard read path — the response will fall \
                 back to GovernancePolicy::default() (write=any, \
                 delete=owner, approver=human). Likely cause: direct \
                 SQL update, older binary, or a typo'd enum variant \
                 that bypassed the set-standard typed gate. Operator \
                 should re-run `memory_namespace_set_standard` to \
                 restore the typed shape."
            );
            GovernancePolicy::default()
        }
    };
    let mut merged = serde_json::to_value(&base)
        .ok()
        .and_then(|v| v.as_object().cloned())
        .unwrap_or_default();

    // Step 2: overlay raw `metadata.governance` keys so off-struct
    // fields survive. Iterate over the raw JSON object and insert
    // every key that wasn't already produced by the typed serialisation
    // (keys present in both are byte-equal because the typed struct
    // was deserialised from the same JSON).
    if let Some(raw) = metadata
        .get(param_names::GOVERNANCE)
        .and_then(Value::as_object)
    {
        for (k, v) in raw {
            merged.entry(k.clone()).or_insert_with(|| v.clone());
        }
    }
    Value::Object(merged)
}

/// Task 1.8 — extract metadata.governance from a serialized memory value,
/// resolving to the default policy when missing or invalid. Used by the
/// `--inherit` get-standard path and tool responses.
///
/// v0.7.0 #1326 — surfaces the FULL `metadata.governance` JSON blob
/// (typed default fields + caller-supplied off-struct fields like
/// `require_approval_above_depth`). The pre-#1326 path returned only
/// the typed [`GovernancePolicy`] whitelist, dropping every off-struct
/// field. See [`merge_governance_for_response`] for the merge contract.
pub(super) fn extract_governance(mem_val: &Value) -> Value {
    let Some(meta) = mem_val.get(param_names::METADATA) else {
        return serde_json::to_value(GovernancePolicy::default()).unwrap_or(Value::Null);
    };
    merge_governance_for_response(meta)
}

pub(crate) fn handle_namespace_clear_standard(
    conn: &rusqlite::Connection,
    params: &Value,
) -> Result<Value, String> {
    let namespace = params["namespace"]
        .as_str()
        .ok_or(crate::errors::msg::NAMESPACE_REQUIRED)?;
    validate::validate_namespace(namespace).map_err(|e| e.to_string())?;

    // #913 (security-medium / SOC2, 2026-05-19) — admin governance audit.
    let caller = crate::identity::resolve_agent_id(params["agent_id"].as_str(), None)
        .unwrap_or_else(|_| sentinels::ANONYMOUS_INVALID.to_string());
    crate::governance::audit::record_decision(
        &caller,
        "allow",
        "namespace_clear_standard",
        "",
        serde_json::json!({ "namespace": namespace }),
    );

    let cleared = db::clear_namespace_standard(conn, namespace).map_err(|e| e.to_string())?;
    Ok(json!({"cleared": cleared, "namespace": namespace}))
}

/// Auto-register namespace parent chain from the filesystem path.
/// Walks from cwd up to home dir, checks if each directory name has a namespace
/// standard set, and registers the parent chain.
///
/// Example: cwd = /home/user/monorepo/frontend
///   → checks "frontend" (cwd), "monorepo" (parent), stops at home dir
///   → if "monorepo" has a standard, sets `parent_namespace` of "frontend" to "monorepo"
#[allow(dead_code)]
pub(super) fn auto_register_path_hierarchy(conn: &rusqlite::Connection, namespace: &str) {
    // Only run if this namespace doesn't already have an explicit parent
    if db::get_namespace_parent(conn, namespace).is_some() {
        return;
    }
    let Ok(cwd) = std::env::current_dir() else {
        return;
    };
    let home = dirs::home_dir().unwrap_or_default();
    // Walk up from parent of cwd (cwd itself IS the namespace)
    let mut current = cwd.parent().map(std::path::Path::to_path_buf);
    while let Some(dir) = current {
        // Stop at or above home directory
        if dir == home || !dir.starts_with(&home) {
            break;
        }
        if let Some(dir_name) = dir.file_name().and_then(|n| n.to_str()) {
            // Check if this directory name has a namespace standard
            if db::get_namespace_standard(conn, dir_name)
                .ok()
                .flatten()
                .is_some()
            {
                // Found a parent with a standard — register it
                let now = chrono::Utc::now().to_rfc3339();
                let _ = conn.execute(
                    "UPDATE namespace_meta SET parent_namespace = ?1, updated_at = ?2 WHERE namespace = ?3 AND parent_namespace IS NULL",
                    rusqlite::params![dir_name, now, namespace],
                );
                tracing::info!(
                    "auto-registered parent namespace: {} -> {}",
                    namespace,
                    dir_name
                );
                break;
            }
        }
        current = dir.parent().map(std::path::Path::to_path_buf);
    }
}

/// v0.7.0 G-PHASE-E-2 (#707) — merge an incoming governance JSON blob
/// onto an existing one, key-by-key. The incoming blob's keys override
/// the existing ones; keys present only on the existing blob (e.g. an
/// operator-set `require_approval_above_depth`) survive untouched.
///
/// Both sides are treated as JSON objects — non-object inputs (or
/// missing `existing`) collapse to "use the incoming side wholesale".
/// Returns a fresh `serde_json::Value::Object` so the caller can
/// re-serialise without aliasing the input slots.
fn merge_governance_fields(
    existing: Option<&serde_json::Value>,
    incoming: &serde_json::Value,
) -> serde_json::Value {
    let mut merged = serde_json::Map::new();
    if let Some(existing_obj) = existing.and_then(serde_json::Value::as_object) {
        for (k, v) in existing_obj {
            merged.insert(k.clone(), v.clone());
        }
    }
    if let Some(incoming_obj) = incoming.as_object() {
        for (k, v) in incoming_obj {
            merged.insert(k.clone(), v.clone());
        }
    } else {
        // Incoming is not an object — fall back to the incoming value
        // wholesale so callers can still pass primitives through (the
        // typed deserialise on the caller side will reject anything
        // structurally wrong).
        return incoming.clone();
    }
    serde_json::Value::Object(merged)
}

// ---------------------------------------------------------------------------
// Archive tool handlers
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    //! Coverage C-2 — focused tests for the namespace-standard MCP handlers
    //! and the private `extract_governance` helper.

    use super::*;
    use crate::models::{Memory, Tier};
    use crate::storage as db;
    use serde_json::json;

    fn fresh_conn() -> rusqlite::Connection {
        db::open(std::path::Path::new(":memory:")).expect("open in-memory db")
    }

    fn insert_one(conn: &rusqlite::Connection, ns: &str, title: &str) -> String {
        let now = chrono::Utc::now().to_rfc3339();
        let mem = Memory {
            id: uuid::Uuid::new_v4().to_string(),
            tier: Tier::Long,
            namespace: ns.to_string(),
            title: title.to_string(),
            content: format!("body for {title}"),
            tags: vec![],
            priority: 5,
            confidence: 1.0,
            source: "test".to_string(),
            access_count: 0,
            created_at: now.clone(),
            updated_at: now,
            last_accessed_at: None,
            expires_at: None,
            metadata: json!({}),
            reflection_depth: 0,
            memory_kind: crate::models::MemoryKind::Observation,
            entity_id: None,
            persona_version: None,
            citations: Vec::new(),
            source_uri: None,
            source_span: None,
            confidence_source: crate::models::ConfidenceSource::CallerProvided,
            confidence_signals: None,
            confidence_decayed_at: None,
            version: 1,
        };
        db::insert(conn, &mem).expect("insert")
    }

    // set_standard: happy path without governance.
    #[test]
    fn set_standard_happy_path() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-a", "standard");
        let resp = handle_namespace_set_standard(&conn, &json!({"namespace": "ns-a", "id": id}))
            .expect("ok");
        assert_eq!(resp["set"], true);
    }

    // set_standard: with parent.
    #[test]
    fn set_standard_with_parent_echoed() {
        let conn = fresh_conn();
        // Seed the parent standard first.
        let parent_id = insert_one(&conn, "parent", "p-standard");
        db::set_namespace_standard(&conn, "parent", &parent_id, None).unwrap();
        let id = insert_one(&conn, "child", "c-standard");
        let resp = handle_namespace_set_standard(
            &conn,
            &json!({"namespace": "child", "id": id, "parent": "parent"}),
        )
        .expect("ok");
        assert_eq!(resp["parent"].as_str(), Some("parent"));
    }

    // set_standard: missing namespace → typed error.
    #[test]
    fn set_standard_missing_namespace_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_set_standard(&conn, &json!({"id": "x"})).unwrap_err();
        assert!(err.contains("namespace"), "got: {err}");
    }

    // set_standard: missing id → typed error.
    #[test]
    fn set_standard_missing_id_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_set_standard(&conn, &json!({"namespace": "x"})).unwrap_err();
        assert!(err.contains("id"), "got: {err}");
    }

    // set_standard: invalid namespace rejected (validate_namespace).
    #[test]
    fn set_standard_invalid_namespace_rejected() {
        let conn = fresh_conn();
        let err =
            handle_namespace_set_standard(&conn, &json!({"namespace": "has spaces", "id": "x"}))
                .unwrap_err();
        assert!(!err.is_empty());
    }

    // set_standard: invalid parent namespace rejected.
    #[test]
    fn set_standard_invalid_parent_rejected() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-parent-bad", "p");
        let err = handle_namespace_set_standard(
            &conn,
            &json!({"namespace": "ns-parent-bad", "id": id, "parent": "has spaces"}),
        )
        .unwrap_err();
        assert!(!err.is_empty());
    }

    // set_standard: with governance — merged into metadata + echoed.
    #[test]
    fn set_standard_with_governance_merged() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-gov", "p");
        // GovernancePolicy schema requires `write`; other fields default.
        let governance = json!({"write": "any"});
        let resp = handle_namespace_set_standard(
            &conn,
            &json!({"namespace": "ns-gov", "id": id, "governance": governance.clone()}),
        )
        .expect("ok");
        assert_eq!(resp["governance"], governance);
        // The merged metadata must round-trip through db::get.
        let mem = db::get(&conn, &id).unwrap().unwrap();
        assert!(mem.metadata.get("governance").is_some());
    }

    // set_standard: invalid governance (deserialization fails).
    #[test]
    fn set_standard_with_invalid_governance_rejected() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-bad-gov", "p");
        let err = handle_namespace_set_standard(
            &conn,
            &json!({
                "namespace": "ns-bad-gov",
                "id": id,
                "governance": "this is not an object",
            }),
        )
        .unwrap_err();
        assert!(err.contains("invalid governance"), "got: {err}");
    }

    // set_standard: governance specified but memory id does not exist.
    #[test]
    fn set_standard_with_governance_unknown_id_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_set_standard(
            &conn,
            &json!({
                "namespace": "ns-missing-id",
                "id": "11111111-2222-3333-4444-555555555555",
                "governance": {"write": "any"},
            }),
        )
        .unwrap_err();
        assert!(err.contains("memory not found"), "got: {err}");
    }

    // get_standard: missing namespace → typed error.
    #[test]
    fn get_standard_missing_namespace_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_get_standard(&conn, &json!({})).unwrap_err();
        assert!(err.contains("namespace"), "got: {err}");
    }

    // get_standard: unknown namespace → standard_id null.
    #[test]
    fn get_standard_unknown_namespace_returns_null() {
        let conn = fresh_conn();
        let resp =
            handle_namespace_get_standard(&conn, &json!({"namespace": "no-such"})).expect("ok");
        assert!(resp["standard_id"].is_null());
    }

    // get_standard: happy path.
    #[test]
    fn get_standard_happy_path() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-get", "got");
        db::set_namespace_standard(&conn, "ns-get", &id, None).unwrap();
        let resp =
            handle_namespace_get_standard(&conn, &json!({"namespace": "ns-get"})).expect("ok");
        assert_eq!(resp["standard_id"].as_str(), Some(id.as_str()));
        assert_eq!(resp["title"].as_str(), Some("got"));
        // governance defaults filled in.
        assert!(resp["governance"].is_object());
    }

    // get_standard: standard_id present but memory deleted — warning surfaced.
    #[test]
    fn get_standard_dangling_id_surfaces_warning() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-dangling", "g");
        db::set_namespace_standard(&conn, "ns-dangling", &id, None).unwrap();
        // Now physically delete the memory row.
        conn.execute("DELETE FROM memories WHERE id = ?1", rusqlite::params![&id])
            .unwrap();
        let resp =
            handle_namespace_get_standard(&conn, &json!({"namespace": "ns-dangling"})).expect("ok");
        assert!(resp["warning"].is_string());
    }

    // get_standard: --inherit returns chain + standards array.
    #[test]
    fn get_standard_inherit_returns_chain() {
        let conn = fresh_conn();
        let global_id = insert_one(&conn, "*", "global");
        db::set_namespace_standard(&conn, "*", &global_id, None).unwrap();
        let leaf_id = insert_one(&conn, "leaf-ns", "leaf");
        db::set_namespace_standard(&conn, "leaf-ns", &leaf_id, None).unwrap();
        let resp =
            handle_namespace_get_standard(&conn, &json!({"namespace": "leaf-ns", "inherit": true}))
                .expect("ok");
        assert!(resp["chain"].is_array());
        assert!(resp["count"].as_u64().unwrap() >= 1);
    }

    // clear_standard: happy.
    #[test]
    fn clear_standard_happy() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-clear", "c");
        db::set_namespace_standard(&conn, "ns-clear", &id, None).unwrap();
        let resp =
            handle_namespace_clear_standard(&conn, &json!({"namespace": "ns-clear"})).expect("ok");
        assert_eq!(resp["cleared"], true);
    }

    // clear_standard: missing namespace → error.
    #[test]
    fn clear_standard_missing_namespace_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_clear_standard(&conn, &json!({})).unwrap_err();
        assert!(err.contains("namespace"), "got: {err}");
    }

    // clear_standard: invalid namespace rejected.
    #[test]
    fn clear_standard_invalid_namespace_rejected() {
        let conn = fresh_conn();
        let err = handle_namespace_clear_standard(&conn, &json!({"namespace": "has spaces"}))
            .unwrap_err();
        assert!(!err.is_empty());
    }

    // extract_governance: empty metadata returns default policy.
    #[test]
    fn extract_governance_default_when_missing_metadata() {
        let val = json!({"id": "x"});
        let gov = extract_governance(&val);
        assert!(gov.is_object());
    }

    // extract_governance: full metadata with valid governance.
    #[test]
    fn extract_governance_round_trips_valid_policy() {
        let val = json!({
            "metadata": {
                "governance": {"min_priority": 0}
            }
        });
        let gov = extract_governance(&val);
        assert!(
            gov.is_object(),
            "expected default-or-resolved policy object"
        );
    }

    // auto_register_path_hierarchy: no-op when parent already set.
    #[test]
    fn auto_register_noop_when_parent_already_set() {
        let conn = fresh_conn();
        // Seed parent_namespace via direct SQL to bypass auto-detect.
        conn.execute(
            "INSERT INTO namespace_meta (namespace, standard_id, updated_at, parent_namespace)
             VALUES ('child-ns', NULL, '2026-01-01T00:00:00Z', 'set-parent')",
            [],
        )
        .unwrap();
        // Should be a no-op: assert the function does not panic and the
        // parent_namespace value survives.
        auto_register_path_hierarchy(&conn, "child-ns");
        let p: Option<String> = conn
            .query_row(
                "SELECT parent_namespace FROM namespace_meta WHERE namespace = 'child-ns'",
                [],
                |r| r.get(0),
            )
            .unwrap();
        assert_eq!(p.as_deref(), Some("set-parent"));
    }

    // auto_register_path_hierarchy: no namespace_meta row → no-op (no panic).
    #[test]
    fn auto_register_handles_missing_row_gracefully() {
        let conn = fresh_conn();
        // Should not panic when nothing is set up.
        auto_register_path_hierarchy(&conn, "non-existent-ns");
    }
}

#[cfg(test)]
mod d1_4_985_tests {
    //! D1.4 (#985) — schema-parity for `memory_namespace_set_standard`,
    //! `memory_namespace_get_standard`, `memory_namespace_clear_standard`.
    use super::*;
    use crate::mcp::d1_4_985_helpers::{
        assert_descriptions_match, assert_property_set_parity, derived_props_for,
    };

    #[test]
    fn memory_namespace_set_standard_parity_985() {
        let derived = derived_props_for::<NamespaceSetStandardRequest>();
        assert_property_set_parity("memory_namespace_set_standard", &derived);
        assert_descriptions_match("memory_namespace_set_standard", &derived);
    }

    #[test]
    fn memory_namespace_set_standard_tool_metadata_985() {
        assert_eq!(
            NamespaceSetStandardTool::name(),
            "memory_namespace_set_standard"
        );
        assert_eq!(NamespaceSetStandardTool::family(), "governance");
    }

    #[test]
    fn memory_namespace_get_standard_parity_985() {
        let derived = derived_props_for::<NamespaceGetStandardRequest>();
        assert_property_set_parity("memory_namespace_get_standard", &derived);
        assert_descriptions_match("memory_namespace_get_standard", &derived);
    }

    #[test]
    fn memory_namespace_get_standard_tool_metadata_985() {
        assert_eq!(
            NamespaceGetStandardTool::name(),
            "memory_namespace_get_standard"
        );
        assert_eq!(NamespaceGetStandardTool::family(), "governance");
    }

    #[test]
    fn memory_namespace_clear_standard_parity_985() {
        let derived = derived_props_for::<NamespaceClearStandardRequest>();
        assert_property_set_parity("memory_namespace_clear_standard", &derived);
        assert_descriptions_match("memory_namespace_clear_standard", &derived);
    }

    #[test]
    fn memory_namespace_clear_standard_tool_metadata_985() {
        assert_eq!(
            NamespaceClearStandardTool::name(),
            "memory_namespace_clear_standard"
        );
        assert_eq!(NamespaceClearStandardTool::family(), "governance");
    }
}