> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Reference

> API reference for @visiq/harness — the visiq() function, VisiqOptions, framework detection, agent modes, local evaluation, and governance outcomes.

This page documents the core surface of `@visiq/harness`: the `visiq()` function and its `VisiqOptions`.

```typescript theme={null}
import { visiq, type VisiqOptions } from "@visiq/harness";
```

<Note>
  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](/rules/action/api-reference) and [retrieval governance](/rules/retrieval/api-reference) API references.
</Note>

<Note>
  **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](/versioning) for the full posture and the invariants we hold even now.
</Note>

***

## `visiq(target, options?)`

Inject governance into an agentic framework instance. Wraps the target's run and tool dispatch methods in place and returns `target`.

```typescript theme={null}
function visiq<T extends object>(target: T, options?: VisiqOptions): T;
```

### Behavior

1. **Detects the framework** by inspecting `target`
2. **Installs action governance** — wraps every tool's own dispatch methods (`invoke`/`call`/`_call` for LangChain, `execute` for the Vercel AI SDK / Mastra / VoltAgent, `invoke` for the OpenAI Agents SDK, `call` for 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.
3. **Installs retrieval governance** — finds retrievers and retriever-backed tools and wraps them so retrieved documents pass through policy before the model sees them
4. **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
5. **Returns the same `target` reference** — your code is unchanged

