Integration docs

How the Agent Visibility Inspector plugin works, and how to run it in your own stack.

The plugin

Integration overview

The plugin delivers PolyphonyAI's one differentiator, control over what each agent sees, as a drop-in you embed in your own agent stack. You keep your framework, your models, and your infrastructure. The plugin adds the layer that decides what each agent is allowed to see and enforces it on the path, before anything reaches a model or a memory store.

It has two parts:

  • The Context Inspector assembles the exact context an agent is about to receive, shows live token counts, resolves each item to a visibility state, and lets a human strike out anything before it is sent. Excluded items are genuinely dropped from what goes to the model, not just hidden in the view.
  • The Visibility Gate checks every model call and every memory write against that agent's visibility before it happens. What an agent is not cleared to see is never assembled into what it receives, and the memory-write gateway blocks any write it is not cleared to make.

The client is AG-UI protocol native. A small framework-agnostic core consumes the raw event stream, and the same core backs three hosts: a zero-build vanilla page, a React component, and a CopilotKit adapter. CopilotKit is one example integration, a thin adapter on top of the core, not the foundation.

What this is, honestly. The plugin is the reusable, embeddable, enforce-by-construction packaging of a pattern the PolyphonyAI app already proved. It is not a new access-control mechanism, and it does not claim to be. Its value is that per-agent visibility control ships as something you can drop into an existing stack.

Architecture

How enforcement works

Enforcement follows a policy-decision / policy-enforcement split. A policy engine decides, and gateways on the request path enforce. The gateways are the enforced path for the agents you route through them, and both fail closed.

The decision point

Decisions are made by an Open Policy Agent engine running a Rego policy that is default-deny. A label with no clearance resolves to excluded. Every decision returns the resolved visibility (full, blind, redacted, or excluded), a plain-language reason, and the basis (which rule decided it).

The two gateways

  • Model-call gateway. Before a request reaches the model, it strips items the agent cannot see, withholds the author on blind items, and masks the sensitive spans on redacted items. It reports what it did on the response with X-Stripped, X-Blind, and X-Redacted headers. If the decision point is unreachable, it returns 403 and forwards nothing.
  • Memory-write gateway. Every write is checked before it reaches the store. An excluded write is blocked with 403. A blind write is stored with the author overwritten to anonymous. A full write keeps the author. So an agent cannot store, and later recall, something it was not cleared to see.

Both gateways take the agent's identity from a bearer token, never from the request body, so an agent cannot claim to be another agent.

Tamper-evident audit and an isolated store

  • Every authorization decision is shipped to an append-only audit log where each entry is hash-chained and signed with an HMAC key held separately from the log file. A GET /verify re-reads the file and reports the exact entry where the chain breaks if anything was altered.
  • The protected memory store answers only over a Unix domain socket. It has no TCP port and no network surface. The gateway is the only path that can reach it.

The path

agent  ──POST /complete──▶  model-call gateway (:8630)
                                  │  asks
                                  ▼
                             OPA policy engine (:8181)  default-deny
                                  │  full / blind / redacted / excluded
                                  ▼
                   strips + masks, then forwards ──▶  your model endpoint

writer ──POST /write────▶  memory-write gateway (:8610)
                                  │  asks OPA
                                  ▼
                     allowed ──▶ memory store (Unix socket)
                     excluded ─▶ 403, never stored

every decision ──▶ audit log (:8640)  hash-chain + HMAC + /verify
                        └─ optional push to your SIEM
Setup

Install and run

Prerequisites

  • An OpenAI-compatible inference endpoint. Out of the box this is a local Ollama running llama3.1:8b. Point it at any endpoint you like with the PROVIDER_BASE_URL, PROVIDER_API_KEY, and PROVIDER_MODEL environment variables, no code change.
  • The OPA binary on your PATH or downloaded to ./bin/opa. On macOS: brew install opa. On Linux: curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static && chmod 755 opa.

Start everything

cp backend/.env.example backend/.env
./run.sh        # or: make dev

This starts, in order: the audit collector, the OPA policy engine, the memory store, the memory-write gateway, the model-call gateway, the backend, and a zero-dependency static host, then opens the inspector.

