Separated projection boundaries¶
OpenMed can store identified and de-identified Journey projections in physically separate local namespaces while applying the same purpose, role, attribute, consent, export, and audit rules to every access. The boundary is a storage and policy primitive. It is not a claim that a projection is clinically correct, legally sufficient, or safe for an autonomous care action.
The default implementation is offline after installation. It makes no network request, sends no telemetry, and does not require a hosted policy service.
Physical and capability separation¶
ProjectionBoundary creates three private paths beneath a caller-owned root:
projections/
├── identified/
│ ├── metadata.sqlite3
│ └── objects/sha256/...
├── deidentified/
│ ├── metadata.sqlite3
│ └── objects/sha256/...
└── audit/
└── events.sqlite3
On POSIX systems, directories use owner-only permissions. On Windows, provision the root with an ACL restricted to the service account; Python permission bits do not establish equivalent Windows access control. Each namespace has its own SQLite metadata database and content-addressed object tree. An identifier present in the identified database is unknown to the de-identified database unless a separate de-identified record was explicitly written.
The APIs also have different capabilities:
DeidentifiedProjectionAPIcan read, write, correct, review, and export only de-identified records. It has no transform-vault resolver and exposes no identified storage path.IdentifiedProjectionAPIis created only when the caller supplies aTransformVault. It can record and resolve protected pseudonymization or date-shift material after the same policy checks.
Do not use filesystem permissions alone as an authorization system. Run the process under a dedicated OS identity, encrypt the volume, and isolate backups according to the deployment's retention and access requirements.
Create the two APIs¶
from openmed.compliance import (
InMemoryTransformVault,
ProjectionBoundary,
)
boundary = ProjectionBoundary("./journey-data/projections")
deidentified = boundary.deidentified()
# The in-memory vault is suitable for tests and short-lived local workflows.
# Production callers should supply a protected TransformVault implementation.
identified = boundary.identified(InMemoryTransformVault())
The boundary rejects symlinked namespace directories. Metadata migrations are ordered and checksummed. A newer or drifted schema returns the typed unsupported state through ProjectionBoundary.open() rather than attempting an unsafe downgrade.
Policy requests¶
Every operation requires a patient-free ProjectionPolicyRequest:
from openmed.compliance import (
ProjectionNamespace,
ProjectionOperation,
ProjectionPolicyRequest,
)
request = ProjectionPolicyRequest(
operation=ProjectionOperation.WRITE,
namespace=ProjectionNamespace.DEIDENTIFIED,
purpose="care",
role="clinician",
attributes=(),
data_use_tags=(),
consent_state="active",
)
The request accepts controlled policy codes, not subject identifiers, patient values, reviewer names, or free-text explanations. The default policy:
- permits identified access only to
clinician,data_steward, andprivacy_officerroles carryingidentified_access; - permits de-identified access to those roles and
researcher; - denies research and quality purposes in the identified namespace;
- denies unknown roles, purposes, namespace changes, and operation changes;
- applies existing data-use tags, including
no-exportandconsent-withdrawn; - returns
reviewfor unknown consent and exports without explicit approval; - requires
export_approvedfor de-identified export and bothexport_approvedandidentified_export_approvedfor identified export.
allow, deny, and review are distinct outcomes. Review is returned as a typed partial result and never converted into success. An optional consent hook may return allow, deny, review, or unknown. Hook failure and unsupported hook output deny safely without persisting exception text.
Write and read a de-identified projection¶
written = deidentified.write(
"projection_0123456789abcdef",
b'{"synthetic":"deidentified"}',
request,
consent_scope="care-summary",
consent_revision="receipt-v1",
occurred_at="2026-01-02T03:04:05Z",
)
assert written.ok
read_request = ProjectionPolicyRequest(
operation=ProjectionOperation.READ,
namespace=ProjectionNamespace.DEIDENTIFIED,
purpose="care",
role="clinician",
)
payload = deidentified.read(
"projection_0123456789abcdef",
read_request,
consent_scope="care-summary",
consent_revision="receipt-v1",
occurred_at="2026-01-02T04:04:05Z",
)
assert payload.ok
Bytes are stored by SHA-256 digest and reverified on every read. Public ProjectionRecord metadata contains only digest, byte count, consent fingerprints, namespace, version, and timestamp. Projection content is excluded from repr, serialization, audit events, and error messages.
Writing the same first version is idempotent. Reusing an identifier for other content returns conflict. correct() appends a new immutable version; it does not overwrite the earlier blob. Reads return the latest version.
Consent withdrawal and cached projections¶
Each API uses the existing fingerprint-only ConsentCache. Successful writes and reads populate it under consent scope and revision fingerprints. Withdrawal invalidates matching cached values and leaves a revocation tombstone:
event = deidentified.withdraw_consent("care-summary", "receipt-v1")
assert event.invalidated_count >= 0
Later reads, writes, exports, corrections, and reviews using that exact consent pair return denied with consent_withdrawn, even if a caller mistakenly sends consent_state="active". Applications that require revocation persistence across process restarts must supply a consent hook backed by their authoritative consent system; the built-in cache is intentionally process-local.
Protected transform vault¶
TransformVault is a narrow protocol for transform material. Its public TransformRecord contains only transform type, input/output digests, timestamp, and an opaque identifier. The secret mapping or date offset is passed separately and is available only through IdentifiedProjectionAPI.resolve_transform().
InMemoryTransformVault deliberately provides no persistence or encryption and is not a production key-management solution. A production implementation must protect material at rest, keep keys outside public records, enforce its own retention policy, and return typed StoreResult outcomes.
Value-free audit events¶
Every read, write, export, correction, and review attempt produces a durable ProjectionAuditEvent. Events contain:
- operation and namespace;
allow,deny,review, orfailureoutcome;- controlled reason code;
- policy-request and resource digests;
- timestamp and schema version.
They never accept or serialize projection bytes, subject identifiers, reviewer identity, vault material, credentials, or exception messages. If the audit event cannot be persisted, the API returns failure; it does not report the protected operation as successful without its audit evidence.
Run boundary.integrity_check() after restart or restore. It verifies canonical metadata hashes, namespace bindings, every referenced content digest, and audit event hashes. It does not prove consent validity, clinical correctness, or the security of a caller-supplied vault.
Schemas and local verification¶
ProjectionRecord, ProjectionPolicyDecision, ProjectionAuditEvent, and TransformRecord use schema version 1.0.0. Their JSON Schemas are bundled in the package and load without network access:
from openmed.compliance import load_all_projection_schemas
schemas = load_all_projection_schemas()
assert schemas["record"]["schema_version"] == 1
The focused synthetic suite covers canonical schemas, physical separation, restart, correction transactions, consent invalidation, vault isolation, denied/review/allowed exports, value-free audit events, migration refusal, and cross-namespace bypass attempts: