IBEX Harness
DocsBenchmarksBlogChangelogRoadmap
GitHub
IBEX Harness

Documentation

Architecture Decision RecordsADR-0002: Repository foundation bootstrapADR-0003: Branch protection and merge policyADR-0004: Protobuf and code generation policyADR-0005: Postgres migration strategyADR-0006: Auth protobuf contract (`ibex.auth.v1`)ADR-0007: Auth token validation implementationADR-0008: Security scanning and CI quality gatesADR-0009: Permission bitmap layoutADR-0010: Cryptography policyADR-0011: Proxy auth gRPC client and middlewareADR-0012: Proxy request normalization (OpenAI chat)ADR-0013: Proxy input validation and stable error envelopeADR-0014: Core domain migration sequencingADR-0015: Proxy rate limit skeleton (Phase 1)ADR-0016: Proxy agent identity verification (Phase 1)ADR-0017: Request ID and trace context strategy (Phase 1)ADR-0018: Graceful shutdown contract (Phase 1)ADR-0019: OpenTelemetry provider configuration (Phase 1)ADR-0020: Shared package boundaries — `packages/config` and `packages/apierror`ADR-0021: Prometheus Metric Catalog (Phase 1)ADR-0022: Health check contract (Phase 1)ADR-0023: Docs site architecture (Phase 1.5)ADR-0024: Benchmark data publishing modelADR-0025: LLM provider abstractionADR-0026: OpenAI client designADR-0027: Streaming dual-write strategyADR-0028: Auth cache designADR-0029: Token revocation propagation via Redis pub/subADR-0030: Directive versioning strategyADR-0031: System prompt injection strategyADR-0032: Session data model and retentionADR-0033: ClickHouse llm_traces schema and retentionADR-0034: Proxy overhead performance measurement methodologyADR-0035: Chat Idempotency-Key Redis dedupeADR-0038: Context assembly service design and gRPC contractADR-0039: Proxy Postgres ownership for session and directive storesADR-0040: Anthropic provider adapterADR-0041: Model capability registryADR-0042: Self-hosted OpenAI-compatible LLM adapterADR-0043: Tokenizer registry architectureADR-0044: Non-streaming response pipelineADR-0045: Streaming response transformationADR-0046: Embedder interface and profile registryADR-0047: Memory temporal validity foundationADR-0048: Memory multi-label categoriesADR-0049: Memory relationship graph readinessADR-0050: MCP server skeleton (transport, auth, audit)ADR-0051: Local LGTM observability stack (Phase 2.5 exit pull-forward)ADR-0052: Memory schema v2 expand (HNSW, quality columns)ADR-0053: Vector store abstraction and composite scoring v2ADR-0054: In-process Presidio PII detection in memoryADR-0055: Memory write-path exact and near-duplicate dedupADR-0056: Temporal-interval-aware conflict detectionADR-0057: Memory write-path persistence and orchestrationADR-0058: Semantic search FTS fallback triggerADR-0059: Hot cache read pathADR-0060: Memory service tenant isolation architectureADR-0061: Retrieval-quality benchmark methodologyADR-0062: Worker task observability and dead-letter handlingADR-0063: Extraction output schema v2ADR-0064: Extraction provider registry and batch executionADR-0065: Extraction batch idempotency and turn pointer orderingADR-0066: Extraction quality evaluation harnessADR-0067: Context capability catalog generate-and-diff for token budgetsADR-0068: Composite scoring-time relevance gateADR-0069: Context packer bounded DP knapsackADR-0070: Context formatting locked ordering and session nonceADR-0071: Context assembly gRPC degradation ladder and retrieval deadlineADR-0072: Extraction enqueue via worker HTTP (not Celery Redis wire format)ADR-0073: Organization Lifecycle and Cascading SuspensionADR-0074 Provider Credential StorageADR-0075 Per-Org Model Routing PolicyADR-0076: Circuit Breaker per ProviderADR-0077: Provider Fallback RoutingADR-0078: Operator runtime topology & environment contract (4.P.0)ADR-0079: Operator session RS256 dual-verify and TOTP step-upIBEX Console service rename and boundaryADR-0081: Fail-closed Proxy runtime controlsD2 metadata-only trace read contractD2 publication, identity, query, and lifecycle contractG0 principal, agent binding, and idempotency contractEvidence relay daemon and session-fence authority (local, default-off)Read-only MCP operator gateway and gateway-side authorization boundary (local, default-off)
ADRs›Evidence relay daemon and session-fence authority (local, default-off)
ADRs