Ports and sockets

All overridable by environment variable:

  • Policy engine: 8181
  • Backend: 8600 · Static host: 8700
  • Memory-write gateway: 8610 · Model-call gateway: 8630
  • Audit collector: 8640
  • Memory store: a Unix socket (pep/memory-store.sock, mode 600), no TCP port

Live policy reloads. The policy engine runs with --watch on the policy folder, so clearance edits are picked up immediately, with no restart. Its management API is locked behind a bearer token.

Two other hosts are included: a React host and a CopilotKit example, each runnable on its own port.

Integrate

Integrating your agents

Enforcement is a set of HTTP services on the path, not modules you import. Any agent, in any language, enforces visibility by calling the gateways. Each gateway publishes an OpenAPI 3.1 description at /openapi.json.

Route model calls through the gateway

POST http://localhost:8630/complete
Authorization: Bearer <agent-token>

{ "agent": "reviewer",
  "items": [ { "id": "m1", "role": "user", "content": "...", "label": "shared" } ],
  "temperature": 0.2 }

Read back the streamed completion, and read X-Stripped to see which items policy removed.

Route memory writes through the gateway

POST http://localhost:8610/write
Authorization: Bearer <agent-token>

{ "agent": "reviewer", "label": "proposal", "content": "...", "author": "author" }
  → 200 { "allowed": true,  "stored": true,  "decision": { ... } }
  → 403 { "allowed": false, "decision": { ... } }

You can also query the policy engine directly at POST http://localhost:8181/v1/data/visibility/decision using the standard OPA REST API.

Clients and languages

  • Python. The backend and the gateways are Python (FastAPI).
  • Node.js / JavaScript. A non-Python example client (built-in fetch, no Python) exercises the gateways end to end: a blocked write, an allowed write, and a model call whose scratchpad item is stripped. This proves the gateways are language-neutral to call.
  • Browser. The framework-agnostic client core is plain JavaScript, consumed by the vanilla, React, and CopilotKit hosts.

Honest scope. Language-neutral access to the gateways is proven. The gateways themselves are Python and run as a sidecar, so there is no separate non-Python implementation to install. Your agents must send context as labeled items, which is a labeling convention you adopt.

Render the inspector

Mount the React inspector component (with its stylesheet), or use the zero-build vanilla host. It consumes the backend's AG-UI stream (a simple POST /agent body, or the standard AG-UI POST /agui input), which emits the inspector snapshot plus policy_stripped, policy_blind, and policy_redacted events as decisions happen.

Policy

The policy file

Clearances live in one file, policy/clearances.json, the single source of truth. The shape is agent to label to level:

{ "clearances": {
    "author":   { "shared": "full", "proposal": "full", "scratchpad": "full" },
    "reviewer": { "shared": "full", "proposal": "blind" }
} }

A label that is absent for an agent means no grant, which resolves to excluded. That is the default-deny property: access is something you grant on purpose, never something you forget to remove.

The four states

  • full: the agent sees the content and the author.
  • blind: the agent sees the content, with the author withheld.
  • redacted: the agent sees the content with sensitive spans masked and the author withheld. Masking uses deterministic, authored patterns, with no AI scoring. You author the patterns; anything you don't write a pattern for is not masked.
  • excluded: the item is never assembled into the agent's context.

Three states in the app, four here. The app model is full, blind, and excluded. The plugin adds redacted as a fourth state for masking sensitive spans while still showing the rest.

Beyond a flat list

The policy is evaluated live from inputs you pass with each decision, so these dimensions never mutate stored data:

  • Roles. Group clearances into roles and assign roles to agents. An agent's effective clearance on a label is the most permissive its roles grant.
  • Permission ceilings. A central cap per label, applied last, so it limits even a staged reveal or a break-glass elevation.
  • Label hierarchy. Labels can inherit: a grant comes from the nearest cleared ancestor, and the most restrictive ancestor ceiling still applies.
  • Staged reveal. Access can open only after a named event has occurred.
  • Break-glass. An audited emergency elevation. A reason is mandatory, it is logged loudly, and it is still bounded by the ceiling.
