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›Read-only MCP operator gateway and gateway-side authorization boundary (local, default-off)
ADRs

Read-only MCP operator gateway and gateway-side authorization boundary (local, default-off)

Proposed record for a gated, read-only MCP operator surface that enforces principal binding, typed scopes, tenant scope, limits, and output redaction at the gateway, using the existing permission bitmap as the authority while PolicySnapshot evaluation remains deferred.

ADR-0086: Read-only MCP operator gateway and gateway-side authorization boundary (local, default-off)

  • Status: proposed for owner review; implemented-local in issue #1013. This ADR is not an acceptance record and GAP-009 remains open.
  • Date: 2026-10-19
  • Owners: Operator/Platform + Security/Privacy; Rick1330 acting across roles; no independent reviewer named.
  • Scope: a single default-off, read-only MCP operator gateway, the shared enforcement core it is built on, its typed scope model mapped onto the existing permission bitmap, its tenant-scoped reads, and its fail-closed taxonomy
  • Supersedes: none; narrows the mcp-gateway-policy.v1 registry entry from not_implemented to a local, default-off implementation without accepting the contract.
  • Revision note (2026-10-20): The enforcement order in section 6 was corrected from resolve tool → policy → validation → rate limit to org rate limit → resolve tool → policy → validation. Implementation review found that the earlier order let a malformed or unauthorized caller reach expensive lookup/authorization work without consuming the org's rate-limit budget. Rate limiting first closes that bypass. The tradeoff is deliberate: unknown or under-scoped calls now consume budget, and the audit event retains the requested tool name so rate-limit rejections remain attributable. This correction does not change the ADR's status, scope, authority, or acceptance posture.

Context

The mcp-gateway-policy.v1 registry entry was a not_implemented placeholder, and the operator/MCP boundary had no gateway. The only MCP surface was services/mcp-memory (ADR-0050), which serves memory tools and therefore conflates the memory-serving boundary with the operator surface the gap register keeps separate. mcp-runtime-tools.v1 did not exist in the repository at all.

That left three unresolved problems for an operator-facing MCP boundary:

  1. There was no gateway-side enforcement of who a tool call is authorized for. Principal binding, typed scopes, tenant scope, per-call limits, and output redaction were either absent or would have to be re-implemented ad hoc per tool.
  2. There was no versioned, inspectable description of the tools a gateway exposes, their declared capabilities, or their per-call bounds, so a tool surface could not be reviewed or diffed.
  3. There was no fail-closed taxonomy: unknown tools, missing scopes, unavailable policy, malformed arguments, and backend outages had no defined, stable, deny-by-default behavior.

Decision

1. Add services/mcp-operator as a deployable process, default off

services/mcp-operator follows the existing Python service layout (app/ package, FastMCP over FastAPI, Dockerfile). The process exposes no operator surface unless IBEX_MCP_OPERATOR_ENABLED=true (default false); a disabled daemon serves no MCP mount and the service advertises no tools. Enabling it in a protected profile (staging/production) requires Redis (rate-limit authority) and a public HTTPS resource URL, and startup fails closed otherwise. Transport is Streamable HTTP only; stdio is not implemented by this service.

2. Put the enforcement core in packages/mcp_gateway

The reusable enforcement core — typed scope model, versioned tool registry with a canonical descriptor hash, the ordered enforcement pipeline, output redaction, and the typed fail-closed error taxonomy — lives in packages/mcp_gateway, following the packages/authclient convention. services/mcp-operator is a thin FastMCP daemon over it. The packages/README.md "no new top-level package until a second consumer" guidance yields here because the mcp-gateway-policy.v1 contract is itself the reason for the seam: authorization logic must be unit-testable outside the MCP transport, and the intended second consumer is the eventual convergence of services/mcp-memory onto the same core. This is recorded rather than assumed.

3. Typed scopes are aliases over the existing permission bitmap

The gateway does not invent an authorization vocabulary. Typed scopes are named aliases over the repository's permission bits (packages/permissions), and the enforcement decision is has_permission(principal.permissions, required_bit) computed from the AuthService-validated token only:

  • operator.read → OPERATOR_METADATA_READ (1<<19)
  • runs.read → TRACE_READ (1<<24)
  • evidence.read → OPERATOR_REDACTED_READ (1<<38)
  • budget.read → BILLING_READ (1<<33)

The caller can never supply its own org_id or agent identity; tool input schemas (additionalProperties: false) reject unknown arguments, and tenant scope is bound from the verified token. Tool annotations are metadata only and never authorize.

4. PolicySnapshot evaluation is deferred, and the deferral is visible

