Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions continuityos/person_twin_admission_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
)
from .person_twin_authority_replay import _current_admission
from .person_twin_privacy_provenance import validate_person_twin_record
from .person_twin_r21h_evidence import _require_current_meta
from .person_twin_r21h_evidence import MEMORY_BINDING_SCHEMA, _require_current_meta

SCHEMA = "continuityos.person-twin.durable-admission/v1"
MAX_RECORD_BYTES = 1024 * 1024 # bounded lab input, not a calibrated production limit
Expand Down Expand Up @@ -346,6 +346,25 @@ def load_current(self) -> AdmissionSnapshot | None:
self._con.execute("ROLLBACK")
raise DurableAdmissionError("ADMISSION_CURRENT_READ_FAILED") from None

def read_current_memory_binding_bytes(self) -> bytes | None:
"""Project persisted bindings from one verified read-only CURRENT snapshot.

No intent or caller-supplied row ID is accepted. load_current() validates
schema, saved hashes, the actual Memory row, R3 receipt and pointer first.
This adds no SQL schema, writes, issuer, or fresh authorization.
"""
snapshot = self.load_current()
if snapshot is None:
return None
binding = snapshot.binding
return _canonical({
"schema": MEMORY_BINDING_SCHEMA,
"pointer_sha256": _hash(snapshot.pointer_bytes),
"candidate_sha256": _hash(snapshot.candidate_bytes),
"memory_id": binding["memory_id"], "namespace": binding["namespace"],
"person_record_id": binding["candidate_id"], "source_record_id": binding["source_record_id"],
})

def read_current_pointer_bytes(self) -> bytes | None:
if self._phase == "PROMOTING" and self._con.in_transaction:
row = self._current_row()
Expand Down Expand Up @@ -431,7 +450,8 @@ def _result(self, status: str, reason: str, snapshot: AdmissionSnapshot | None =
candidate_commit_state=self._candidate_commit_state,
commit_state=self._commit_state,
possible_persisted_effect=self._candidate_commit_state != "NOT_ATTEMPTED" or self._commit_state != "NOT_ATTEMPTED",
publication_id=self._next_publication,
attempt_publication_id=self._next_publication,
publication_id=(self._next_publication if self._commit_state == "CONFIRMED" else None),
admission_receipt=None if snapshot is None else snapshot.receipt,
**_CEILING)

Expand Down
37 changes: 37 additions & 0 deletions continuityos/person_twin_r21h_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
from .sovereign_twin_runtime_r21b import TwinEvidence


MEMORY_BINDING_SCHEMA = "continuityos.person-twin.r21h-memory-binding/v1"


class PersonTwinEvidenceError(ValueError):
"""Fail-closed, fixed-code error containing no protected input or anchors."""

Expand Down Expand Up @@ -70,6 +73,16 @@ def read_context(self) -> Mapping[str, Any]:
...


class PersonAdmissionMemoryReadStore(Protocol):
def read_current_memory_binding_bytes(self) -> bytes | None:
"""Read the saved row binding and CURRENT commitments, never derive from intent.

Required for R21H, in addition to the unchanged R4 read-store methods.
None or an unavailable/malformed read is DENY, not a legacy fallback.
"""
...


class MemoryReadStore(Protocol):
def get(self, rid: int) -> Any: ...

Expand Down Expand Up @@ -102,6 +115,29 @@ def _require_current_meta(raw: Any, at: str) -> None:
raise PersonTwinEvidenceError("PERSON_MEMORY_NOT_YET_CURRENT")


def _require_admission_memory_binding(context: Mapping[str, Any], intent: PersonReadIntent) -> None:
reader = getattr(context["admission_store"], "read_current_memory_binding_bytes", None)
if not callable(reader):
raise PersonTwinEvidenceError("PERSON_ADMISSION_MEMORY_BINDING_UNAVAILABLE")
try:
stored = reader()
except Exception:
raise PersonTwinEvidenceError("PERSON_ADMISSION_MEMORY_BINDING_READ_FAILED") from None
expected = {
"schema": MEMORY_BINDING_SCHEMA,
"pointer_sha256": _canonical_hash(context["current_pointer"]),
"candidate_sha256": _canonical_hash(context["person_record"]),
"memory_id": intent.memory_id, "namespace": intent.namespace,
"person_record_id": intent.person_record_id, "source_record_id": intent.source_record_id,
}
expected_bytes = json.dumps(expected, ensure_ascii=False, sort_keys=True,
separators=(",", ":"), allow_nan=False).encode("utf-8")
# Exact canonical bytes reject missing/extra fields, duplicates, and bool IDs.
# The persisted row binding comes from the read-store, never from the intent.
if type(stored) is not bytes or stored != expected_bytes:
raise PersonTwinEvidenceError("PERSON_ADMISSION_MEMORY_BINDING_MISMATCH")


