@visiq/harness: the visiq() function and its VisiqOptions.
The SDK ships for TypeScript (Node 20+,
npm install @visiq/harness) and Python (pip install visiq); both wrap the same governance API. Any language without an SDK can integrate directly over the REST API — see the action governance and retrieval governance API references.The SDK is pre-1.0 (0.x). Breaking changes may ship in any minor release until v1.0 GA — pin an exact version and read the changelog before upgrading. See SDK versioning & compatibility for the full posture and the invariants we hold even now.
visiq(target, options?)
Inject governance into an agentic framework instance. Wraps the target’s run and tool dispatch methods in place and returns target.
Behavior
- Detects the framework by inspecting
target - Installs action governance — wraps every tool’s own dispatch methods (
invoke/call/_callfor LangChain,executefor the Vercel AI SDK / Mastra / VoltAgent,invokefor the OpenAI Agents SDK,callfor LlamaIndex.TS) so a deny actually prevents execution. Framework callbacks are observational only — a throwing callback is logged and execution continues — so enforcement happens at the tool method itself. - Installs retrieval governance — finds retrievers and retriever-backed tools and wraps them so retrieved documents pass through policy before the model sees them
- Captures telemetry — each top-level run gets a fresh session id, and LLM prompts, responses, and agent reasoning are attached to decisions so the dashboard shows the full run context around each one
- Returns the same
targetreference — your code is unchanged
visiq() exactly once. Tool and retriever wrapping is idempotent (guarded by internal symbol markers), but a LangChain executor’s invoke()/stream() are re-patched on each call.
Framework detection
visiq() detects what you pass and installs the right hooks:
For the Vercel AI SDK, Mastra, VoltAgent, the OpenAI Agents SDK, and LlamaIndex.TS, RAG is a tool whose result is document-shaped — retrieval governance filters the documents that tool returns (a bare
Document[], or a document-shaped array nested inside the result object). Anything unrecognized throws:
Retriever detection
For LangChain targets, the harness checks each tool intarget.tools against four patterns:
A retriever-backed tool whose name also carries an unambiguous mutation verb (e.g.
retrieve_and_archive) is treated as a hybrid: one combined decision governs both the call gate and the returned data, so the call is evaluated exactly once.
Tools that capture their retriever in a closure (e.g. LangChain’s
createRetrieverTool, which returns a joined string) cannot be governed per document — the harness detects them by name, still masks their output via value-shape and pattern rules, and logs a one-time console.warn explaining that per-document metadata rules cannot apply. For full per-document governance, use a tool that exposes its retriever as a .retriever property or returns a Document[].VisiqOptions
agentId is optional. Resolution order: explicit option → VISIQ_AGENT_ID → the nearest package.json name (npm scope stripped) → hostname → "agent". The backend auto-provisions the first id it sees — in monitor mode — so setting just the API key and endpoint works end-to-end. Set it explicitly when you want a stable, rule-friendly name.
Agent modes
Every agent runs in one of three modes. The mode is server-authoritative — resolved on the backend and shipped to the SDK inside the rule bundle, where running agents pick it up within seconds.
Inheritance. An agent’s mode can inherit an org-wide default or be overridden per agent. Leave an agent’s mode unset and it resolves to the org default
allow_settings.default_agent_mode (which itself defaults to monitor); set it explicitly (enforce / monitor / off) to override for that one agent. Changing the org default moves every inheriting agent together.
Per-operation overrides. An agent can also pin a different mode per operation via mode_by_operation — for example enforce actions while keeping retrievals in monitor. Any operation left unset inherits the agent’s resolved mode.
Local evaluation
Decisions resolve in-process against a locally cached rule bundle — not via a per-tool-call network round-trip:- The harness fetches
GET /rules/bundlein the background when you wrap, then refreshes every 5 seconds withIf-None-Match(a304keeps the cached bundle). - Every evaluation runs locally against the cached bundle. The only decision-path network traffic is human approval: registering an
approval_requireddecision, then polling for its resolution. - If the backend becomes unreachable after the bundle has loaded, governance keeps working from the cached rules; refresh resumes when connectivity returns.
- No bundle yet — monitor-until-confirmed. The cold-start fail-safe is a mode envelope, not fail-open. An agent whose mode the backend has never confirmed (cold start, or no
apiKey/endpoint) runsmonitor— it observes and records but never blocks. An agent that was confirmed inenforceand then loses its cached bundle stays fail-closed to deny (G001): every action is denied and every document suppressed until the bundle returns. A backend-confirmedmonitor/offagent stays permissive. - Uncovered actions — an action no rule matches resolves via your no-match default. Out of the box uncovered actions proceed — “no default disruption”, even in
enforcemode — and you can switch the default to Deny or Require approval under Organization Settings → Security → “When no rule matches”. A per-agent fail-safe override (the agent’sno_coveragesetting: “Fail open — permit” / “Fail closed — deny” on the agent page) takes precedence over the org-level default for that agent; “Fail closed” also overrides autopilot. Uncovered retrieval keeps its default-deny data-protection floor: an unmatched document is denied. - Masking failure — fail closed, never fail open: if argument masking itself fails the call is blocked, and if document redaction fails the document is excluded rather than returned raw.
Governance outcomes
Actions and retrieval each resolve to one of four decisions:Denied tool calls
Nothing is thrown. On a deny, the tool’s underlying function is never invoked; the harness returns the denial as the tool’s output, so the agent reads it and keeps reasoning — yourinvoke() caller receives a normal completion:
<rule-code> is the matched rule’s human-readable code, or a synthetic policy code (e.g. D-WRITE-DENY) for uncovered actions. Don’t wrap invoke() in try/catch to detect blocks — inspect the tool output, or the decision stream in the dashboard.
Human approval (approval_required)
The call pauses. The harness registers the decision with the backend and polls GET /v1/allow/decisions/:id every 2 seconds until a human resolves it or hitlTimeoutMs (default 120s) elapses. Approved → the tool runs. Rejected, expired, timed out, or persistent polling failure → the call is blocked in the same shape as a denial (fail-closed). A rule configured with the mask fallback proceeds with masked arguments instead of blocking when approval never arrives. See Human-in-the-loop.
Denied documents
Denied documents are silently excluded from results and redacted documents are masked in place. No error is thrown — suppression is a normal policy outcome, not an exception. If every document is denied, the retriever returns an empty array.Audit receipts
Every decision — action, retrieval, and human-approval — is recorded server-side with no SDK import or extra call. Each recorded decision additionally receives an asynchronously signed (Ed25519) receipt; verify any receipt from the dashboard or viaGET /record/envelopes/:id/verify. See Audit Trail.