No Python PolicySnapshot verifier and no resource/action Check API exist, so the gateway cannot delegate authorization to a policy snapshot today. The authorization authority is therefore the permission bitmap. This is recorded explicitly in mcp-gateway-policy.v1 and the gap register so the authority limit is visible rather than silent, and the pipeline has a single enforcement point (PolicyEvaluator) so a future Python PolicySnapshot verifier can be attached without re-architecting the gateway. A configured-but-unavailable policy evaluator must deny; it must never allow.

5. Read-only tools only, over tenant-scoped reads

The initial tool set is read-only: session.get, run.get, evidence.query, and budget.get. These read ibex_core.sessions, ibex_core.evidence_runs, and ibex_billing.budget_periods under a transaction bound with SET LOCAL ROLE ibex_app and app.current_org_id, so RLS is enforced in addition to explicit org_id predicates. evidence.query serves only the OPERATOR_REDACTED_READ tier (sanitized metadata, never raw payloads). Mutating tools are forbidden because no accepted contract for them exists.

6. Fail-closed taxonomy and non-blocking audit

The pipeline is ordered and deny-by-default: resolve verified principal → org rate limit (rate_limited/rate_limit_unavailable) → resolve tool (tool_not_found) → bitmap policy (permission_denied; policy_unavailable when a configured evaluator is unavailable) → input schema validation (invalid_params) → tenant-scoped read (session opened only after all checks pass) → output redaction → metadata-only audit. Rate limiting runs before tool lookup and authorization so malformed or unauthorized calls cannot bypass org budget enforcement; the tradeoff is that unknown or under-scoped calls consume budget. A rate-limit rejection records the requested tool name in the audit event. A backend outage is backend_unavailable (503). Per-call timeouts and row caps bound every read. Audit is asynchronous and never blocks a read (AGENTS.md §6.1): operator reads emit a metadata-only ibex.mcp_tool_calls row (principal, tool, latency, outcome) with descriptor hash/scope/decision carried in structured logs and are not wrapped in the durable-before-ack evidence envelope, which applies to mutating evidence runs.

What this does not authorize

This record is deliberately narrow. It does not:

  • accept mcp-gateway-policy.v1 or any G1–G7 registry entry, or promote any row to accepted;
  • authorize any mutating MCP tool;
  • authorize a Python PolicySnapshot verifier/issuer or any resource/action policy authority (deferred; the authority hole stays visible);
  • authorize admin, service-account, or RLS-bypass queries behind operator tools;
  • make operator reads durable-before-ack evidence, or close GAP-009;
  • enable the gateway by default in any profile.

Evidence

  • packages/mcp_gateway unit suites cover scope evaluation, unknown tools, schema rejection, principal binding that ignores caller-supplied identity, fail-closed policy/backend behavior, row caps, redaction, and descriptor-hash stability.
  • services/mcp-operator unit and integration suites cover per-tool end-to-end reads, cross-tenant isolation (org A cannot read org B, no existence leak), policy-unavailable denial, and gate-disabled absence under ibex_app RLS on real PostgreSQL.
  • The mcp-gateway-policy.v1 registry entry is updated to record the local, read-only, default-off implementation, the fail-closed taxonomy, and the deferred PolicySnapshot path; it is not marked accepted.
  • 18-gap-register.md GAP-009 evidence is refreshed and remains open; no acceptance is declared.

These are local results. They are not hosted/HA, restore, or production-enablement evidence.

Alternatives considered

  • Extend services/mcp-memory with operator tools. Rejected: it conflates the memory-serving and operator boundaries the gap register keeps separate, and entangles the memory tool contract with the operator surface.
  • Build a Go MCP service. Rejected: it diverges from the repository's only MCP implementation (Python/FastMCP), re-solves MCP transport for no benefit, and would not be testable against the existing MCP tooling.
  • Keep the enforcement logic inside the service. Rejected: authorization logic must be unit-testable outside the MCP transport, and the contract justifies the shared core.
  • Wire a Python PolicySnapshot authority now. Rejected: no Python verifier or Check API exists, so it would be a fake seam. The enforcement point is built so it can be attached later.

Review and expiry

Owner review is required before any status change, contract acceptance, or production enablement. Review/expiry is 2026-11-10, or earlier on a material authority, scope, tool-surface, or policy change.

Was this page helpful?

Edit on GitHub

Last updated on

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

On this page

  • Context
  • Decision
  • 1. Add services/mcp-operator as a deployable process, default off
  • 2. Put the enforcement core in packages/mcp_gateway
  • 3. Typed scopes are aliases over the existing permission bitmap
  • 4. PolicySnapshot evaluation is deferred, and the deferral is visible
  • 5. Read-only tools only, over tenant-scoped reads
  • 6. Fail-closed taxonomy and non-blocking audit
  • What this does not authorize
  • Evidence
  • Alternatives considered
  • Review and expiry
0%