class PersonTwinR21HEvidenceAdapter:
"""R2 filter + R4 READ + exact existing-memory projection, without retrieval.

Expand Down Expand Up @@ -184,6 +220,7 @@ def _read(
binding = decision.get("binding", {})
if any(binding.get(key) != value for key, value in expected_binding.items()):
raise PersonTwinEvidenceError("PERSON_R4_BINDING_MISMATCH")
_require_admission_memory_binding(context, intent)
payload = source["payload"]
text = payload.get("text") if isinstance(payload, dict) else None
if not isinstance(text, str) or not text.strip():
Expand Down
94 changes: 94 additions & 0 deletions docs/PERSON_ADMISSION_PUBLICATION_RESULT_A3_R1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# A3 R1: distinguish an attempted publication from a confirmed COMMIT

Scope: the outer result of `PersonTwinAdmissionStore.admit()` only. Parent:
`fe04df409f86bc773d7210dbfaefa620afc3a231`, tree
`8b52fb2e3986e2c159cbcf38e0e56497e0369b4a` (local A4 candidate).
This correction is not a new admission issuer, SQL schema, R3/R4 algorithm,
backup/restore implementation, runtime generation or production admission.

## Reproduced defect

On the parent revision, denial immediately before the promotion COMMIT returns
HOLD / `commit_state=NOT_ATTEMPTED`, yet `publication_id` contains a generated
identifier absent from `person_admission_publications`. The staged candidate may
already be committed separately. A failed re-publication preserves the previous
CURRENT but similarly returns an identifier for the failed attempt.

No successful authorization was observed on these HOLD paths. The defect is an
ambiguous result contract, not a demonstrated bypass of HOLD or a data breach.

## Result contract

The existing outer result schema name remains
`continuityos.person-twin.durable-admission/v1`. This is an additive field plus a
narrowing of the meaning of its existing `publication_id` field:

- `attempt_publication_id`: the identifier allocated to this invocation's
prospective publication; null if no identifier was allocated. It can be used
for later explicit reconciliation. It is not proof of persistence, freshness,
authorization or successful rollback.
- `publication_id`: populated only when this invocation's promotion
`commit_state` is `CONFIRMED`. It equals `attempt_publication_id`. Otherwise
it is null. It never substitutes the pre-existing CURRENT identifier.
- `commit_state`: unchanged: `NOT_ATTEMPTED`, `UNKNOWN`, `CONFIRMED` describe
the promotion COMMIT observation. Candidate COMMIT state remains separate.
- `status`: unchanged. PASS still requires confirmed promotion and successful
fresh readback. A confirmed COMMIT can still be reported as HOLD when fresh
readback fails. Such a HOLD retains its confirmed `publication_id` but returns
no `admission_receipt` and grants no permission.

| Observed case | attempt_publication_id | publication_id | Status / commit_state |
| --- | --- | --- | --- |
| Rejected before allocation | null | null | HOLD / NOT_ATTEMPTED |
| Rejected after allocation, before COMMIT | attempt ID | null | HOLD / NOT_ATTEMPTED |
| Promotion COMMIT response uncertain | attempt ID | null | HOLD / UNKNOWN |
| COMMIT confirmed, fresh readback failed | attempt ID | same ID | HOLD / CONFIRMED |
| COMMIT confirmed, fresh readback verified | attempt ID | same ID | PASS / CONFIRMED |

If the COMMIT response is lost after SQLite actually commits, `publication_id`
still remains null: the operation could not confirm success. The attempt ID is
retained for investigation. If COMMIT fails before persistence, the same public
UNKNOWN result is conservative even when a later read proves there is no row.
The implementation does not automatically retry, reclassify UNKNOWN, or erase
possible effects. `ROLLBACK_UNVERIFIED` still suspends the store and retains
`possible_persisted_effect=true`.

A non-null `publication_id` confirms the observed COMMIT on that connection; it
does NOT by itself attest current database-path identity, continued CURRENT
status, fresh consent, or successful post-COMMIT verification. Consumers must
continue checking the complete result and obtaining fresh authorization.

## Compatibility boundaries

The change affects only the outer Python outcome dictionary. Existing SQL DDL,
stored candidate/pointer/binding/R3 receipt bytes, `AdmissionSnapshot`, CURRENT
semantics, transaction ordering, public R21H evidence receipt and the product
demo's strict receipt validators are unchanged. There is no data migration.
Strict consumers of the outer dictionary must accept the newly documented
`attempt_publication_id`. Consumers previously treating every `publication_id`
as an attempt identifier must use the new field instead.

## Tests and acceptance evidence

The existing independent A3/A4/N4 acceptance bodies are rerun without changes.
A3 must change from red on the parent to green; A4 exact row binding and the
backup -> revoke -> restore test with a fresh external ledger must stay green.

Nineteen new synthetic cases cover early and late denials, confirmed success,
COMMIT response loss before/after actual SQLite COMMIT for both phases, failed
or mismatched post-COMMIT readback, failed re-publication, identifier reset and
unverified rollback. Existing test bodies are preserved. All DBs are temporary.
A separate deterministic review compares old/new result functions across state
combinations and verifies that transaction methods and guards are unchanged.
Actual test receipts and the candidate manifest are external build artifacts.

## Remaining limits

Local synthetic tests do not replace fresh official wheel/CI, independent review
of this specific change, or owner signoff. A4 still has its own local candidate
and publication status. A3 does not address trust-origin, stale external ledger,
legacy effect-path bypass, OS isolation or cross-machine restoration questions.

`NO_PUSH`, `NO_PR_MUTATION`, `NO_MERGE`, `NO_DEPLOY`, `C0 HOLD`,
`T11/T12 UNEXECUTED`, `NOT_PRODUCTION_ADMITTED`, `can_trade=false`,
`capital_permission=DENY` remain in force for this local stage.
82 changes: 82 additions & 0 deletions docs/PERSON_R21H_MEMORY_BINDING_A4_R1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Person R21H — exact saved Memory binding (A4 R1)

Base: `b11af1a69957e77accc0a6d74dedb7c4dab8ee28`, tree
`69893ca2d8417c5b1904863b97e506f536168134` (Draft PR #213).
Scope: local A4 repair, not A3, a new runtime, an authority issuer, or a schema migration.

## Reproduced defect

An existing durable admission bound `memory_id=3`. A trusted-host read intent
selected another row `memory_id=4` with identical text and namespace. R2/R4
validated the Person/source candidate, while R21H checked the selected row
against the intent and content only. The recording model received `mem:4`.
Different text, namespace, superseded metadata and absent rows were already
rejected. This was a P2 exact-source-reference defect, not a reproduced
cross-tenant disclosure or arbitrary-content read.

## Correction and compatibility boundary

R21H admission stores must now implement:

```python
read_current_memory_binding_bytes() -> bytes | None
```

The unchanged R4 protocol and evaluator do not acquire this requirement. It is
required only when a host configures `PersonTwinR21HEvidenceAdapter`. The durable
`PersonTwinAdmissionStore` implements the method by calling the existing
`load_current()` and projecting the saved binding plus CURRENT commitments from
that single verified snapshot. No intent, requested id or namespace is passed
to this read method. It does not read from a second database, create a schema,
write an admission, cache ALLOW, or issue fresh consent/authority.

The canonical UTF-8 JSON readback contains exactly:

- `schema = continuityos.person-twin.r21h-memory-binding/v1`;
- `pointer_sha256`, `candidate_sha256`;
- `memory_id`, `namespace`, `person_record_id`, `source_record_id`.

The adapter compares exact canonical bytes to its independently pinned intent
and R4 context **before** selecting the Memory row. Missing method, non-callable
method, read exception, no CURRENT, malformed/noncanonical bytes, extra fields,
duplicate keys and any binding mismatch deny the request. There is no fallback
to content-only matching. Existing integrations supplying only the minimal R4
read-store interface now deliberately fail closed at R21H until they implement
this saved-binding read contract; do not synthesize the value from request pins.

The repository's earlier in-memory R21H test store was extended only in its test
fixture: it saves the binding once after actual fixture admission/Memory seeding,
before a read intent or provider is constructed. The existing demo uses the
durable store and needs no change. Non-Person runtime paths are untouched.

The saved-binding read runs independently in both prepare and final prompt
validation. Public receipt keys and schema are preserved exactly; no new public
commitment field is added. A changed binding after prepare denies before model
generation. The existing demo keeps its exact-key receipt validator unchanged.

## Validation and limits

Acceptance bodies from the prior A3/A4/N4 reproduction are preserved unchanged;
only their test import origin is redirected to this local checkout. The baseline
must reproduce A4. After correction A4 must become green while A3's documented
publication-result ambiguity remains red. A3 is not silently reclassified or fixed.

New tests exercise warm chat and cold streaming, exact/identical-other/mismatched
rows, unavailable and corrupted readback, prompt-boundary drift, query-only reads,
no-CURRENT and suspended-store rejection. Existing R1-R4, R21H, demo, export,
backup and restore tests are included in local qualification. Run counts and
source hashes belong to the external immutable execution manifest, not a claim
that all repository tests or official wheel CI ran.

The read port remains a trusted-host integration boundary, not a cryptographic
attestation or authentication of arbitrary Python objects. A malicious host that
supplies fabricated stores/identities is not solved here. Database identity across
distinct read stores, generation/ABA handling (A6), receipt provenance (A5), H3/C0
native identity, independent revocation-witness freshness and real model egress
remain separate requirements. Restore continues to require a fresh external
revocation ledger. No production or live-data qualification is asserted.

`NOT_PRODUCTION_ADMITTED`; `C0 HOLD`; `T11/T12 UNEXECUTED`;
`execution_authority=NONE`; `can_execute=false`; `can_trade=false`;
`capital_permission=DENY`. No push, PR mutation, merge, deploy, Scheduler, TPM,
installed-runtime change or real Memory access is part of this local stage.
Loading
Loading