feat(graph): rfc 0047 slice 4 — emitter + erasure (RFC0047.10–.11), RFC green - #708
Conversation
compact_partition_hooked threads RowHooks through the rewrite: observe sees every input row once as decoded (the graph emitter's feed); drop removes rows from the consolidated output (a conversation-scoped erasure) and rewrites even a single-file partition so the erasure lands (CompactionOutcome.rows_dropped). hour_partitions is public for the erasure pass. Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
…re (RFC0047.10–.11) The graph emitter (ourios-ingester graph_emitter) derives the §3.3 tuples — conversation parent, participant/actor with their scoped_reader binding tuples, the per-tenant tool objects — from stored rows and writes them in ≤100-tuple idempotent batches (ourios.graph.tuples). It is fed from the compaction sweep (every input row it decodes, via the new RowHooks/SweepHooks) and from the receiver's PublishCoordinator after each durable batch. Naming is TenantObjects, the one place the rule lives. Erasure (§3.6): a durable store marker (erasure/tenant_id=…/ conversation=…, request_erasure) makes the next sweep rewrite every partition of the tenant with the conversation's rows dropped (single-file partitions too), advance the marker, then — after the blocking pass — read and delete the object's tuples, write the new conversation_erased audit event (RFC 0005 §3.7 kind 9, four OPTIONAL erasure_* columns; schema pin + round-trip updated) and remove the marker. Interrupted sweeps retry only the tuple deletion. The OpenFGA client gains read_by_object; the compactor exposes sweep_once (the daemon's per-tick body); main wires the emitter into the receiver and the compactor. Fake-backed unit tests + a real-container served-binary test (RFC0047.10/.11, and the proof that emitted tuples bind a participant end-to-end). RFC 0047 → green; guides updated. Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
|
Warning Review limit reached
Next review available in: 23 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (28)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Implements RFC 0047 slice 4 by adding a data-fed OpenFGA graph emitter (flush cadence + compaction sweep) and durable conversation erasure (store marker → row rewrite → tuple delete → audit), plus the supporting semconv, audit schema, and CI coverage to move RFC 0047 to green.
Changes:
- Add
GraphEmitterand wire it into the receiver publish cadence and compaction sweeps; addourios.graph.tuples{ourios.graph.tuple.operation}metric. - Add erasure request markers in object storage and a sweep flow that rewrites partitions to drop rows, then deletes OpenFGA tuples, then emits a
conversation_erasedaudit event (new kind 9 + optionalerasure_*columns). - Add unit + real-container integration tests (and CI job coverage) for RFC0047.10–.11.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| semconv/registry/metrics.yaml | Adds ourios.graph.tuples counter definition. |
| semconv/registry/attributes.yaml | Adds ourios.graph.tuple.operation attribute enum. |
| docs/rfcs/0047-rebac-resolver-and-graph-visibility.md | Marks RFC 0047 as green and records slice-4 decisions. |
| docs/rfcs/0005-parquet-storage.md | Updates audit-kind mapping table with kind 9 conversation_erased. |
| docs/guides/authentication.md | Documents graph feeding and erasure marker workflow for operators. |
| crates/ourios-server/tests/it/rfc0047_visibility.rs | Exposes helper fns to reuse in new emitter/erasure E2E tests. |
| crates/ourios-server/tests/it/rfc0047_emitter.rs | Adds real-OpenFGA container E2E test for emitter + erasure ordering. |
| crates/ourios-server/tests/it/rfc0047_9_tool_gate.rs | Updates scenario description to reflect tenant-wide bypass behavior. |
| crates/ourios-server/tests/it/main.rs | Registers the new RFC0047 emitter/erasure integration test module. |
| crates/ourios-server/src/receiver.rs | Threads optional graph emitter into the publish coordinator. |
| crates/ourios-server/src/main.rs | Builds graph emitter at startup (when configured) and wires it into receiver + compactor. |
| crates/ourios-semconv/src/lib.rs | Adds constants for the new metric and attribute key. |
| crates/ourios-parquet/tests/it/schema_pin.rs | Pins audit schema to include new optional erasure_* columns. |
| crates/ourios-parquet/tests/it/audit_round_trip.rs | Adds round-trip test for conversation_erased audit events. |
| crates/ourios-parquet/src/lib.rs | Exposes RowHooks, compact_partition_hooked, hour_partitions; adds audit column constants. |
| crates/ourios-parquet/src/compaction.rs | Implements RowHooks (observe + drop) and rows_dropped reporting; exports hour_partitions. |
| crates/ourios-parquet/src/audit_record_batch.rs | Writes new conversation_erased payload columns into audit batches. |
| crates/ourios-parquet/src/audit_reader.rs | Reads/decodes new conversation_erased payload from erasure_* columns (tolerant of absent columns). |
| crates/ourios-ingester/src/publish.rs | Derives and emits graph tuples asynchronously after durable publish (flush-cadence bridge). |
| crates/ourios-ingester/src/metrics.rs | Updates sweep-report test fixture for new report fields. |
| crates/ourios-ingester/src/lib.rs | Exposes graph_emitter module behind openfga feature. |
| crates/ourios-ingester/src/graph_emitter.rs | Adds GraphEmitter (derive tuples, emit in chunks, erase by read+delete) + unit tests. |
| crates/ourios-ingester/src/compactor.rs | Adds erasure marker workflow, sweep hooks, async graph phase, and audit emission for erasure completion. |
| crates/ourios-core/src/auth/openfga/mod.rs | Adds MCP_TOOL_NAMES and strengthens tenant/object-id length constraints. |
| crates/ourios-core/src/auth/openfga/client.rs | Adds bounded, paginated read_by_object for erasure tuple deletion. |
| crates/ourios-core/src/audit.rs | Adds ConversationErased audit payload + kind/type constants (kind 9). |
| crates/ourios-core/src/alias.rs | Ensures alias-map audit replay ignores the new audit payload kind. |
| .github/workflows/ci.yml | Extends openfga-resolver job to run RFC0047.10–.11 container test. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…hase, audit after transition Copilot on #708: request_erasure is create-if-absent (a repeat never resets an in-flight erasure); pending_erasures surfaces marker read errors and parses the phase leniently (whitespace/key order) instead of regressing to rows on any error; the conversation_erased audit event is written only by the process that removes the marker (at-most-once), and the ordering comment says lexicographic key order. Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
crates/ourios-ingester/src/compactor.rs:842
- If a sweep crashes after advancing a marker to the
tuplesphase (rows already dropped) but beforegraph_phasecompletes, the next sweep will seeErasurePhase::Tuplesand emit aconversation_erasedaudit event withpartitions_rewritten = 0/rows_dropped = 0(because the rows phase is skipped). This makes the audit payload incorrect for the interrupted-sweep case described in the RFC/PR description.
payload: AuditPayload::ConversationErased {
conversation_id: request.conversation_id.clone(),
partitions_rewritten: outcome.partitions_rewritten,
rows_dropped: outcome.rows_dropped,
tuples_deleted: to_u64(deleted),
crates/ourios-core/src/auth/openfga/client.rs:461
- Typo in error context: this formats
"read read: {e}", which is confusing in logs/errors. It should describe the actual failing step (reading response bytes).
.map_err(|e| OpenFgaError::Unavailable(format!("read read: {e}")))?;
Copilot on #708: the erasure filter reads the stored value (not the object-id-filtered one), so a conversation whose id can never be a graph object is still erased from the rows; its tuple deletion is a no-op (none could exist) and the audit event is honest. Unit-tested. Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.
Suppressed comments (2)
crates/ourios-ingester/src/compactor.rs:522
erase_pendingperforms potentially large Parquet rewrites, but on a committed rewrite it only updates the per-erasure outcome. The sweep-level accounting (SweepReport.bytes_read,compacted_files→ bytes_written,partitions_compacted/files_compacted/rows_compacted, andcompaction_events) is not updated for these rewrites, soCompactionMetrics::record_sweepunder-reports IO and the audit trail won’t show which files were rewritten as part of an erasure.
Consider recording erasure-triggered rewrites in the sweep report (and emitting compaction audit events for them), or introduce separate erasure IO/rewrites metrics if you intentionally want to exclude them from compaction metrics.
if o.committed.is_some() {
outcome.partitions_rewritten += 1;
outcome.rows_dropped += o.rows_dropped;
}
crates/ourios-core/src/auth/openfga/client.rs:462
- The error string
"read read: {e}"looks like a copy/paste typo and will be confusing in logs/telemetry. It should describe the failing step (reading the response bytes).
let bytes = response
.bytes()
.await
.map_err(|e| OpenFgaError::Unavailable(format!("read read: {e}")))?;
let page: ReadResponse = serde_json::from_slice(&bytes)
* fix(ingester): rfc 0047 erasure rewrites count as compactions; read error text Copilot follow-ups on #708: an erasure rewrite now feeds the sweep's IO accounting and audit trail like any compaction (partitions/files/rows/ bytes, a compaction event per committed rewrite, gc failures), so ourios.compaction.* stops under-reporting; the OpenFGA read error text names the step. Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org> * fix(ingester): erasure compaction events share the sweep timestamp Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org> --------- Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org>
Summary
RFC 0047 implementation slice 4 of 4 — the graph emitter and conversation erasure (RFC0047.10–.11, §3.3 / §3.6). Follows #705, #706, #707. With this, all twelve §5 criteria pass and the RFC moves to
green(validated/acceptedare yours).compact_partition_hookedthreadsRowHooksthrough the rewrite:observesees every input row once as decoded;dropremoves rows from the consolidated output (rewriting even a single-file partition so an erasure lands;CompactionOutcome.rows_dropped). New audit kind 9conversation_erasedwith four OPTIONALerasure_*columns (RFC 0005 §3.7 table rows 6–9 added — 6–8 were already in code but missing from the table; schema pin + round-trip updated).OpenFgaClient::read_by_object(paginatedRead, bounded);TenantObjects::conversation_fitsand an encoded-length bound inTenantObjects::new(Copilot's feat(auth): rfc 0047 slice 2 — planner two-step + visibility (RFC0047.4–.8) #706 follow-up: percent-encoding can grow the tenant segment past 256 B).MCP_TOOL_NAMESshared vocabulary.graph_emitter::GraphEmitter: derives the §3.3 tuples (conversation:T/<id>#parent@tenant:T,#participant@user:<user.hash|enduser.pseudo.id>+tenant:T#scoped_reader@user:…,#actor@agent:<gen_ai.agent.id>+ binding, and the per-tenanttool:T/<name>#parentobjects so operators grantcalleronly); values that cannot be object ids are skipped; writes in ≤100-tupleon_duplicate=ignorebatches, counted onourios.graph.tuples{ourios.graph.tuple.operation}(registry + weaver regen). Fed from both hooks: the compaction sweep (SweepHooksobserve on every row it decodes) and the receiver'sPublishCoordinatorafter each durable batch (off the flush path).sweep_onceis the daemon's per-tick body (blocking pass → async graph phase → blocking audit/marker phase), used byCompactor::runand the tests.erasure/tenant_id=<enc>/conversation=<enc>,request_erasure/pending_erasures): the object store is the source of truth (§3.6), no new network surface or credential, and an operator can write it with tooling they have. The sweep rewrites every partition of the tenant with the conversation's rows dropped, advances the marker to thetuplesphase, then — after the blocking pass — reads and deletes the object's tuples, writes theconversation_erasedaudit event after the sweep's compaction events, and removes the marker. Interrupted sweeps retry only the tuple deletion; an unreachable graph leaves the marker; without a bound conversation object a marker is a recorded sweep error, never silently dropped.conversationobject is bound) and wired into the receiver's coordinator and the compactor; a compactor-only pod gets it too.openfga/openfgacontainer, including the end-to-end proof that emitted tuples bind a participant on the served binary (no operator-written conversation tuples anywhere) and that a tenant-wide reader no longer sees the erased rows. Theopenfga-resolverCI job runs all three container tests. Green locally.green; RFC 0005 §3.7 amendment row.Decision to flag (RFC-level, chosen here)
The erasure request channel (store marker, above) was not specified by the RFC. Alternatives were an admin HTTP endpoint (new surface + auth) or a programmatic-only API (not operator-usable). If you'd rather have a CLI verb / MCP tool over the marker, that's the §9 follow-on.
Invariants / hazards touched
TenantObjectsrule.Related
RFC: docs/rfcs/0047-rebac-resolver-and-graph-visibility.md (spec #704; slices #705, #706, #707).
Checklist
cargo fmtcleancargo clippyclean (no new warnings)🤖 Generated with Claude Code
https://claude.ai/code/session_01JZXtbyWoQY19ZGtNecDfgv