Administer

Authoring and applying clearances

You can edit policy/clearances.json directly and let the policy engine reload it, or apply a change over the API:

POST /policy/clearance
Authorization: Bearer <PAP_ADMIN_TOKEN>

{ "agent": "reviewer", "label": "proposal", "level": "blind", "reason": "review phase" }
  • level is one of full, blind, redacted, excluded. Setting excluded removes the grant.
  • The write is admin-authenticated. A missing token returns 401; a wrong token returns 403 and raises an identity_rejected alert.
  • The change rewrites clearances.json (so it stays version-controllable) and updates the running policy. Because the engine watches the file, it takes effect live with no restart.
  • Every change emits a policy_change entry to the audit log.

Preview before you apply. Applying a clearance is separate from asking "what would this change do?". The policy sandbox answers that without touching the live policy.

Rehearse

The policy sandbox

Before you change a clearance, you can rehearse it. The sandbox is read-only and preview-only, so nothing you try there affects the live policy. It can:

  • Explain any decision in plain language, and show which rule decided it.
  • Run a what-if against a proposed change, and a policy diff of who gained or lost access.
  • Simulate the impact of a change over a corpus of recorded traffic.
  • Preview a rollback to an earlier policy version, with a diff.
  • Time-travel: re-decide the recorded corpus as of a prior policy snapshot.
  • Trace influence lineage across agents.

See the policy sandbox in depth →

Observe

Audit, SIEM export, and alerting

The audit log

Every decision is recorded to a tamper-evident, append-only log (hash-chained and HMAC-signed). A readable feed is available at GET /decisions, and GET /verify checks the chain and reports the exact point of any break.

SIEM export

Export is vendor-neutral, over plain HTTP and NDJSON, with no OpenTelemetry dependency. Two mechanisms:

  • Push. Set SIEM_URL and each decision is posted as newline-delimited JSON as it lands. If your SIEM is briefly unavailable, enforcement keeps running and delivery resumes when it returns.
  • Pull. GET /export serves the log as application/x-ndjson, oldest first, each line carrying a seq. Pass ?since=<last seq> as a cursor (plus an optional limit) for incremental, gap-free scraping.

Alerting and rate limiting

  • Alerts fire on fail_closed, audit_tamper, rate_limited, identity_rejected, and policy_change, to a JSONL file, to stderr, and to an optional webhook (ALERT_WEBHOOK).
  • Rate limiting is a per-agent sliding window; requests over budget get 429. Tune it with RATE_LIMIT_PER_WINDOW and RATE_LIMIT_WINDOW_S.

Compliance mapping

The repo includes an engineering control-mapping of each enforcement mechanism to the documented control it supports, with every row pointing at the code that enforces it, so a reviewer can start from a requirement and trace it straight to the evidence.

Deploy

Self-hosting

The plugin runs entirely in your environment. Inference goes to a self-hostable, OpenAI-compatible endpoint (a local Ollama by default, or any endpoint you configure), so it has no dependency on a hosted vendor and your data never has to leave the host. The protected store has no network surface at all.

Before production

Production hardening

The enforcement core is built and proven: the four-state model, both fail-closed gateways, the default-deny policy engine, the tamper-evident audit log (proven by tampering), Unix-socket store isolation, per-agent token identity, rate limiting, alerting, SIEM export, clearance authoring, and the full policy sandbox.

Because it runs on your own infrastructure, the plugin slots into the controls you already operate. For a production deployment, wire it into your environment:

  • Transport security. Run the gateways behind your mutual TLS, so traffic between agent, gateway, and policy engine is encrypted in transit.
  • Encryption at rest. Place the store and audit log on your encrypted volumes.
  • Key and token rotation. Rotate the signing key and bearer tokens through your own secrets management, and issue real tokens in place of the dev- placeholders.
  • Identity and availability. Front the admin console with your identity provider, run redundant policy-engine and gateway instances behind your load balancer, and back up the store with your standard procedures.
  • External anchoring. Anchor the tamper-evident audit log to your external timestamping for end-to-end assurance.