Evidence relay daemon and session-fence authority (local, default-off)

Proposed record for the first deployable evidence relay process: an explicitly gated, least-privilege daemon that authoritatively rejects tombstoned or stale-version evidence using a PostgreSQL session fence.

ADR-0085: Evidence relay daemon and session-fence authority (local, default-off)

  • Status: proposed for owner review; implemented-local in issue #1002. This ADR is not an acceptance record and GAP-011 remains open.
  • Date: 2026-10-19
  • Owners: Evidence/Operations + Platform/Data + Security/Privacy; Rick1330 acting across roles; no independent reviewer named.
  • Scope: a single deployable evidence relay process, its default-off feature gate, its least-privilege PostgreSQL identity, and its authoritative session fence check
  • Supersedes: none; narrows the accepted evidence-relay-sink.v1 shape (decision 21) from "library only" to "library plus a local, default-off daemon" without accepting a new contract.

Context

The accepted evidence-relay-sink.v1 contract shape (decision 21, 2026-10-10) fixes PostgreSQL as the evidence/outbox authority and ClickHouse as a non-production projection pilot. Until this change the repository had only the packages/evidenceoutbox library: NewRelay was invoked by tests, and the pilot was "default-disabled by the absence of a runtime daemon", not by a tested flag. The gap register recorded that the shared relay "still has no production daemon/feature gate or tenant/agent allowlist" (GAP-005/GAP-011 finding).

That left two unresolved problems for the security-sensitive relay path:

  1. There was no way to run the relay as a bounded process with a least-privilege identity, so the retry/poison/shutdown semantics could not be exercised outside unit tests.
  2. There was no authoritative, deployment-real fence check. The producer reads the PostgreSQL session lifecycle row, but nothing in a relay process verified the same row before projecting, which is the tombstone/version race GAP-005 and GAP-011 describe.

Decision

1. Add services/relay as a deployable process, default off

services/relay follows the existing Go service layout (cmd/relay entrypoint, internal/{bootstrap,config,fence}). The process is inert unless IBEX_RELAY_ENABLED=true (default false); a disabled daemon with valid configuration logs a documented reason and exits 0 (configuration, including the required explicit IBEX_ENV, is validated before the gate, so an invalid environment fails closed first). Enabling it requires both a PostgreSQL DSN and a ClickHouse DSN — startup fails closed (exit 1) rather than degrading to a partial runtime. When enabled, a missing or unreachable dependency is a fatal startup error, not a silent fallback.

The runtime loops claim → fence-check → deliver (ClickHouse pilot sink) → finalize, recovers stale in-flight claims, retries with bounded jittered backoff, and dead-letters permanent failures. Shutdown stops claiming, drains the in-flight batch, and stops reporting readiness before closing dependencies; claims abandoned by an interrupted batch are left to the loop's stale-recovery window on restart rather than released at shutdown (releasing them would also reset claims owned by other relay instances). Dependency spans are emitted through the shared OpenTelemetry setup.

2. Least-privilege PostgreSQL identity pinned per connection

The daemon connects as a login role and pins ibex_evidence_relay per connection via the PostgreSQL startup option options=-c role=ibex_evidence_relay (supported for both URL and keyword DSN forms; an existing options value is merged or rejected, never overwritten). The daemon asserts current_user = ibex_evidence_relay at startup and fails closed otherwise.

ibex_evidence_relay remains NOLOGIN, is not granted to ibex_app, and has EXECUTE only on the relay outbox helpers. It has no direct table access; the role's membership grant from a login role is a deployment prerequisite, not a repository grant.

3. Authoritative session fence via a SECURITY DEFINER function

Migration 000047_relay_fence_checker adds ibex_core.evidence_outbox_check_session_fence(p_org_id, p_resource_id, p_resource_ver), owned by ibex_service, STABLE SECURITY DEFINER, returning alive / tombstoned / stale_version / not_found. EXECUTE is granted to ibex_evidence_relay only (revoked from PUBLIC and ibex_app). Because a SECURITY DEFINER function runs with the owner's ACLs, the migration also adds a column-level GRANT SELECT (id, org_id, deleted_at, evidence_fence_version) ON ibex_core.sessions TO ibex_service; BYPASSRLS alone does not bypass table ACLs.

