A taint-tracking tool gate for AI agents whose policy is proven unbreakable at design time. polyman is the man-in-the-middle of the PolySec family: it sits between an agent and its tools, tracks what each session has touched, and denies any tool call that would let untrusted content, sensitive data, and outbound egress coexist — the lethal trifecta that turns a prompt injection into a data exfiltration.
Every agent framework now ships a "tool approval" hook. What polyman adds is the
thing none of them have: the policy is model-checked airtight before it ships
(FULLY MEDIATED ✓), and the runtime enforces that exact proven policy — the
same state-machine engine that proved it also steps it live (asserted by a
byte-for-byte parity test). You are not trusting a hand-written if; you are
enforcing a theorem.
Scope, stated up front. polyman is a reference monitor, not a sandbox. It stops a misguided agent (injection steering legitimate tools). It does not, by itself, contain a compromised runtime that opens its own socket around the gate — that needs confinement (a sandbox), the Phase-B story. And it reasons over which capability a session holds, not payload contents. See Limitations.
npm i @cognitive-fab/polyman # Node ≥ 24
The lethal-trifecta policy ships pre-proven, with a starter label map for common tool names. No state machine to author, no proof to run — wrap your existing call:
import { generateText } from 'ai';
import { createPolyman } from '@cognitive-fab/polyman';
import { wrapVercel } from '@cognitive-fab/polyman/vercel';
const pm = await createPolyman(); // ← shipped, proven policy
const res = await generateText({
model, tools,
...wrapVercel(pm, { session: conversationId }), // ← in-line default-deny gate
});Now a session that has fetched untrusted content and read a secret cannot
send email: the gmail.send tool call is denied (lethal-trifecta) before it
runs — no matter what a prompt injection told the model to do. wrapVercel
maps polyman's gate onto the SDK's native toolApproval (deny before execution)
and onStepEnd (record observed taint). Pass tools to wrapVercel to refuse
provider-executed tools it can't mediate; pass onDeny: 'user-approval' to
escalate to a human instead of hard-denying.
No Vercel? Drive the same gate directly (this is all wrapVercel does):
const pm = await createPolyman();
await pm.mediate(sessionId, { tool: 'gmail.send', args }, () => runTool()); // throws if blocked
// …or the two phases explicitly:
const d = await pm.gate(sessionId, { tool, args }); // before the call
if (!d.ok) return toolError(d.reason); // e.g. 'lethal-trifecta'
await pm.settle(sessionId, { tool, args }, await runTool()); // afterEvery decision lands in a {pre, action, data, post, decision} journal
(pm.journal) — the same stream the PolySec guard and audit consume.
- Label — each tool maps to an abstract effect class (untrusted-source /
sensitive-source / egress-sink / benign) in
security.json. The starter map covers common names; extend it for yours. An unlabeled tool is denied (default-deny). - Track — a tiny per-session state machine accumulates the effects. Session state is isolated and updated atomically (concurrent tool calls on one session are serialized, so a race can't drop a taint flag).
- Gate — before a call runs, polyman steps the machine over the tool's
declared effect; if the result would enter the forbidden region, it
rejects with a contract-anchored reason, journaled. - Prove — the shipped machine is
FULLY MEDIATEDunder PolySecharden(from-init clean ∧ no havoc counterexample ∧ no gate-deletion bypass). The runtime engine steps it byte-identically to the checker (golden-parity test).
No model or network call is on the decision path — $0, deterministic, and
the test suite asserts it (mock model, no API key).
Point polyman at your own proven machine + manifest:
const pm = await createPolyman({
machine: './security/policy.next.cjs', // your SAM v2 machine
security: './security/security.json', // regions, gates, tools map
});Author and prove a custom machine with PolySec (polysec harden). polyman will
serve only a policy the proof has passed once polyman verify lands (Phase A2).
- Not a sandbox. The guarantee holds only if the agent has no tool/network path around the gate. Providing that confinement is out of scope here (the Phase-B OpenHands sidecar borrows a real one).
- The labels are a trust obligation. polyman proves the policy over effect classes; it does not prove your tool → class labels are right. A mislabeled or unlabeled tool is a hole the proof can't see (unlabeled ones are denied; the starter map is a convenience, not a guarantee — review it).
- Single process today. Per-session serialization is in-memory; a multi-process deployment sharing one store needs an atomic/locking store.
- Control-flow layer only. It mediates capability combinations, not covert channels inside an allowed call.
This release: the deployment-agnostic kernel + zero-config default + the Vercel
AI SDK adapter (@cognitive-fab/polyman/vercel), self-contained, built to
ESM+CJS+.d.ts, 20 tests green (no API key). Next: the runtime guard +
polyman verify coherence gate, then the OpenHands (sandboxed) sidecar. See
docs/polyman-plan.md.
Apache-2.0 · © Cognitive Fab LLC