Register item 16 — Step-0 verification + build record (2026-08-20)
Read-only verification of "gemini-kg-pipeline FirestoreKGSink deviation —
migrate it onto the governed path" before building it, and the record of what
was then built. Claims are measured in-repo at file:line unless labeled
otherwise. Companion to docs/reports/ITEM15_STEP0_VERIFICATION_2026-08-20.md
(which built the canonical→KG projector this item's register line pointed at).
Headline
The deviation is real but narrower than the register implied, carries measured zero traffic, and its stated landing surface was wrong: the item-26 projector is entity-only by construction (no person nodes), so journey/person projection can never move into it. The correct migration is the Shopify single-ingress shape — the canonical door comes FIRST, and the pipeline's own PII-governed projection stays local as the post-ingestion hook.
Measured surface of the deviation
Two FirestoreKGSink construction sites only:
pipeline.py:434 (build_default_pipeline, reached by
/api/v1/journey-events/{ingest,sync,meta/sync}, /webhooks/gemini,
batch_worker.py, the meta-worker Job) and virtuoso_ingest.py:153
(/api/v1/journey-events/virtuoso/{source}, the only person-adjacent path —
virtuoso_kg.py:96-101 emits PERFORMED_BY edges keyed on actor.user_id).
The nightly/weekly mycocoons jobs are NOT this deviation — they write the
disjoint nodes/edges pair via FirebaseKGClient (item 15/8a's lane).
Zero live traffic, positively evidenced: no scheduler targets any
journey-events route (setup_ingest_schedulers.sh:74,93,112), the Boss
registry exposes none of them as tools, and the platform record measures
JourneyEvent nodes at 0 with the projection wiring deployed
(shared-memory record …-starved-aea9fe2f; connectors memory rule-6c row).
Preconditions the register line did not name
- IAM: the service pins NO runtime SA (default compute SA) and canonical
ingestion's allowlist is exactly
mizoki-platform@…— every forward would 403 until the operator either pins this service's SA (after auditing what the default SA currently grants it) or extends the allowlist deliberately. WideningALLOWED_CALLER_SAto the default compute SA would be a least-privilege regression (≈78 services run as it) — do not. - No
tenant_idconcept anywhere in the service; canonical ingestion requires one (TENANT-001). mizoki_contractsis not in the image (Dockerfile copies requirements + src/ only) — the forwarder therefore mirrors the D17-fixed OIDC mint (origin audience +format=full) in stdlib, pinned by test.- No CI test gate ran the in-service suites before this change.
- Erasure trap (sharpest risk): canonical-store subject erasure matches
ONLY on
entity_idsintersection (mizoki_contracts/erasure.py). Forwarding virtuoso payloads verbatim (actor.email/ip/ua— persisted today invirtuoso_journey_events) with emptyentity_idswould create UN-ERASABLE subject data in the canonical store — strictly worse than the deviation being fixed.
What was built (this change set)
src/journey_events/canonical_forwarder.py— one client, both doors. FlagJOURNEY_EVENTS_CANONICAL_FORWARD, default OFF (literal, pinned); D17-mirrored OIDC mint; forwarded virtuoso actors reduced to the KG boundary's pseudonym allowlist (user_id/phone_sha256/device_ifa— mirror asserted againstvirtuoso_kg._ACTOR_ID_KEYSby test); person-token keys dropped from door-A free-form dicts at any depth; every forward carries subject-resolvableentity_idsplusoriginal_payload_sha256provenance so lineage to the verbatim local record survives without re-persisting PII.- Door A (
pipeline.py): forward runs after validation, BEFORE any local durable write or KG projection; a refused door returnsok=false(retryable — content-derived canonical ids + merge-idempotent local writes make the retry converge). - Door B (
virtuoso_ingest.py): validate → forward → CAS → project, in that order, so a refused door leaves nothing CAS'd and nothing projected — no stranded "duplicate" that was never forwarded. The route surfaces the refusal as 502 (main.py), never 422. /healthgains acanonical_forward {enabled, configured, tenant_id}disclosure (IAM-locked service — not a publication).cloudbuild.yamlenv list (REPLACE semantics) gainsCANONICAL_INGESTION_URL+DEFAULT_TENANT_ID=mycocoons; the flag is deliberately absent — activation is its own reviewed diff after the IAM preconditions land.deploy-gemini-kg-pipeline.ymlgains a test-gate job (233 tests; the deploy previously ran only a container preflight).SERVICE_VERSION1.4.0 → 1.4.1; journey_events README documents the door.- Tests:
tests/test_canonical_forwarder.py(15) — flag posture as source literal, both doors' ordering in both directions (flag-off byte-identical; refused door writes nothing), PII allowlists, entity_ids reachability, mint discipline. Full in-service suite 233/233 green (python -m unittest discover -s tests, pydantic 2.11.7 venv).
Honest end condition
This is zero-traffic surgery: the governed order is pinned by tests and
flag-ready, not live-proven — no real journey events flow to prove the path
end-to-end, and none can until the operator activates it. Remaining operator
work, in order: (1) resolve the runtime-SA question (pin vs. allowlist — an
owner/operator decision with a grant audit), (2) grant run.invoker on
service-canonical-ingestion to that SA and add it to ALLOWED_CALLER_SA
in deploy-service-canonical-ingestion.yml (reviewed commit), (3) register
the mycocoons tenant mapping for that caller, (4) flip
JOURNEY_EVENTS_CANONICAL_FORWARD=1 in cloudbuild.yaml (reviewed commit).
Until then the deviation remains OPEN-but-instrumented: the direct write
still serves (unchanged, zero traffic), and the governed path is one flag
away.