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.
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, andX-Redactedheaders. If the decision point is unreachable, it returns403and forwards nothing. - Memory-write gateway. Every write is checked before it reaches the store. An
excludedwrite is blocked with403. Ablindwrite is stored with the author overwritten toanonymous. Afullwrite 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 /verifyre-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
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 thePROVIDER_BASE_URL,PROVIDER_API_KEY, andPROVIDER_MODELenvironment 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, mode600), 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.
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.
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.
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" }
levelis one offull,blind,redacted,excluded. Settingexcludedremoves the grant.- The write is admin-authenticated. A missing token returns
401; a wrong token returns403and raises anidentity_rejectedalert. - 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_changeentry 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.
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.
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_URLand 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 /exportserves the log asapplication/x-ndjson, oldest first, each line carrying aseq. Pass?since=<last seq>as a cursor (plus an optionallimit) for incremental, gap-free scraping.
Alerting and rate limiting
- Alerts fire on
fail_closed,audit_tamper,rate_limited,identity_rejected, andpolicy_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 withRATE_LIMIT_PER_WINDOWandRATE_LIMIT_WINDOW_S.
Compliance mapping
The repo includes an engineering control-mapping of each mechanism to the SOC 2 Trust Services Criteria and ISO/IEC 27001:2022 Annex A, with every row pointing at the code that enforces it. It is a mapping to aid an audit, not a certification or attestation.
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.
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.