services/relay/internal/fence maps the outcome to the library's typed errors: alive → deliver, tombstoned / stale_version / not_found → ErrTombstonedResource (poison), and a non-session or malformed fence → ErrUnsupportedFenceResource (poison without retrying). Database or timeout errors stay retryable. The sink remains a mandatory TombstoneFencedDeliverer; legacy Deliver without a fence fails closed.

4. Observability and bounded labels

The daemon exposes /health, /ready, and /metrics using the shared health-check and metrics packages. Relay counters/histograms use bounded labels only: ibex_relay_batches_total{result}, ibex_relay_claimed_total, ibex_relay_delivered_total, ibex_relay_retried_total, ibex_relay_dead_letter_total, ibex_relay_recovered_total, ibex_relay_batch_duration_seconds, ibex_relay_delivery_duration_seconds{result}, ibex_relay_fence_check_duration_seconds{outcome}, and ibex_process_up. Logs carry IDs, counts, and reasons, never payload content.

What this does not authorize

This record is deliberately narrow. It does not:

  • accept a new contract shape or promote any G1–G7 registry entry;
  • authorize a production or hosted ClickHouse sink, HA topology, backup/restore drill, or operator runbook evidence (GAP-011 stays open);
  • add a tenant/agent allowlist, request-auth layer, or admin/operator endpoint to the relay;
  • make chat acknowledgement response-bound, or make fence-check, outbox write, and projection a single atomic transaction (GAP-005 stays open);
  • enable the daemon by default in any profile.

The ClickHouse sink adapter remains a non-production pilot gated at G4. Production enablement requires independent review, an accepted deployment profile, and the recovery/backup evidence GAP-011 tracks.

Evidence

  • infra/migrations/postgres/000047_relay_fence_checker.{up,down}.sql and migrate_relay_fence_checker_integration_test.go (definition/grants, fence outcomes, role isolation, clean rollback) pass against local PostgreSQL.
  • services/relay/internal/fence/session_fence_integration_test.go and services/relay/internal/bootstrap/relay_e2e_integration_test.go exercise the pinned role, cross-tenant isolation, PersistRun → ProcessBatch delivery, tombstone → poison, and in-flight recovery on real PostgreSQL.
  • Unit suites cover config bounds, fence mapping, loop/backoff, health/readiness, and shutdown ordering. Relay package coverage is recorded in the status ledger.
  • All three golangci-lint configs and go test -race -tags=integration -p 1 ./services/relay/... pass locally.

These are local, disposable-PostgreSQL results. They are not hosted/HA, restore, or G4 sink evidence.

Alternatives considered

  • Keep the library only. Rejected: it leaves the fence race and retry/shutdown semantics unexercised and keeps the "default-disabled by absence of a daemon" statement true, which blocks any honest enforcement claim.
  • SET ROLE per transaction instead of a pinned startup option. Rejected: SET ROLE on a pooled connection can leak role state across borrowers; pinning at connect time keeps every pool connection inside the relay role, including after reconnects. ALTER ROLE <login> SET role = ... remains a documented deployment alternative.
  • Read the session lifecycle row directly as the relay role. Rejected: it would grant the relay direct table access and duplicate RLS/lifecycle logic; a SECURITY DEFINER helper keeps the ACL surface minimal and centralizes the version semantics.

Review and expiry

Owner review is required before any status change or production enablement. Review/expiry is 2026-11-10, or earlier on a material authority, profile, or fence change.

Was this page helpful?

Edit on GitHub

Last updated on

PreviousG0 principal, agent binding, and idempotency contractNextRead-only MCP operator gateway and gateway-side authorization boundary (local, default-off)

On this page

  • Context
  • Decision
  • 1. Add services/relay as a deployable process, default off
  • 2. Least-privilege PostgreSQL identity pinned per connection
  • 3. Authoritative session fence via a SECURITY DEFINER function
  • 4. Observability and bounded labels
  • What this does not authorize
  • Evidence
  • Alternatives considered
  • Review and expiry
0%