Every decision is recorded by the backend automatically — no extra SDK calls (see [Audit receipts](#audit-receipts)).

Pass each executor to `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:

| Target                                                       | Detection                                                                     |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| LangChain `AgentExecutor`                                    | `target.invoke` + `target.agent` + `target.tools` array                       |
| LangGraph `CompiledGraph`                                    | `target.invoke` + `target.nodes` + `target.edges`                             |
| Vercel AI SDK agent (`ToolLoopAgent` / `Experimental_Agent`) | `target.generate` + `target.settings.tools` (or `target.tools`)               |
| Mastra `Agent`                                               | `target.generate` + `target.getLLM` + `target.listTools` or `target.getTools` |
| VoltAgent `Agent`                                            | `target.generateText` + `target.getTools` / `target.toolManager`              |
| LlamaIndex.TS `AgentWorkflow` (`agent()`)                    | `target.run` + `target.runStream` + `target.agents` map                       |
| OpenAI Agents SDK `Agent`                                    | `target.tools` array + `target.handoffs` + an AgentHooks emitter (`on`)       |
| Single tool                                                  | exposes `_call` / `execute` / `call` / `run` / `invoke`                       |

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:

```text theme={null}
[VisIQ] Cannot detect agentic framework. Pass a LangChain AgentExecutor, LangGraph CompiledGraph, or a tool with one of: _call, execute, call, run, invoke.
```

### Retriever detection

For LangChain targets, the harness checks each tool in `target.tools` against four patterns:

| Order | Pattern                   | Match condition                                                                                                                                                                                                      | What gets patched                                                                                         |
| ----- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| 1     | LangChain `BaseRetriever` | `tool._getRelevantDocuments` is a function                                                                                                                                                                           | `_getRelevantDocuments()` wrapped; results filtered per document                                          |
| 2     | Generic retriever         | `tool.retrieve` is a function (no `_getRelevantDocuments`)                                                                                                                                                           | `retrieve()` wrapped and filtered                                                                         |
| 3     | Nested retriever          | `tool.retriever` is an object                                                                                                                                                                                        | Recurses into `tool.retriever` and re-applies patterns 1–3                                                |
| 4     | Retriever-backed tool     | `tool.func` is a function, plus a reachable `.retriever` **or** a retrieval-suggesting name — an explicit `retriev*` token always counts; `search…doc`/`knowledge` count only when the name carries no mutation verb | `func` wrapped; a reachable retriever is filtered per document, otherwise the returned result is filtered |

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.

<Note>
  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[]`.
</Note>

***

## `VisiqOptions`

```typescript theme={null}
interface VisiqOptions {
  agentId?: string;
  apiKey?: string;
  endpoint?: string;
  hitlTimeoutMs?: number;
  timeoutMs?: number;
}
```

| Field           | Type     | Env var fallback        | Default            | Description                                                                                |
| --------------- | -------- | ----------------------- | ------------------ | ------------------------------------------------------------------------------------------ |
| `agentId`       | `string` | `VISIQ_AGENT_ID`        | auto-derived       | Agent identity for every evaluation                                                        |
| `apiKey`        | `string` | `VISIQ_API_KEY`         | —                  | Harness API key (`vq_prod_…` / `vq_test_…`) from Settings → Harness Keys                   |
| `endpoint`      | `string` | `VISIQ_ENDPOINT`        | — (**no default**) | Backend base URL — set `https://api.visiqlabs.com`                                         |
| `hitlTimeoutMs` | `number` | `VISIQ_HITL_TIMEOUT_MS` | `120000`           | Max wait (ms) for a human to resolve an `approval_required` decision before failing closed |
| `timeoutMs`     | `number` | `VISIQ_TIMEOUT_MS`      | `5000`             | Network timeout (ms) per backend call on the decision path                                 |

**`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.

<Warning>
  **There is no default `endpoint`.** If `endpoint` or `apiKey` is unset, the harness has no backend and never loads a rule bundle, so its mode is **never confirmed** — it cold-starts in `monitor` (monitor-until-confirmed): every wrapped tool call and document is observed but nothing is blocked. Governance only takes effect once the harness reaches a backend, so set `endpoint: "https://api.visiqlabs.com"` (or `VISIQ_ENDPOINT`).
</Warning>

***

## 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.

| Mode      | Behavior                                                                                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `monitor` | **The default.** Every event is evaluated and the would-be decision is recorded, but nothing is blocked, masked, or paused — observe first, enforce when the dashboard shows your rules behaving as intended. |
| `enforce` | Decisions are enforced: denials block, masks redact, approvals pause.                                                                                                                                         |
| `off`     | Evaluation is skipped entirely — no enforcement, no telemetry.                                                                                                                                                |

**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/bundle` in the background when you wrap, then refreshes every 5 seconds with `If-None-Match` (a `304` keeps the cached bundle).
* Every evaluation runs locally against the cached bundle. The only decision-path network traffic is human approval: registering an `approval_required` decision, 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.

The fail-closed ladder:

1. **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`) runs `monitor` — it observes and records but never blocks. An agent that was **confirmed in `enforce`** and 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-confirmed `monitor`/`off` agent stays permissive.
2. **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 `enforce` mode — 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's `no_coverage` setting: "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.
3. **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:

| Facet     | Decision            | What happens                                                                                                                                                               |
| --------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Action    | `permit`            | The tool runs unchanged                                                                                                                                                    |
| Action    | `deny`              | The tool never runs; a block message is returned as the tool's output                                                                                                      |
| Action    | `approval_required` | The call pauses while a human approves or rejects it                                                                                                                       |
| Action    | `mask`              | The tool runs with the named arguments redacted                                                                                                                            |
| Retrieval | `allow`             | The document passes through unchanged                                                                                                                                      |
| Retrieval | `deny`              | The document is excluded from the results                                                                                                                                  |
| Retrieval | `redact`            | The document's fields/patterns are masked                                                                                                                                  |
| Retrieval | `escalate`          | Retrieval never pauses: the document passes through and the escalate decision is recorded in the decision stream — a rule with the mask fallback returns it masked instead |

### 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 — your `invoke()` caller receives a normal completion:

```text theme={null}
[VisIQ <rule-code>] Action blocked: <description>. (VisIQ is a security harness installed by your developer.)
```

`<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](/rules/action/hitl).

### 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 via `GET /record/envelopes/:id/verify`. See [Audit Trail](/record/introduction).
