Skip to content

Commit 9b9c602

Browse files
FoundupFoundups Agent
andauthored
fix(memex): harden multi-foundup current-state scoping (#1107)
Co-authored-by: Foundups Agent <dev@foundups.com>
1 parent a820918 commit 9b9c602

8 files changed

Lines changed: 335 additions & 20 deletions

holo_index/ModLog.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# HoloIndex Package ModLog
22

3+
## [2026-07-16] FOUNDUP_MEMEX_MULTI_FOUNDUP_SCOPE_HARDENING_PHASE1
4+
5+
**Agent**: 0102 (Codex) | Commander: 012 | Gate: implementation slice
6+
**WSP**: 00, 15, 22, 50, 60, 97
7+
8+
- UPDATE `holo_index/memex_projection_integrity.py`: serialized Memex
9+
projection records must now carry consistent operational snapshot
10+
`metadata.snapshot_id` and `metadata.snapshot_content_digest` values across
11+
every record.
12+
- ADD optional expected binding checks for operational snapshot id and content
13+
digest so a later assignment-binding slice can pin the Memex projection to
14+
the exact RedDog snapshot.
15+
- TEST `tests/test_holoindex_memex_projection_integrity.py`: mixed, missing,
16+
and expected-mismatched operational snapshot metadata all fail closed.
17+
- Boundary: this remains integrity, not authentication. Runtime policy
18+
issuance, assignment-bound authorization, content-bearing evidence, and typed
19+
citation policy remain downstream.
20+
321
## [2026-07-16] HOLOINDEX_MEMEX_ACCESS_POLICY_RECEIPT_PHASE1
422

523
**Agent**: 0102 (Codex) | Commander: 012 | Gate: implementation slice

holo_index/memex_projection_integrity.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ def verify_and_rehydrate_memex_projection(
6565
expected_source_revision: str | None = None,
6666
expected_access_policy_digest: str | None = None,
6767
expected_holoindex_generation_id: str | None = None,
68+
expected_operational_snapshot_id: str | None = None,
69+
expected_operational_snapshot_content_digest: str | None = None,
6870
) -> MemexProjectionIntegrityResult:
6971
"""Return a typed verified projection or fail closed.
7072
@@ -143,6 +145,16 @@ def verify_and_rehydrate_memex_projection(
143145
for record in records
144146
if isinstance(record.metadata, Mapping)
145147
}
148+
operational_snapshot_ids = {
149+
str(record.metadata.get("snapshot_id") or "").strip()
150+
for record in records
151+
if isinstance(record.metadata, Mapping)
152+
}
153+
operational_snapshot_content_digests = {
154+
str(record.metadata.get("snapshot_content_digest") or "").strip()
155+
for record in records
156+
if isinstance(record.metadata, Mapping)
157+
}
146158

147159
if len(foundup_ids) != 1:
148160
reasons.append("mixed_foundup_records")
@@ -156,6 +168,12 @@ def verify_and_rehydrate_memex_projection(
156168
policy_digests and next(iter(policy_digests)) != receipt.access_policy_digest
157169
):
158170
reasons.append("mixed_policy_digests")
171+
if len(operational_snapshot_ids) != 1 or not next(iter(operational_snapshot_ids or {""})):
172+
reasons.append("mixed_operational_snapshot_ids")
173+
if len(operational_snapshot_content_digests) != 1 or not next(
174+
iter(operational_snapshot_content_digests or {""})
175+
):
176+
reasons.append("mixed_operational_snapshot_content_digests")
159177

160178
if expected_foundup_id and foundup_ids != {_clean(expected_foundup_id)}:
161179
reasons.append("expected_foundup_mismatch")
@@ -171,6 +189,14 @@ def verify_and_rehydrate_memex_projection(
171189
expected_holoindex_generation_id
172190
):
173191
reasons.append("expected_generation_mismatch")
192+
if expected_operational_snapshot_id and operational_snapshot_ids != {
193+
_clean(expected_operational_snapshot_id)
194+
}:
195+
reasons.append("expected_operational_snapshot_id_mismatch")
196+
if expected_operational_snapshot_content_digest and operational_snapshot_content_digests != {
197+
_clean(expected_operational_snapshot_content_digest)
198+
}:
199+
reasons.append("expected_operational_snapshot_content_digest_mismatch")
174200

175201
for record in records:
176202
reasons.extend(_record_integrity_reasons(record, receipt=receipt))
@@ -256,6 +282,10 @@ def _record_integrity_reasons(
256282
reasons.append("metadata_source_revision_mismatch")
257283
if record.metadata.get("access_policy_digest") != receipt.access_policy_digest:
258284
reasons.append("metadata_access_policy_mismatch")
285+
if not str(record.metadata.get("snapshot_id") or "").strip():
286+
reasons.append("metadata_snapshot_id_missing")
287+
if not str(record.metadata.get("snapshot_content_digest") or "").strip():
288+
reasons.append("metadata_snapshot_content_digest_missing")
259289
section = str(record.metadata.get("section") or "").strip()
260290
if not section:
261291
reasons.append("missing_record_section")

holo_index/tests/test_holoindex_memex_projection_integrity.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,35 @@ def test_mixed_policy_digests_rejected() -> None:
138138
_assert_fail(projection, "mixed_policy_digests")
139139

140140

141+
def test_operational_snapshot_metadata_bindings_are_validated() -> None:
142+
projection = _projection()
143+
projection["records"][0]["metadata"] = dict(projection["records"][0]["metadata"])
144+
projection["records"][0]["metadata"]["snapshot_id"] = "other-snapshot"
145+
146+
_assert_fail(projection, "mixed_operational_snapshot_ids")
147+
148+
digest_mismatch = _projection()
149+
digest_mismatch["records"][0]["metadata"] = dict(digest_mismatch["records"][0]["metadata"])
150+
digest_mismatch["records"][0]["metadata"]["snapshot_content_digest"] = "sha256:other-snapshot"
151+
_assert_fail(digest_mismatch, "mixed_operational_snapshot_content_digests")
152+
153+
missing = _projection()
154+
missing["records"][0]["metadata"] = dict(missing["records"][0]["metadata"])
155+
missing["records"][0]["metadata"]["snapshot_id"] = ""
156+
_assert_fail(missing, "metadata_snapshot_id_missing")
157+
158+
_assert_fail(
159+
_projection(),
160+
"expected_operational_snapshot_id_mismatch",
161+
expected_operational_snapshot_id="other-snapshot",
162+
)
163+
_assert_fail(
164+
_projection(),
165+
"expected_operational_snapshot_content_digest_mismatch",
166+
expected_operational_snapshot_content_digest="sha256:other-snapshot",
167+
)
168+
169+
141170
def test_record_count_and_rejected_count_are_validated() -> None:
142171
projection = _projection()
143172
projection["receipt"]["records_indexed"] = 99

modules/communication/moltbot_bridge/ModLog.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# ModLog - moltbot_bridge
22

3+
## 2026-07-16: FOUNDUP_MEMEX_MULTI_FOUNDUP_SCOPE_HARDENING_PHASE1
4+
5+
**Author**: 0102 (Codex) | Commander: 012 | WSP: 00, 15, 50, 60, 97
6+
7+
- Hardened FoundUp Brain/Memex current-state assembly for resident
8+
multi-FoundUp operation.
9+
- Resident mode now requires explicit FoundUp scoping across identity,
10+
roadmap, outcomes, worker claims, queue items, and policy FoundUp scope.
11+
- Foreign worker claims and queue items are excluded with deterministic
12+
count/digest accounting instead of leaking into the selected FoundUp or
13+
rejecting the whole mixed snapshot.
14+
- Legacy single-FoundUp inference now requires explicit compatibility mode and
15+
marks inferred records with `legacy_single_foundup_compatibility`.
16+
- Added an assembly receipt to the read-only view with included/excluded work
17+
counts, excluded-record digest, policy scope, and no-write attestations.
18+
- Tests cover independent A/B FoundUp views from one mixed snapshot, resident
19+
rejection of unscoped records, explicit compatibility mode, and policy-scope
20+
mismatch.
21+
322
## 2026-07-16: HOLOINDEX_MEMEX_PROJECTION_INTEGRITY_AND_REHYDRATION_GATE_PHASE1
423

524
**Author**: 0102 (Codex) | Commander: 012 | WSP: 00, 15, 50, 60, 97

modules/communication/moltbot_bridge/src/foundup_brain_current_state.py

Lines changed: 138 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class FoundUpBrainView:
6464
verified_outcomes: tuple[dict[str, Any], ...]
6565
learning_candidates: tuple[dict[str, Any], ...] = ()
6666
roadmap_signals: tuple[dict[str, Any], ...] = ()
67+
assembly_receipt: dict[str, Any] | None = None
6768
invariants: dict[str, bool] | None = None
6869

6970
def to_dict(self) -> dict[str, Any]:
@@ -100,6 +101,9 @@ def assemble_foundup_brain_current_state(
100101
roadmap_state: Mapping[str, Any] | None = None,
101102
verified_outcomes: Sequence[Mapping[str, Any]] = (),
102103
now_iso: str | None = None,
104+
resident_mode: bool = True,
105+
legacy_single_foundup_compatibility: bool = False,
106+
policy_foundup_scope: Sequence[str] | None = None,
103107
) -> FoundUpBrainAssemblyResult:
104108
"""Assemble one FoundUp's current cognition from existing receipts."""
105109

@@ -129,28 +133,77 @@ def assemble_foundup_brain_current_state(
129133
identity_clean = _normalize_identity(identity)
130134
if not identity_clean.get("name"):
131135
reasons.append("missing_identity_name")
132-
identity_foundup_id = str(identity_clean.get("foundup_id", normalized_foundup_id)).strip()
133-
if identity_foundup_id and identity_foundup_id != normalized_foundup_id:
136+
identity_foundup_id = str(identity_clean.get("foundup_id", "")).strip()
137+
if not identity_foundup_id:
138+
if resident_mode and not legacy_single_foundup_compatibility:
139+
reasons.append("identity_missing_foundup_id")
140+
elif legacy_single_foundup_compatibility:
141+
identity_clean["scope_origin"] = "legacy_single_foundup_compatibility"
142+
elif identity_foundup_id != normalized_foundup_id:
134143
reasons.append("identity_foundup_id_mismatch")
135144
identity_clean["foundup_id"] = normalized_foundup_id
136145

137-
roadmap_clean = _normalize_roadmap_state(roadmap_state, normalized_foundup_id, reasons)
146+
policy_scope = tuple(str(item).strip() for item in (policy_foundup_scope or ()) if str(item).strip())
147+
if resident_mode:
148+
if policy_scope != (normalized_foundup_id,):
149+
reasons.append("policy_foundup_scope_mismatch")
150+
151+
roadmap_clean = _normalize_roadmap_state(
152+
roadmap_state,
153+
normalized_foundup_id,
154+
reasons,
155+
resident_mode=resident_mode,
156+
legacy_single_foundup_compatibility=legacy_single_foundup_compatibility,
157+
)
138158
outcomes_clean = tuple(
139-
_normalize_verified_outcome(outcome, normalized_foundup_id, reasons)
159+
_normalize_verified_outcome(
160+
outcome,
161+
normalized_foundup_id,
162+
reasons,
163+
resident_mode=resident_mode,
164+
legacy_single_foundup_compatibility=legacy_single_foundup_compatibility,
165+
)
140166
for outcome in verified_outcomes
141167
)
142-
active_work = _scope_work_records(
168+
active_work, excluded_active_work = _scope_work_records(
143169
snapshot.work_state.get("worker_claims", ()),
144170
normalized_foundup_id,
145171
"worker_claim",
146172
reasons,
173+
resident_mode=resident_mode,
174+
legacy_single_foundup_compatibility=legacy_single_foundup_compatibility,
147175
)
148-
queued_work = _scope_work_records(
176+
queued_work, excluded_queued_work = _scope_work_records(
149177
snapshot.work_state.get("wre_queue_items", ()),
150178
normalized_foundup_id,
151179
"queue_item",
152180
reasons,
181+
resident_mode=resident_mode,
182+
legacy_single_foundup_compatibility=legacy_single_foundup_compatibility,
153183
)
184+
excluded_records = excluded_active_work + excluded_queued_work
185+
excluded_record_digest = _digest(excluded_records)
186+
assembly_receipt_payload = {
187+
"schema_version": "foundup_memex_assembly_receipt.v1",
188+
"foundup_id": normalized_foundup_id,
189+
"snapshot_id": snapshot.snapshot_receipt_id,
190+
"snapshot_content_digest": snapshot.snapshot_content_digest,
191+
"resident_mode": resident_mode is True,
192+
"legacy_single_foundup_compatibility": legacy_single_foundup_compatibility is True,
193+
"policy_foundup_scope": policy_scope,
194+
"included_worker_claims": len(active_work),
195+
"included_queue_items": len(queued_work),
196+
"excluded_record_count": len(excluded_records),
197+
"excluded_record_digest": excluded_record_digest,
198+
"no_brain_write_performed": True,
199+
"no_breadcrumb_write_performed": True,
200+
"no_holoindex_mutation_performed": True,
201+
"no_queue_mutation_performed": True,
202+
}
203+
assembly_receipt = {
204+
**assembly_receipt_payload,
205+
"receipt_id": _digest(assembly_receipt_payload),
206+
}
154207

155208
candidate_payload = {
156209
"identity": identity_clean,
@@ -195,6 +248,7 @@ def assemble_foundup_brain_current_state(
195248
"verified_outcomes": outcomes_clean,
196249
"learning_candidates": (),
197250
"roadmap_signals": (),
251+
"assembly_receipt": assembly_receipt,
198252
}
199253
view_id = _digest(content)
200254
view = FoundUpBrainView(
@@ -218,6 +272,7 @@ def assemble_foundup_brain_current_state(
218272
"no_worker_spawn": True,
219273
"no_repo_mutation": True,
220274
},
275+
assembly_receipt=assembly_receipt,
221276
)
222277
return FoundUpBrainAssemblyResult(
223278
accepted=True,
@@ -236,10 +291,19 @@ def _normalize_roadmap_state(
236291
roadmap_state: Mapping[str, Any] | None,
237292
foundup_id: str,
238293
reasons: list[str],
294+
*,
295+
resident_mode: bool,
296+
legacy_single_foundup_compatibility: bool,
239297
) -> dict[str, Any]:
240298
data = dict(roadmap_state or {})
241-
scoped_id = str(data.get("foundup_id", foundup_id)).strip()
242-
if scoped_id and scoped_id != foundup_id:
299+
scoped_id = str(data.get("foundup_id", "")).strip()
300+
scope_origin = ""
301+
if not scoped_id:
302+
if resident_mode and not legacy_single_foundup_compatibility:
303+
reasons.append("roadmap_missing_foundup_id")
304+
elif legacy_single_foundup_compatibility:
305+
scope_origin = "legacy_single_foundup_compatibility"
306+
elif scoped_id != foundup_id:
243307
reasons.append("roadmap_foundup_id_mismatch")
244308
roadmap_id = str(data.get("roadmap_id", "")).strip()
245309
version = str(data.get("version", "")).strip()
@@ -257,16 +321,26 @@ def _normalize_roadmap_state(
257321
"content_digest": content_digest,
258322
"active_item_ids": tuple(str(value) for value in data.get("active_item_ids", ())),
259323
"blocked_item_ids": tuple(str(value) for value in data.get("blocked_item_ids", ())),
324+
"scope_origin": scope_origin,
260325
}
261326

262327

263328
def _normalize_verified_outcome(
264329
outcome: Mapping[str, Any],
265330
foundup_id: str,
266331
reasons: list[str],
332+
*,
333+
resident_mode: bool,
334+
legacy_single_foundup_compatibility: bool,
267335
) -> dict[str, Any]:
268-
scoped_id = str(outcome.get("foundup_id", foundup_id)).strip()
269-
if scoped_id and scoped_id != foundup_id:
336+
scoped_id = str(outcome.get("foundup_id", "")).strip()
337+
scope_origin = ""
338+
if not scoped_id:
339+
if resident_mode and not legacy_single_foundup_compatibility:
340+
reasons.append("verified_outcome_missing_foundup_id")
341+
elif legacy_single_foundup_compatibility:
342+
scope_origin = "legacy_single_foundup_compatibility"
343+
elif scoped_id != foundup_id:
270344
reasons.append("verified_outcome_foundup_id_mismatch")
271345
accepted = bool(outcome.get("accepted", False))
272346
held_out_passed = bool(outcome.get("held_out_passed", False))
@@ -287,6 +361,7 @@ def _normalize_verified_outcome(
287361
**required_ids,
288362
"accepted": accepted,
289363
"held_out_passed": held_out_passed,
364+
"scope_origin": scope_origin,
290365
}
291366

292367

@@ -295,19 +370,67 @@ def _scope_work_records(
295370
foundup_id: str,
296371
record_kind: str,
297372
reasons: list[str],
298-
) -> tuple[dict[str, Any], ...]:
373+
*,
374+
resident_mode: bool,
375+
legacy_single_foundup_compatibility: bool,
376+
) -> tuple[tuple[dict[str, Any], ...], tuple[dict[str, Any], ...]]:
299377
scoped: list[dict[str, Any]] = []
300-
for record in records:
378+
excluded: list[dict[str, Any]] = []
379+
for index, record in enumerate(records):
301380
data = dict(record)
302381
record_foundup_id = str(data.get("foundup_id", "")).strip()
303382
if record_foundup_id and record_foundup_id != foundup_id:
304-
reasons.append(f"{record_kind}_foundup_id_mismatch")
383+
excluded.append(
384+
_excluded_record_summary(
385+
record_kind=record_kind,
386+
record=data,
387+
index=index,
388+
foundup_id=record_foundup_id,
389+
reason=f"{record_kind}_foundup_id_mismatch",
390+
)
391+
)
305392
continue
306393
if not record_foundup_id:
394+
if resident_mode and not legacy_single_foundup_compatibility:
395+
reasons.append(f"{record_kind}_missing_foundup_id")
396+
excluded.append(
397+
_excluded_record_summary(
398+
record_kind=record_kind,
399+
record=data,
400+
index=index,
401+
foundup_id="",
402+
reason=f"{record_kind}_missing_foundup_id",
403+
)
404+
)
405+
continue
307406
data["foundup_id"] = foundup_id
308-
data["scope_origin"] = "legacy_single_foundup_poc"
407+
data["scope_origin"] = "legacy_single_foundup_compatibility"
309408
scoped.append(data)
310-
return tuple(scoped)
409+
return tuple(scoped), tuple(excluded)
410+
411+
412+
def _excluded_record_summary(
413+
*,
414+
record_kind: str,
415+
record: Mapping[str, Any],
416+
index: int,
417+
foundup_id: str,
418+
reason: str,
419+
) -> dict[str, Any]:
420+
identifier = (
421+
record.get("claim_id")
422+
or record.get("queue_item_id")
423+
or record.get("work_order_id")
424+
or record.get("slice_name")
425+
or record.get("selected_slice")
426+
or f"{record_kind}:{index}"
427+
)
428+
return {
429+
"record_kind": record_kind,
430+
"record_ref": str(identifier),
431+
"foundup_id": str(foundup_id),
432+
"reason": reason,
433+
}
311434

312435

313436
def _snapshot_expired(valid_until: str, now_iso: str) -> bool:

0 commit comments

Comments
 (0)