# Compile an action rule from natural language
Source: https://docs.visiqlabs.com/api/action-rules/compile-an-action-rule-from-natural-language
/api-reference/openapi.yaml post /allow/rules/compile
Compile a plain-language policy request into Rego. Rate limited to 10 requests per minute per account. Set `?stream=true` (or send `Accept: text/event-stream`) for a streamed response. When no AI provider is configured this facet returns a pre-canned fallback (HTTP 200 with a `warning`), not an error.
Requires permission `allow_rules:create`.
# Create an action rule
Source: https://docs.visiqlabs.com/api/action-rules/create-an-action-rule
/api-reference/openapi.yaml post /allow/rules
Create an action-governance rule from Rego source. Requires permission `allow_rules:create`.
# Delete an action rule
Source: https://docs.visiqlabs.com/api/action-rules/delete-an-action-rule
/api-reference/openapi.yaml delete /allow/rules/{id}
Delete an action rule. Requires permission `allow_rules:delete`.
# Get an action rule
Source: https://docs.visiqlabs.com/api/action-rules/get-an-action-rule
/api-reference/openapi.yaml get /allow/rules/{id}
Fetch a single action rule including its Rego source. Requires permission `allow_rules:view`.
# List action rules
Source: https://docs.visiqlabs.com/api/action-rules/list-action-rules
/api-reference/openapi.yaml get /allow/rules
Paginated list of action-governance rules. Requires permission `allow_rules:view`.
# Update an action rule
Source: https://docs.visiqlabs.com/api/action-rules/update-an-action-rule
/api-reference/openapi.yaml put /allow/rules/{id}
Partially update an action rule. At least one field is required. Requires permission `allow_rules:update`.
# Create an agent
Source: https://docs.visiqlabs.com/api/agents/create-an-agent
/api-reference/openapi.yaml post /allow/agents
Register a new agent. The response includes a one-time plaintext `api_key` that is unrecoverable afterwards, so store it securely. If you omit `api_key` in the request, one is generated. Requires permission `allow_agents:create`.
# Delete an agent
Source: https://docs.visiqlabs.com/api/agents/delete-an-agent
/api-reference/openapi.yaml delete /allow/agents/{id}
Delete an agent. Requires permission `allow_agents:delete`.
# Get an agent
Source: https://docs.visiqlabs.com/api/agents/get-an-agent
/api-reference/openapi.yaml get /allow/agents/{id}
Fetch a single agent by its UUID. Requires permission `allow_agents:view`.
# List agents
Source: https://docs.visiqlabs.com/api/agents/list-agents
/api-reference/openapi.yaml get /allow/agents
Paginated list of registered agents. Requires permission `allow_agents:view`.
# Regenerate an agent's AI name
Source: https://docs.visiqlabs.com/api/agents/regenerate-an-agents-ai-name
/api-reference/openapi.yaml post /allow/agents/{id}/regenerate-naming
Re-runs AI naming for one agent now, from its normalized event schemas. This is an explicit opt-in, so it takes AI ownership of the name even if a person set it. A naming failure returns 200 with `status: skipped`. Requires permission `allow_agents:update`.
# Register agent environment
Source: https://docs.visiqlabs.com/api/agents/register-agent-environment
/api-reference/openapi.yaml post /allow/agents/register
The idempotent registration handshake an SDK/harness performs on startup: it provisions the agent if needed and records environment metadata (OS, hostname, IP, username). It returns only an acknowledgement and no API key. Requires scope `rules:evaluate` (or `allow:write`) and permission `allow_agents:create`.
# Report agent tool surface
Source: https://docs.visiqlabs.com/api/agents/report-agent-tool-surface
/api-reference/openapi.yaml post /allow/agents/tools
The harness startup handshake that reports the tools an agent was bound with: name, description and argument JSON Schema (shape only, never argument values). VisIQ persists them and infers the agent's blast radius. Called again whenever the tool set changes. Requires scope `rules:evaluate` (or `allow:write`) and permission `allow_agents:create`.
# Report installed agent skills
Source: https://docs.visiqlabs.com/api/agents/report-installed-agent-skills
/api-reference/openapi.yaml post /allow/agents/skills
The harness startup handshake that reports the skills installed for an agent: name, path, description and a SHA-256 of the instruction body. The body itself never leaves the agent. Requires scope `rules:evaluate` (or `allow:write`) and permission `allow_agents:create`.
# Stream agent mode changes
Source: https://docs.visiqlabs.com/api/agents/stream-agent-mode-changes
/api-reference/openapi.yaml get /allow/agents/me/stream
A Server-Sent Events stream of mode changes for one agent. The first event is a snapshot of the agent's effective mode; later `mode_changed` events carry `{"agent_mode": "enforce" | "monitor" | "off"}`. A `:ping` comment is sent every 25 seconds to keep idle proxies from closing the connection. Requires permission `allow_agents:view`.
# Update an agent
Source: https://docs.visiqlabs.com/api/agents/update-an-agent
/api-reference/openapi.yaml put /allow/agents/{id}
Partially update an agent. Setting `mode` to `null` clears the per-agent override so it inherits the account default. At least one field is required. Requires permission `allow_agents:update`.
# Get a retrieval decision receipt
Source: https://docs.visiqlabs.com/api/audit-log/get-a-retrieval-decision-receipt
/api-reference/openapi.yaml get /recall/receipts/{id}
The cryptographic receipt for a retrieval decision: an Ed25519 signature and public key over the SHA-256 payload hash, optional Merkle proof fields, and the linked decision. Requires permission `recall_receipts:view`.
# Get a single action decision
Source: https://docs.visiqlabs.com/api/audit-log/get-a-single-action-decision
/api-reference/openapi.yaml get /v1/allow/decisions/{id}
Fetch one action decision by ID, including the linked approval item when the decision was `approval_required`. Requires permission `allow_audit_log:view` and scope `rules:read` (or `allow:read`).
# Query the action decision log
Source: https://docs.visiqlabs.com/api/audit-log/query-the-action-decision-log
/api-reference/openapi.yaml get /v1/allow/audit-log
Paginated, filterable log of action-governance decisions. Requires permission `allow_audit_log:view` and scope `allow:read`.
# Query the record event log
Source: https://docs.visiqlabs.com/api/audit-log/query-the-record-event-log
/api-reference/openapi.yaml get /v1/record/audit-log
Paginated, filterable log of recorded events across all records. Requires permission `record_audit_log:view` and scope `record:read`.
# Query the retrieval decision log
Source: https://docs.visiqlabs.com/api/audit-log/query-the-retrieval-decision-log
/api-reference/openapi.yaml get /v1/recall/audit-log
Paginated, filterable log of retrieval-governance decisions. Requires permission `recall_audit_log:view` and scope `recall:read`.
# Reveal a retrieval decision's original payload
Source: https://docs.visiqlabs.com/api/audit-log/reveal-a-retrieval-decisions-original-payload
/api-reference/openapi.yaml get /recall/decisions/{id}/unmask
Decrypts the original, unmasked payload of a retrieval decision and records an audit event for the reveal. Only available when the organization has turned on masked-original retention; otherwise 404. Requires permission `payloads:unmask`.
# Ingest cognition beats
Source: https://docs.visiqlabs.com/api/cognition/ingest-cognition-beats
/api-reference/openapi.yaml post /allow/cognition
The wrapped harness batch-ships Agent Cortex cognition beats here. Capture is off by default and is re-checked on the server: beats for an agent with capture off are counted in `dropped` and not stored. Beats are validated one by one, like decision telemetry. Raw content is encrypted at rest. Accepted batches return 202.
# Query captured cognition sessions
Source: https://docs.visiqlabs.com/api/cognition/query-captured-cognition-sessions
/api-reference/openapi.yaml get /v1/allow/cognition/sessions
Paginated list of Agent Cortex cognition sessions (capture is default-OFF and server-gated per agent). Sorted by `last_event_at` descending. Sessions are identified by their natural `session_id` key. Requires permission `allow_cognition:view` and the dedicated scope `cognition:read` (never granted by `allow:read`/`rules:read`).
# Query cognition beats (events)
Source: https://docs.visiqlabs.com/api/cognition/query-cognition-beats-events
/api-reference/openapi.yaml get /v1/allow/cognition/events
Paginated cognition beats. Under a `session_id` filter, beats are ordered `seq` ascending (the client-authoritative interleave order); otherwise `created_at` descending. `limit` above 200 is a 400, never a silent clamp. Stored content is floor-redacted; the encrypted raw is NEVER served here (reveal is the audited unmask endpoint). Beat `cost` carries raw token buckets only; price at read time. Requires permission `allow_cognition:view` and scope `cognition:read`.
# Reveal a retained cognition value
Source: https://docs.visiqlabs.com/api/cognition/reveal-a-retained-cognition-value
/api-reference/openapi.yaml post /allow/cognition/reveal
Decrypts one retained raw value (beat content, tool arguments or a spawn prompt) and records an audit event for the reveal. Pass either the opaque `token` shown in the dashboard or an explicit `event_id` and `field`. Returns 404 when the organization does not retain raw content. Requires permission `payloads:unmask`.
# Accept a delegation grant
Source: https://docs.visiqlabs.com/api/delegation/accept-a-delegation-grant
/api-reference/openapi.yaml post /orchestrate/grants/{id}/accept
The child agent accepts a pending grant and receives a signed `grant_token` to present on subsequent delegated evaluations. The child identity must be supplied in the `X-Agent-ID` header. Requires scope `allow:write`.
# Create a delegation grant
Source: https://docs.visiqlabs.com/api/delegation/create-a-delegation-grant
/api-reference/openapi.yaml post /orchestrate/grants
A parent agent creates a scoped, time-boxed delegation to a child agent. The grant starts `pending` and must be accepted within the accept window. Requires scope `allow:write`.
# Get grant status
Source: https://docs.visiqlabs.com/api/delegation/get-grant-status
/api-reference/openapi.yaml get /orchestrate/grants/{id}
Poll a grant's lifecycle status. Requires scope `allow:write` or `allow:read`.
# Revoke a delegation grant
Source: https://docs.visiqlabs.com/api/delegation/revoke-a-delegation-grant
/api-reference/openapi.yaml post /orchestrate/grants/{id}/revoke
Revoke a grant and eagerly cascade the revocation to every descendant grant. Returns the number of grants revoked. Requires scope `allow:write`.
# Enforce a delegated action
Source: https://docs.visiqlabs.com/api/evaluation/enforce-a-delegated-action
/api-reference/openapi.yaml post /orchestrate/evaluate
A delegated (child) agent checks a single action against the scope of its signed grant token. A `deny` outcome is a governance result and is still returned with HTTP 200; the hand-off is recorded either way.
Requires scope `allow:write`.
# Evaluate a governed event (unified)
Source: https://docs.visiqlabs.com/api/evaluation/evaluate-a-governed-event-unified
/api-reference/openapi.yaml post /evaluate
The single, operation-native evaluation endpoint. Declare which operations the event performs via `operations[]` and receive the union decision vocabulary. Hybrid events (e.g. `["retrieval","action"]`) are evaluated on both facets and combined fail-closed to the most restrictive outcome. A legacy `{ kind: "action" | "retrieval", ... }` shape is also accepted for compatibility.
Requires scope `rules:evaluate` (dual-accepts the legacy `allow:write` / `recall:write`).
# Evaluate a retrieval
Source: https://docs.visiqlabs.com/api/evaluation/evaluate-a-retrieval
/api-reference/openapi.yaml post /recall/evaluate
Evaluate a retrieval, tool call, or prompt render against your retrieval-governance rules and return the enforced decision (with mask directives when the decision is `redact`).
Requires scope `rules:evaluate` (or the legacy `recall:write`).
# Evaluate an agent action
Source: https://docs.visiqlabs.com/api/evaluation/evaluate-an-agent-action
/api-reference/openapi.yaml post /allow/evaluate
Evaluate a single agent action against your action-governance rules and return the enforced decision. This is the agent-facing hot path.
Requires scope `rules:evaluate` (or the legacy `allow:write`).
# Create an approval request
Source: https://docs.visiqlabs.com/api/human-in-the-loop/create-an-approval-request
/api-reference/openapi.yaml post /allow/hitl/queue
Directly enqueue an approval request. Requires permission `allow_hitl:respond`.
# List the approval queue
Source: https://docs.visiqlabs.com/api/human-in-the-loop/list-the-approval-queue
/api-reference/openapi.yaml get /allow/hitl/queue
Paginated approval queue. Defaults to `pending` items (FIFO). Filter by `status` and `category`. Requires permission `allow_hitl:view`.
# Respond to an approval request
Source: https://docs.visiqlabs.com/api/human-in-the-loop/respond-to-an-approval-request
/api-reference/openapi.yaml post /allow/hitl/queue/{id}
Resolve a pending approval item. The accepted request body depends on the item's `category`: `enduser` items take `{ decision: "approved" | "rejected", responded_by }`; all other categories take `{ action: "dismiss" | "create_rule", responded_by, linked_rule_id? }` (`linked_rule_id` is required when `action` is `create_rule`). Requires permission `allow_hitl:respond`.
# Revoke the calling API key
Source: https://docs.visiqlabs.com/api/key-lifecycle/revoke-the-calling-api-key
/api-reference/openapi.yaml post /allow/self/revoke
Revokes the exact API key that authenticated this request. Any key, harness or management, can always revoke itself; no permission is needed, and no other key can be named. A dashboard session or a legacy root key gets a 400. A key that is already revoked cannot authenticate, so a repeat call fails with 401.
# Finalize a record
Source: https://docs.visiqlabs.com/api/records/finalize-a-record
/api-reference/openapi.yaml patch /record/records/{id}/finalize
Seal a record so no further events can be appended. Idempotent; a record that is already finalized is returned unchanged. Requires permission `record_records:finalize`.
# Get a checkpoint
Source: https://docs.visiqlabs.com/api/records/get-a-checkpoint
/api-reference/openapi.yaml get /record/checkpoints/{seq}
Fetch a single checkpoint by its batch sequence. Requires permission `record_records:view` and scope `record:read`.
# Get a record
Source: https://docs.visiqlabs.com/api/records/get-a-record
/api-reference/openapi.yaml get /record/records/{id}
Fetch a single record. Requires permission `record_records:view`.
# Ingest a record envelope
Source: https://docs.visiqlabs.com/api/records/ingest-a-record-envelope
/api-reference/openapi.yaml post /record/envelopes
Append a signed event (with optional artifacts and attestations) to the tamper-evident audit trail. `tenantId` must match the authenticated account. Requires permission `record_records:create` and scope `record:write`.
# List record artifacts
Source: https://docs.visiqlabs.com/api/records/list-record-artifacts
/api-reference/openapi.yaml get /record/records/{id}/artifacts
The artifacts attached to a record. Requires permission `record_records:view`.
# List record attestations
Source: https://docs.visiqlabs.com/api/records/list-record-attestations
/api-reference/openapi.yaml get /record/records/{id}/attestations
The attestations attached to a record. Requires permission `record_records:view`.
# List record events
Source: https://docs.visiqlabs.com/api/records/list-record-events
/api-reference/openapi.yaml get /record/records/{id}/events
The ordered event log for a record. Requires permission `record_records:view`.
# List records
Source: https://docs.visiqlabs.com/api/records/list-records
/api-reference/openapi.yaml get /record/records
Paginated, filterable list of records. Requires permission `record_records:view`.
# List transparency-log checkpoints
Source: https://docs.visiqlabs.com/api/records/list-transparency-log-checkpoints
/api-reference/openapi.yaml get /record/checkpoints
The tenant-neutral transparency-log checkpoint feed (signed Merkle roots and counts). Paginated by sequence cursor. Requires permission `record_records:view` and scope `record:read`.
# Verify a record envelope
Source: https://docs.visiqlabs.com/api/records/verify-a-record-envelope
/api-reference/openapi.yaml get /record/envelopes/{id}/verify
Run the full cryptographic verification chain (leaf signature, Merkle inclusion, signed root, and RFC-3161 timestamp) for a record. Requires permission `record_records:view` and scope `record:read`.
# Verify hash-chain consistency
Source: https://docs.visiqlabs.com/api/records/verify-hash-chain-consistency
/api-reference/openapi.yaml get /record/chain/consistency
Return a consistency proof over the checkpoint hash-chain for a sequence range. An unverifiable result is reported as `verified: false` in a 200 body (fail-closed), not an error. Requires permission `record_records:view` and scope `record:read`.
# Activate emergency bypass
Source: https://docs.visiqlabs.com/api/retrieval-rules/activate-emergency-bypass
/api-reference/openapi.yaml post /recall/rules/{id}/bypass
Temporarily deactivate a single retrieval rule (a time-boxed emergency bypass). The activation is written to the audit trail. Requires permission `recall_rules:bypass`.
# Compile a retrieval rule from natural language
Source: https://docs.visiqlabs.com/api/retrieval-rules/compile-a-retrieval-rule-from-natural-language
/api-reference/openapi.yaml post /recall/rules/compile
Compile a plain-language retrieval-governance request into Rego. Rate limited to 10 requests per minute per account; SSE streaming supported. This facet is fail-closed: if no AI provider is configured it returns HTTP 503 (unlike action compilation, which returns a fallback).
Requires permission `recall_rules:create`.
# Create a retrieval rule
Source: https://docs.visiqlabs.com/api/retrieval-rules/create-a-retrieval-rule
/api-reference/openapi.yaml post /recall/rules
Create a retrieval-governance rule from Rego source. Requires permission `recall_rules:create`.
# Deactivate emergency bypass
Source: https://docs.visiqlabs.com/api/retrieval-rules/deactivate-emergency-bypass
/api-reference/openapi.yaml delete /recall/rules/{id}/bypass
Clear an active emergency bypass and re-enforce the rule. Requires permission `recall_rules:bypass`.
# Delete a retrieval rule
Source: https://docs.visiqlabs.com/api/retrieval-rules/delete-a-retrieval-rule
/api-reference/openapi.yaml delete /recall/rules/{id}
Delete a retrieval rule. Requires permission `recall_rules:delete`.
# Get a retrieval rule
Source: https://docs.visiqlabs.com/api/retrieval-rules/get-a-retrieval-rule
/api-reference/openapi.yaml get /recall/rules/{id}
Fetch a single retrieval rule including its Rego source. Requires permission `recall_rules:view`.
# List retrieval rules
Source: https://docs.visiqlabs.com/api/retrieval-rules/list-retrieval-rules
/api-reference/openapi.yaml get /recall/rules
Paginated list of retrieval-governance rules. Requires permission `recall_rules:view`.
# Update a retrieval rule
Source: https://docs.visiqlabs.com/api/retrieval-rules/update-a-retrieval-rule
/api-reference/openapi.yaml put /recall/rules/{id}
Partially update a retrieval rule. At least one field is required. Requires permission `recall_rules:update`.
# Get the action rule bundle
Source: https://docs.visiqlabs.com/api/rule-bundles/get-the-action-rule-bundle
/api-reference/openapi.yaml get /allow/rules/bundle
The compiled action-governance bundle for one agent, used by the SDK runtime. Responds with an `ETag`; send `If-None-Match` to get a `304`. A shutdown agent returns a fail-closed shutdown bundle. Requires permission `allow_rules:view`.
# Get the retrieval rule bundle
Source: https://docs.visiqlabs.com/api/rule-bundles/get-the-retrieval-rule-bundle
/api-reference/openapi.yaml get /recall/rules/bundle
The compiled retrieval-governance bundle for the SDK runtime. Responds with an `ETag`; send `If-None-Match` to get a `304`. Requires permission `recall_rules:view`.
# Get the unified rule bundle
Source: https://docs.visiqlabs.com/api/rule-bundles/get-the-unified-rule-bundle
/api-reference/openapi.yaml get /rules/bundle
The single compiled bundle covering both facets for one agent, carrying the resolved agent attributes and dialect version. Responds with an `ETag`; send `If-None-Match` to get a `304`. Requires permission `allow_rules:view`.
# Get governance settings
Source: https://docs.visiqlabs.com/api/settings/get-governance-settings
/api-reference/openapi.yaml get /allow/settings
Fetch the account's governance defaults. Returns `200` if a row exists, or `201` when defaults are first initialized. Requires permission `allow_settings:view`.
# Update governance settings
Source: https://docs.visiqlabs.com/api/settings/update-governance-settings
/api-reference/openapi.yaml put /allow/settings
Partial update of the account's governance defaults. Only the supplied fields change; at least one is required. Requires permission `allow_settings:update`.
# Record an action's execution outcome
Source: https://docs.visiqlabs.com/api/telemetry/record-an-actions-execution-outcome
/api-reference/openapi.yaml post /allow/execution-events
Closes the loop on a permitted decision: after the action runs, the SDK reports whether it succeeded, failed or errored. An outcome can be recorded once per decision; a second report is a 409.
# Report locally-evaluated decisions
Source: https://docs.visiqlabs.com/api/telemetry/report-locally-evaluated-decisions
/api-reference/openapi.yaml post /allow/telemetry
The SDK batches decisions it evaluated locally (from the rule bundle) and ships them here so they reach the audit log. Each decision is validated on its own: valid items are ingested, malformed items come back in `rejected` with their index, and the request is a 400 only when every item is invalid. Ingest is idempotent on `decision_id`. Accepted batches return 202.
# Create a rule
Source: https://docs.visiqlabs.com/api/unified-rules/create-a-rule
/api-reference/openapi.yaml post /rules
Create a rule tagged with the operations it governs. `operations[]` is mapped to `applies_to`. Requires permission `allow_rules:create`.
# Delete a rule
Source: https://docs.visiqlabs.com/api/unified-rules/delete-a-rule
/api-reference/openapi.yaml delete /rules/{id}
Delete a rule. Requires permission `allow_rules:delete`.
# Get a rule
Source: https://docs.visiqlabs.com/api/unified-rules/get-a-rule
/api-reference/openapi.yaml get /rules/{id}
Fetch a single rule including its Rego source. Requires permission `allow_rules:view`.
# List rules
Source: https://docs.visiqlabs.com/api/unified-rules/list-rules
/api-reference/openapi.yaml get /rules
Paginated list of rules across both facets. Filter by facet with `?operations=action,retrieval`. Requires permission `allow_rules:view`.
# Update a rule
Source: https://docs.visiqlabs.com/api/unified-rules/update-a-rule
/api-reference/openapi.yaml put /rules/{id}
Partially update a rule. At least one field is required. Requires permission `allow_rules:update`.
# Authentication
Source: https://docs.visiqlabs.com/authentication
The canonical guide to VisIQ credentials — the two key audiences (harness vs management), permissions and scopes, rotation, self-revocation, the agent device flow, and MCP.
Every call into VisIQ authenticates with an API key sent as a bearer token.
There are exactly two kinds of key, and picking the right one is the first
decision. This page is the map; each section links to the deep reference.
```bash theme={null}
Authorization: Bearer vq_prod_...
```
***
## The two key audiences
VisIQ issues keys in two **audiences**. The audience is fixed when the key is
created and determines what the key can reach.
| | Harness keys | Management keys |
| - | - | - |
| **Who uses it** | The SDK / harness / agents at runtime | Your scripts, CI, and back-office tooling |
| **What it reaches** | The SDK operational endpoints only — evaluation, rule bundles, HITL decision polling, telemetry, record ingestion, agent registration, discovery reporting | The management API — rules, agents, audit log, settings, and more |
| **How it's scoped** | Not scoped: full power within its route allowlist, denied everywhere else | An explicit `resource:action` permission list you choose at creation |
| **Where to create one** | **Settings → Harness Keys** (or minted for you by the SDK install studios and agent registration) | **Settings → API Keys**, or the agent device flow |
The two audiences are asymmetric. A harness key that calls a management endpoint
is rejected with `403 harness_key_not_permitted`, no matter what else it can do.
A management key is a **superset** — it may also reach the runtime endpoints its
permissions cover.
**The `vq_prod_` / `vq_test_` prefix encodes the *environment*, not the
audience.** Whether a key is a harness or a management key is fixed at creation
and shown in the dashboard — it is **not** derivable from the key string. A
`vq_prod_…` key can be either audience; read the audience from the dashboard key
table, not from the prefix.
**`test` keys are a labeling convention, not an isolated sandbox.** A `vq_test_`
key authenticates against the same tenant and the same data at
`https://api.visiqlabs.com` as a `vq_prod_` key — the prefix only helps you tell
credentials apart in logs and secret stores. There is no separate test tenant or
sandboxed dataset behind it.
If you are integrating the `@visiq/harness` SDK, you want a **harness key** — see
the [Quickstart](/quickstart). Everything below the audiences is about management
keys and the flows shared by both.
***
## Permissions & scopes
A management key carries an **explicit list of permissions** — the same
`resource:action` catalogue that governs your team members (for example
`allow_rules:view`, `allow_agents:create`, `allow_audit_log:view`). Enforcement is
exact and fail-closed: a request is allowed only when the route's required
permission is in the key's list, with no wildcard expansion and no implication
between permissions. Anything else returns `403 insufficient_permission`, naming
both the required permission and what the key holds.
You can only grant permissions you hold yourself — a request for anything beyond
your own effective permissions is rejected with `403` and an `exceededPermissions`
list, so a narrowly-permissioned caller can never bootstrap a stronger key.
**Legacy scoped keys** created before explicit permissions shipped carry coarse
**scopes** instead of a permission list (for example `rules:read` / `rules:write`
/ `rules:evaluate`, or `full_access`). They are still honored, and a scope denial
returns `403 insufficient_scope`. Automation keys created in the dashboard use
explicit permissions. The API still accepts the legacy `scopes` shape when
creating a management key, and harness keys always carry a fixed set of
operational scopes rather than a permission list. Full model:
[The permission model](/automation/introduction#the-permission-model).
***
## Lifecycle: create, rotate, revoke
The complete lifecycle — creation dialog, key format, expiry, rotation with grace
windows, rate limits, and the error reference — lives in
[Managing API keys](/automation/api-keys). The essentials:
Name it, pick environment and expiry, select permissions. The plaintext key
is shown **exactly once** — VisIQ stores only a SHA-256 hash.
Issue a new secret for the same logical key with a configurable grace window
so in-flight callers cut over with zero downtime.
Immediate and irreversible: the next request with that key gets `401`. Every
create, rotate, and revoke is written to your audit log, including keys
issued and revoked through the agent device flow.
A key can always revoke **itself** — no permission required, authenticated by
the presenting key. The clean way for an agent to end its own session.
Self-revocation is authenticated by the presenting key itself, so the only key it
can ever target is the caller's own — cross-key revocation is impossible through
that path ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009) spirit):
```bash theme={null}
# Works for a harness OR a management key — make it the last call with that key
curl -X POST https://api.visiqlabs.com/allow/self/revoke \
-H "Authorization: Bearer vq_prod_..."
```
***
## Agents that mint their own key: the device flow
An AI agent can obtain its own credential without a human pasting one in, through
the human-approved `agent_auth` **device flow**
([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)-shaped). The agent registers,
shows an operator a short `user_code`, the operator reviews the requested audience
and permissions and approves, and the agent polls for the key — issued **exactly
once**. Issuance always requires a human; there is no anonymous or instant
credential, and the grant can never exceed the approver's own permissions.
By default the flow requests a **harness** key; pass
`"requested_key_type": "management"` with an explicit `requested_permissions` list
to request the management surface. Only a signed-in person can approve or deny
a registration. The device flow writes `AGENT_KEY_ISSUED`,
`AGENT_REGISTRATION_DENIED` and `AGENT_KEY_REVOKED` to the approving account's
audit log. See
[Agent self-registration](/automation/agent-device-flow) for the full ceremony,
and the machine-readable [agent authentication guide](https://visiqlabs.com/auth.md)
served at `visiqlabs.com/auth.md`.
***
## Operating over MCP
Once an agent holds a **management** key it can operate VisIQ through the
authenticated [Platform MCP server](/automation/mcp) — the same management
surface as the web app, with every tool gated by the identical RBAC permission
its web route requires. Point any MCP client at
`https://app.visiqlabs.com/api/mcp` with an `Authorization: Bearer` header, then
call `whoami` first to confirm the permissions your key resolves to. A harness
key is **not** accepted there; the server is fail-closed.
***
## Where each audience is documented
The runtime credential the `@visiq/harness` SDK uses. Minted by the install
studios or under **Settings → Harness Keys**.
Automation keys for scripts and CI, their permission model, and guardrails.
Full lifecycle: creation, format, expiry, rotation, rate limits, errors.
The one-line definitions of harness vs management keys.
# Agent self-registration
Source: https://docs.visiqlabs.com/automation/agent-device-flow
How an AI agent obtains its own VisIQ credential through the human-approved agent_auth device flow — register, get a user code, have an operator approve, poll for the key.
Instead of a human minting a key up front and pasting it into your agent, the
agent can **register itself** and have a human approve it. VisIQ implements this
as an [RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)-shaped **device
authorization flow** (the `agent_auth` grant). Issuance still always requires a
human to approve — there is no anonymous or instant credential.
This is the machine-readable counterpart to the
[agent authentication guide](https://visiqlabs.com/auth.md) served for agents at
`visiqlabs.com/auth.md`. The flow is advertised in the `agent_auth` block of
[`/.well-known/oauth-authorization-server`](https://visiqlabs.com/.well-known/oauth-authorization-server).
## When to use it
* Your agent runs somewhere an operator cannot pre-provision a key, but a human
can approve a one-time request.
* You want the credential **bound to the approving human's account** and scoped
to exactly what they agree to — never broader than their own permissions.
If an operator can simply create a key in the dashboard, use
[Managing API keys](/automation/api-keys) instead — this flow exists for the
agent-initiated case.
## The flow at a glance
1. **Agent → VisIQ:** `POST /api/agent/identity` to register.
2. **VisIQ → Agent:** returns a `claim_token`, a short `user_code`, and a
verification URL.
3. **Agent → Human:** show the operator the `user_code` /
`verification_uri_complete`.
4. **Human → VisIQ:** the operator signs in, opens the URL, reviews the
requested audience and permissions, and approves.
5. **Agent → VisIQ (poll):** `POST /api/agent/identity/claim` every `interval`
seconds — `400 authorization_pending` until approved.
6. **VisIQ → Agent:** once approved, the `api_key` is returned **exactly once**.
## Step 1 — Register
`POST https://app.visiqlabs.com/api/agent/identity`
```json theme={null}
{ "type": "anonymous", "agent_name": "my-agent" }
```
By default this requests a **harness** key (least privilege — see
[key audiences](/automation/introduction)). To request the **full product
surface**, ask for a management key, optionally with an explicit permission
list:
```json theme={null}
{
"type": "anonymous",
"agent_name": "my-agent",
"requested_key_type": "management",
"requested_permissions": [
"allow_rules:view",
"allow_rules:create",
"allow_agents:view",
"allow_audit_log:view"
]
}
```
| Field | Required | Notes |
| - | - | - |
| `type` | yes | `anonymous`, or `identity_assertion` to assert a verified identity |
| `agent_name` | no | A label the operator sees on the approval screen (≤ 200 chars) |
| `assertion_type` / `assertion` | no | With `type: identity_assertion` — e.g. `verified_email` + the address |
| `requested_key_type` | no | `harness` (default) or `management` |
| `requested_permissions` | no | For a management key: the exact RBAC grants you need (≤ 64). Omit for the default product grant |
Omitting `requested_permissions` requests the **default product grant** below.
The default grant never includes account administration: a key minted from the
default grant cannot manage API keys, the team, or billing.
* `dashboard:view`
* `allow_rules:view`
* `allow_rules:create`
* `allow_rules:update`
* `allow_rules:delete`
* `recall_rules:view`
* `recall_rules:create`
* `recall_rules:update`
* `recall_rules:delete`
* `allow_agents:view`
* `allow_agents:create`
* `allow_agents:update`
* `allow_agents:delete`
* `allow_audit_log:view`
* `recall_audit_log:view`
* `record_records:view`
* `record_audit_log:view`
* `audit_log:view`
* `events:view`
* `allow_hitl:view`
* `allow_hitl:respond`
* `allow_settings:view`
* `allow_settings:update`
An explicit `requested_permissions` list may name **any** permission in the
catalogue, account administration included. The approver sees the full list
before approving, and the grant is always bounded by what the approver holds:
nobody can approve a permission they do not have themselves.
Registration is rate-limited per client IP (10 per minute and 100 per hour,
counted across every VisIQ server). Over the limit you get
`429 {"error": "rate_limited"}` with a `Retry-After` header; if the limiter
itself cannot answer, you get `503 {"error": "temporarily_unavailable"}` and
should retry later.
The response is a device-flow ceremony:
```json theme={null}
{
"registration_id": "...",
"claim_token": "vqac_...",
"claim": {
"user_code": "WXYZ-2345",
"verification_uri": "https://app.visiqlabs.com/agent/claim",
"verification_uri_complete": "https://app.visiqlabs.com/agent/claim?user_code=WXYZ-2345",
"expires_in": 1800,
"interval": 5
}
}
```
Keep the `claim_token` private — it is what you exchange for the key in Step 3.
Show the **human** the `user_code` / `verification_uri`.
## Step 2 — Have a human approve
Show your operator the `verification_uri_complete` — a single clickable link
with the code already embedded (RFC 8628 one-click); the code survives the
sign-in / MFA detour and the approval page auto-loads your pending registration.
Fall back to the bare `verification_uri` plus the typed `user_code` if a
one-click link is not usable.
They sign in to VisIQ, open the URL, **review the requested audience and
permissions**, and approve. Approval is for signed-in people only: a request to
the approval endpoint made with an API key (management keys included) is refused
with `403`, so one agent can never approve another. Approval mints a governed key bound to their account
— harness by default, management if you requested it and they agreed. They can
narrow the grant, and can never grant beyond their own permissions.
## Step 3 — Poll for the credential
`POST https://app.visiqlabs.com/api/agent/identity/claim`, every `interval`
seconds, with your `claim_token`:
```json theme={null}
{ "claim_token": "vqac_..." }
```
While the human has not approved yet, you get:
```json theme={null}
{ "error": "authorization_pending" }
```
(HTTP `400`). Keep polling at `interval` seconds. If you poll sooner than that,
you get `slow_down` instead (RFC 8628 section 3.5), also HTTP `400`, carrying
the new, longer interval to use from then on:
```json theme={null}
{ "error": "slow_down", "interval": 10 }
```
Each `slow_down` adds 5 seconds, up to 60. Once approved, you receive the
credential **exactly once**, whatever your polling pace:
```json theme={null}
{
"token_type": "api_key",
"api_key": "vq_prod_...",
"agent_id": "...",
"api_base_url": "https://api.visiqlabs.com",
"key_type": "harness",
"permissions": null
}
```
* `key_type` tells you which audience was actually granted (`harness` or
`management`).
* For a management key, `permissions` is the exact grant list your credential
holds; it is `null` for a harness key.
The `api_key` is shown **once**. Store it in your secret manager immediately —
VisIQ keeps only a SHA-256 hash and cannot show it again. If you lose it,
[revoke it](#revoking) and register again.
Claim tickets expire after **30 minutes**. If the ceremony lapses (the human
never approves in time), register again for a fresh `user_code`.
Then use the key as a Bearer token — see
[Use a key](/automation/api-keys#use-a-key).
## Revoking
You can retire a device-flow credential yourself, at any time:
* **Cancel or revoke the device-flow credential** —
`POST https://app.visiqlabs.com/api/agent/identity/revoke` with
`{ "token": "vq_prod_..." }` (an issued key) or `{ "claim_token": "vqac_..." }`
(the pending ticket, which also revokes an already-issued key).
An unknown or already-revoked token still gets `200`, so the endpoint never
reveals whether a token existed. If the revoke could not be completed you get
`503 {"error": "temporarily_unavailable"}`: treat the key as still live and
retry. Revoking a live key is recorded in the approving account's audit log
as `AGENT_KEY_REVOKED` (a denied registration is recorded as
`AGENT_REGISTRATION_DENIED`, an issued key as `AGENT_KEY_ISSUED`).
* **Self-revoke any key** you hold — `POST /allow/self/revoke`, authenticated by
the key itself, no permission required. See
[Revoke your own key](/automation/api-keys#revoke-your-own-key-self-revocation).
## Prefer MCP?
Once you hold a **management** key you can operate VisIQ through the
[Platform MCP server](/automation/mcp) instead of raw REST — including retiring
the key with the `revoke_self` tool when you are done.
# Managing API Keys
Source: https://docs.visiqlabs.com/automation/api-keys
Create, rotate, and revoke automation keys — key format, expiry, grace windows, rate limits, and error codes.
Automation keys authenticate your scripts and CI against the VisIQ management
API. This page covers their full lifecycle. For what automation keys are and
how the permission model works, start with the
[Platform Automation introduction](/automation/introduction).
**Agents can obtain automation keys too.** Besides the dashboard flow below,
an AI agent can self-register through the `agent_auth` device flow with
`"requested_key_type": "management"` and an explicit permission list; a human
reviews the exact grant at **`/agent/claim`** and approves or narrows it. The
grant can never exceed the approver's own permissions. See the
[agent authentication guide](https://visiqlabs.com/auth.md).
***
## Create a key
1. In the dashboard, open **Settings → API Keys**.
2. Click **Create New Key** (requires the `api_keys:create` permission).
3. Fill in the dialog:
* **Name** — a label for the key (1–50 characters), e.g. `ci-audit-export`.
* **Environment** — `production` or `test`. This only determines the key
prefix (`vq_prod_` vs `vq_test_`) so you can tell credentials apart.
* **Expires** — `30 days`, `90 days` (default), `180 days`, `1 year`, or
`No expiry`. Prefer an expiry; expired keys stop authenticating
automatically.
* **Permissions** — pick the explicit permissions the key needs, grouped
the same way as the team Roles matrix. Each group has a select-all
checkbox, and **Grant all** selects everything — use it sparingly.
4. The full key is displayed **exactly once** — copy it into your secrets
manager immediately.
You can only grant permissions you hold yourself. A request for anything
beyond your own effective permissions is rejected with `403` and an
`exceededPermissions` list — no key can be more powerful than its creator.
Every creation is recorded in your audit log as `API_KEY_CREATED`, and the
operation aborts if the audit write fails.
The plaintext key is shown only at creation (and rotation). VisIQ stores a
SHA-256 hash — verified with a timing-safe comparison — never the key itself.
If you lose it, rotate or re-create the key. Never commit keys to source
control.
### Key format
```text theme={null}
vq_prod_f3a91c0e5b27d8146a0c9e3f71b52d80e4c6a1f97d3b08e25c41f6a890b7d213
vq_test_0d8e2a71c45f9b36e810d72c4a95f3081b6e0d49c27a85f1e3b09c64d2a7f581
```
A `vq_prod_` / `vq_test_` prefix followed by 64 hex characters. The dashboard
key table shows only the first 16 characters (`vq_prod_f3a91c0e****`) so you
can identify a key without exposing it.
**The prefix encodes the *environment*, not the audience.** `vq_prod_` vs
`vq_test_` only tells you which environment a key was labeled for — whether it is
a **harness** key or a **management** key is fixed at creation and shown in the
dashboard, not derivable from the string. A `vq_prod_…` key can be either
audience. See [Authentication](/authentication#the-two-key-audiences).
**`test` keys are a labeling convention, not an isolated sandbox.** A `vq_test_`
key authenticates against the **same tenant and the same data** at
`https://api.visiqlabs.com` as a `vq_prod_` key — the prefix only helps you keep
credentials apart in logs and secret stores. There is no separate test tenant or
sandboxed dataset behind it, so treat a `vq_test_` key with the same care as a
production one.
***
## Use a key
Send it as a bearer token to `https://api.visiqlabs.com`:
```bash theme={null}
curl https://api.visiqlabs.com/v1/allow/audit-log \
-H "Authorization: Bearer vq_prod_..."
```
The request succeeds when the route's required permission is in the key's
permission list — this example needs `allow_audit_log:view` — otherwise it is
denied (see [Errors](#errors)).
Every request a key authenticates updates its **Last Used** timestamp, shown in
the dashboard key table. Requests to `https://api.visiqlabs.com` update it on
every call; requests a management key makes to the web JSON API
(`https://app.visiqlabs.com/api/...`) update it at most once every 60 seconds
per key, so the value can trail real use by up to a minute. Use it to spot
stale keys and revoke them before
they become forgotten liabilities.
***
## Rotate a key
Rotation issues a **new secret** for the same logical key — name, environment,
permissions, audience, and expiry all carry over — and retires the old one.
In the key table, open the row's actions menu and choose **Rotate Key**
(requires `api_keys:rotate`).
The rotation dialog asks how long to **keep the old key valid** — the grace
window: `Revoke immediately`, `1 hour`, `24 hours` (default), or `7 days`.
* The new key is returned **once**, exactly like at creation.
* The **old key keeps authenticating until the grace window lapses**, so
in-flight callers can cut over with zero downtime. The table shows the
retiring row as `Grace ends in …`.
* With **Revoke immediately**, the old key stops authenticating at once.
* The grace window never extends a key's life: if the old key was due to
expire sooner, it still expires then, and an old key that has already
expired is revoked at once.
* You can only rotate a key whose permissions you hold yourself; otherwise the
rotation is refused with `403` and an `exceededPermissions` list.
* Once the grace window lapses the old key returns `401 API key has expired`.
* The retired key is linked to its replacement (`replaced_by_key_id`), so the
rotation chain is auditable, and the rotation itself is logged as
`API_KEY_ROTATED`. If the audit write fails, the new key is rolled back —
a rotation never leaves an unaudited live secret behind.
Rotate-with-grace means **two** valid secrets exist for the duration of the
window. Pick **Revoke immediately** when you suspect the old key is
compromised — grace is for routine credential hygiene, not incident response.
***
## Revoke a key
Revocation is available today, including for existing automation keys. Choose
**Delete Key** in the row's actions menu (requires `api_keys:delete`) and
confirm. Revocation is **immediate** — the very next request with that key
receives `401` — and is logged as `API_KEY_REVOKED`. There is no grace window
and no undo; create a new key if you revoked the wrong one.
You can revoke a management key only if you hold every permission it grants, or
if you created it yourself. Otherwise the request is refused with
`403 Cannot revoke a key more powerful than you` and an `exceededPermissions`
list, so a narrowly-permissioned member cannot remove a more powerful key.
Harness keys can be revoked by anyone holding `api_keys:delete`. If the key was
revoked but the audit entry could not be written, the response is
`500 API key revoked, but the audit write failed`: the key is already dead.
***
## Revoke your own key (self-revocation)
Beyond the dashboard, **the holder of a key can always revoke that key itself** —
no dashboard, no human, and **no special permission** ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009)
spirit). This is the clean way for an automation or agent to end its own session
or retire a credential it believes is compromised, and it is the only revoke
path a permission-scoped key can rely on (such a key may not hold
`api_keys:delete`).
The endpoint is authenticated by the **presenting key itself** — the credential
is resolved from its own bearer token, so the only key it can ever target is the
caller's own. There is no request body naming another key, which makes cross-key
revocation impossible through this path.
```bash theme={null}
# Works for a harness OR a management key
curl -X POST https://api.visiqlabs.com/allow/self/revoke \
-H "Authorization: Bearer vq_prod_..."
```
```json theme={null}
{ "revoked": true, "api_key_id": "...", "key_type": "management" }
```
The same operation is exposed on the web JSON API at
`POST https://app.visiqlabs.com/api/allow/self/revoke`, and as the
[`revoke_self`](/automation/mcp) tool on the Platform MCP server (no arguments).
Self-revocation is **immediate, idempotent, and irreversible**. After it
succeeds the key stops authenticating at once, so it must be the **last call**
you make with that key. A key that is already revoked, unknown, or expired
returns `401`; a dashboard user session (not an API key) returns `400
not_an_api_key`. Legacy vendor root keys have no `api_keys` row and are not
self-revocable — rotate them from the dashboard instead.
Agents that obtained their key through the
[device flow](/automation/agent-device-flow) can also tear it down (including a
still-pending registration) via `POST /api/agent/identity/revoke`.
***
## Expiry
A key with an expiry stops authenticating the moment `expires_at` passes —
requests receive `401 API key has expired`. The dashboard shows expiry as
`in 12d` / `Never` / `Expired`. Expiry cannot be edited after creation, and a
rotated key's replacement **inherits the original expiry** — to extend a key's
life, create a new key.
***
## Rate limits
API-key requests are rate limited per key with a sliding window: **600
requests per minute** by default. The budget is **per key across both hosts**:
requests to `https://api.visiqlabs.com` and to the web JSON API at
`https://app.visiqlabs.com/api/...` spend the same allowance. A legacy
account-level root key (one created before per-key API keys, with no row in the
key table) has the same limit per account on the web JSON API; on
`https://api.visiqlabs.com` it is not yet rate limited. Every authenticated
response carries the current state:
| Header | Meaning |
| - | - |
| `X-RateLimit-Limit` | The per-window request limit |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
Exceeding the limit returns `429 Too Many Requests` with a `Retry-After`
header (seconds until the window frees up):
```json theme={null}
{
"error": "rate_limited",
"detail": "API key rate limit exceeded.",
"retryAfter": 42
}
```
Back off for `Retry-After` seconds before retrying. Dashboard sessions are not
subject to this limit — it applies to API-key traffic only. The limiter is
fail-closed: if it cannot be evaluated, the request is denied rather than
waved through.
***
## Permission ↔ endpoint matrix
Each management route requires one explicit permission (a few also require a coarse scope). Grant a key exactly the permissions its endpoints need. `full_access` (or a legacy scope that covers the route) satisfies any of these; owners hold every permission.
### Action governance
| Permission | Endpoints |
| - | - |
| `allow_agents:view` | `GET /allow/agents`, `GET /allow/agents/:id`, `GET /allow/agents/me/stream` |
| `allow_agents:create` | `POST /allow/agents`, `POST /allow/agents/register`, `POST /allow/agents/tools` |
| `allow_agents:update` | `PUT /allow/agents/:id`, `POST /allow/agents/:id/regenerate-naming` |
| `allow_agents:delete` | `DELETE /allow/agents/:id` |
| `allow_rules:view` | `GET /allow/rules`, `GET /allow/rules/:id`, `GET /allow/rules/bundle` |
| `allow_rules:create` | `POST /allow/rules`, `POST /allow/rules/compile` |
| `allow_rules:update` | `PUT /allow/rules/:id` |
| `allow_rules:delete` | `DELETE /allow/rules/:id` |
| `allow_hitl:view` | `GET /allow/hitl/queue` |
| `allow_hitl:respond` | `POST /allow/hitl/queue`, `POST /allow/hitl/queue/:id` |
| `allow_audit_log:view` | `GET /v1/allow/audit-log`, `GET /v1/allow/decisions/:id` |
| `allow_cognition:view` | `GET /v1/allow/cognition/sessions`, `GET /v1/allow/cognition/events` (dedicated `cognition:read` scope — never granted by `allow:read`/`rules:read`) |
| `allow_settings:view` | `GET /allow/settings` |
| `allow_settings:update` | `PUT /allow/settings` |
| `payloads:unmask` | `POST /allow/cognition/reveal` |
### Retrieval governance
| Permission | Endpoints |
| - | - |
| `recall_rules:view` | `GET /recall/rules`, `GET /recall/rules/:id`, `GET /recall/rules/bundle` |
| `recall_rules:create` | `POST /recall/rules`, `POST /recall/rules/compile` |
| `recall_rules:update` | `PUT /recall/rules/:id` |
| `recall_rules:delete` | `DELETE /recall/rules/:id` |
| `recall_rules:bypass` | `POST /recall/rules/:id/bypass`, `DELETE /recall/rules/:id/bypass` |
| `recall_audit_log:view` | `GET /v1/recall/audit-log` |
| `recall_receipts:view` | `GET /recall/receipts/:id` |
| `payloads:unmask` | `GET /recall/decisions/:id/unmask` |
### Unified rules
The unified `/rules` CRUD reuses the action-facet rule permissions, so no new grants are needed:
| Permission | Endpoints |
| - | - |
| `allow_rules:view` | `GET /rules`, `GET /rules/:id`, `GET /rules/bundle` |
| `allow_rules:create` | `POST /rules` |
| `allow_rules:update` | `PUT /rules/:id` |
| `allow_rules:delete` | `DELETE /rules/:id` |
### Audit trail
| Permission | Scope | Endpoints |
| - | - | - |
| `record_records:create` | `record:write` | `POST /record/envelopes` |
| `record_records:view` | — (scope `record:read` on the checkpoint, consistency, and verify routes) | `GET /record/records`, `GET /record/records/:id`, `GET /record/records/:id/events`, `GET /record/records/:id/artifacts`, `GET /record/records/:id/attestations`, `GET /record/envelopes/:id/verify`, `GET /record/checkpoints`, `GET /record/checkpoints/:seq`, `GET /record/chain/consistency` |
| `record_records:finalize` | — | `PATCH /record/records/:id/finalize` |
| `record_audit_log:view` | `record:read` | `GET /v1/record/audit-log` |
### Delegation governance
The `/orchestrate/*` delegation endpoints are gated by the coarse `allow:write` scope (the read-only grant status also accepts `allow:read`) rather than a fine-grained permission — see the [delegation API reference](/rules/delegation/api-reference). A management key with `allow:write` (or `full_access`) reaches them; a harness key is rejected as `harness_key_not_permitted`.
### Evaluation & operational routes
The runtime routes an SDK calls — `POST /evaluate`, `POST /allow/evaluate`, `POST /recall/evaluate`, the rule bundles, `POST /allow/telemetry`, `POST /record/envelopes` — are **scope**-gated (`rules:evaluate`, `allow:write`, `recall:write`, `record:write`), not permission-gated, and a harness key is full-power within that operational allowlist. See the per-facet API references for the exact scope on each. (Delegation is reached operationally through the unified `POST /evaluate` with a `delegation` operation — the dedicated `POST /orchestrate/evaluate` is management-audience and rejects harness keys, as noted above.)
***
## Errors
| Status | Error | Cause |
| - | - | - |
| `401` | `Invalid API key or token` | The key is unknown, malformed, or revoked (`api.visiqlabs.com`) |
| `401` | `Invalid API key` | The key is unknown (web JSON API at `app.visiqlabs.com/api`) |
| `401` | `API key has been revoked` | The key was revoked (web JSON API) |
| `401` | `API key has expired` | The key passed its expiry, or its rotation grace window lapsed |
| `403` | `Cannot revoke a key more powerful than you` | Revoking a management key that grants permissions you do not hold |
| `403` | `insufficient_permission` | The key's permission list does not contain the permission this route requires |
| `403` | `insufficient_scope` | The key's coarse scopes do not cover this scope-gated route |
| `403` | `harness_key_not_permitted` | A harness key was used against a management endpoint — use an automation key |
| `429` | `rate_limited` | Per-key rate limit exceeded — honor `Retry-After` |
| `503` | `Authentication unavailable` | The key could not be checked (web JSON API); retry with backoff |
A `403 insufficient_permission` response tells you exactly what was missing:
```json theme={null}
{
"error": "insufficient_permission",
"detail": "This management API key is not granted the requested permission.",
"requiredPermission": "allow_rules:update",
"grantedPermissions": ["allow_rules:view", "allow_audit_log:view"]
}
```
Fix it by creating a key that includes the `requiredPermission` — permissions
cannot be edited on an existing key.
`403 insufficient_scope` comes from the coarse scope layer. You will see it
most often on legacy keys created before explicit permissions shipped, but any
key whose (derived) scopes do not cover a scope-gated route — the evaluation
endpoints, for example — receives it too. Permission denials on
permission-gated routes return `insufficient_permission` instead.
# Platform Automation
Source: https://docs.visiqlabs.com/automation/introduction
Automate the VisIQ management API from scripts and CI with permission-scoped automation keys.
**Automation keys are live.** Mint them under **Settings → API Keys**, or let
an AI agent request one through the human-approved `agent_auth` device flow
(`"requested_key_type": "management"` — see the
[agent authentication guide](https://visiqlabs.com/auth.md)). Every grant is
permission-scoped and can never exceed its approver's own permissions.
VisIQ issues API keys in two **audiences**. Which one you need depends on what
the key is for:
| | Harness keys | Automation keys |
| - | - | - |
| **Who uses it** | The SDK / harness / agents at runtime | Your scripts, CI pipelines, and back-office tooling |
| **What it can reach** | The SDK operational endpoints only (evaluation — unified and per-facet — rule bundles, HITL decision polling, telemetry, record envelope ingestion, agent registration, discovery sensor reporting) | The management API — rules, agents, audit log, settings, and more |
| **How access is scoped** | Not scoped — full power within its route allowlist, denied everywhere else | An explicit permission list you choose at creation |
| **Where to create one** | **Settings → Harness Keys** (or minted automatically by the SDK install studios and agent registration) | **Settings → API Keys**, or agent self-registration via the `agent_auth` device flow |
If you are integrating the `@visiq/harness` SDK, you want a
[harness key](/quickstart) — the dashboard studios mint one for you. This
section is about the other audience: **automation keys** (also called
management keys), which let your own automation call the VisIQ management API
at `https://api.visiqlabs.com`.
***
## What an automation key is
An automation key is a bearer credential for the management API:
```bash theme={null}
curl https://api.visiqlabs.com/allow/rules \
-H "Authorization: Bearer vq_prod_..."
```
Use it to script anything you could do in the dashboard — manage
action-governance rules, register and update agents, query the audit log,
respond to HITL items, or adjust settings — without a browser session.
The two audiences are asymmetric. A harness key that tries to call a
management endpoint is rejected with `403 harness_key_not_permitted`, no
matter what it is otherwise allowed to do. An automation key, by contrast, may
call any endpoint its permissions allow — including the SDK operational
endpoints.
***
## The permission model
Every automation key carries an **explicit list of permissions** — the same
`resource:action` catalogue that governs your team members (for example
`allow_rules:view`, `allow_agents:create`, `allow_audit_log:view`). You pick
the permissions when you create the key, grouped exactly as they appear in the
team Roles matrix.
Enforcement is **exact and fail-closed**:
* A request is allowed only when the permission required by that route is
**in the key's list**. There is no wildcard expansion and no implication
between permissions — `allow_rules:view` does not grant
`allow_rules:update`.
* Anything not explicitly granted is denied with `403 insufficient_permission`.
The response names the permission the route required and the permissions the
key actually holds, so a denial is always diagnosable:
```json theme={null}
{
"error": "insufficient_permission",
"detail": "This management API key is not granted the requested permission.",
"requiredPermission": "allow_rules:update",
"grantedPermissions": ["allow_rules:view", "allow_audit_log:view"]
}
```
Grant the minimum set the job needs. A nightly job that pulls the
action-governance audit log needs `allow_audit_log:view` — not **Grant all**.
**Operational SDK routes.** A management key is a superset of a harness key,
so it can also reach the runtime endpoints. The evaluation legs — the unified
`POST /evaluate` plus the per-facet `POST /allow/evaluate` and
`POST /recall/evaluate` — are gated by coarse product scopes: a management key
holding a `…:write`-class permission for a product derives the matching write
scope and can evaluate through it. Two runtime routes require an explicit
permission on top: record envelope ingestion (`POST /record/envelopes`) also
requires `record_records:create`, and writing agent attribution
(`POST /allow/agents/register`) also requires `allow_agents:create`. If you
only want a key to reach the management API (rules, agents, audit) and never
the evaluate path, grant read-class permissions — they do not derive the write
scopes that open the operational routes.
**Legacy scoped keys still work.** Keys created before explicit permissions
shipped carry coarse scopes instead of a permission list. The scope
vocabulary is the per-product pairs `allow:read`/`allow:write`,
`recall:read`/`recall:write`, `record:read`/`record:write`, the unified
`rules:read`/`rules:write`/`rules:evaluate` (the unified read and write scopes
subsume their per-product counterparts; `rules:evaluate` is evaluation-only
and never satisfies a rule-management gate), and `full_access`. Scoped keys
are still honored under the original scope-mapping rules, and a scope denial
returns `403 insufficient_scope`. New automation keys always use explicit
permissions.
***
## Built-in guardrails
Automation keys ship with the security properties a credential system should
have, on by default:
* **Privilege-bound minting** — a key can never be created with permissions
its creator does not hold. The request is rejected with `403` and the list
of exceeded permissions, so a narrowly-permissioned caller cannot bootstrap
a stronger key.
* **Audited lifecycle** — every create, rotate, and revoke is written to your
tenant audit log; key creation and rotation **abort (rolling back the new secret) if the audit write fails**.
There is no unaudited key event.
* **Hashed at rest** — VisIQ stores only a SHA-256 hash of each key and
verifies it with a timing-safe comparison. The plaintext is shown exactly
once, at creation.
* **Fail-closed authentication** — expired keys (including a lapsed rotation
grace window) stop authenticating immediately, and every key is
[rate limited](/automation/api-keys#rate-limits) with a per-key sliding
window.
***
## Where to create one
Open **Settings → API Keys** and click **Create New Key** — naming the key,
picking its environment and expiry, and selecting its permissions. The full
key is shown **exactly once**. AI agents can instead self-register via the
`agent_auth` device flow and receive a management key after human approval.
See the [managing keys guide](/automation/api-keys) for the complete flow,
rotation, rate limits, and error reference.
Creation flow, key format, expiry, rotation with grace windows, rate
limits, and error codes.
The management endpoints for rules, agents, audit log, HITL, and settings.
# Platform MCP server
Source: https://docs.visiqlabs.com/automation/mcp
Operate VisIQ from any MCP client. The authenticated Model Context Protocol server exposes rules, agents, decisions, HITL, settings, and natural-language queries as tools.
VisIQ exposes an authenticated **Model Context Protocol (MCP)** server so an AI
agent or MCP-capable client can operate the product with tool calls instead of
raw REST. It is the same management surface as the web app and the
[automation API](/automation/introduction) — every tool enforces the identical
RBAC permission its web route requires.
There are **two** VisIQ MCP servers, both listed in
[`/.well-known/mcp/server-cards.json`](https://visiqlabs.com/.well-known/mcp/server-cards.json):
* **Public MCP** — `https://visiqlabs.com/mcp` — read-only discovery, **no
credentials**. Public information about VisIQ only.
* **Platform MCP** (this page) — `https://app.visiqlabs.com/api/mcp` —
**authenticated**; operates your tenant.
## Endpoint & transport
| | |
| - | - |
| **URL** | `https://app.visiqlabs.com/api/mcp` |
| **Transport** | Streamable HTTP (stateless) |
| **Auth** | `Authorization: Bearer `, required for `tools/call` |
| **Protocol** | MCP `2025-06-18` (also `2025-03-26`) |
You need a **management-audience** API key. Get one from **Connectors → API
Keys** in the dashboard, or have your agent
[self-register](/automation/agent-device-flow) with
`"requested_key_type": "management"`. A harness key is confined to the SDK
operational surface and is **not** accepted here.
### What needs a key
| Request | Credential |
| - | - |
| `initialize`, `ping`, `tools/list` | None. Discovery is open, so a client can list the tools before it has a key. |
| `tools/call` for `describe_chat` | A bearer must be present; the tool reads nothing about it. |
| Every other `tools/call` | A valid management key, checked before the tool runs. |
Tool calls are **fail-closed**. A missing key is a JSON-RPC error; an unknown,
revoked or expired key, or a harness key, is refused before any tool runs; and
a tool whose permission the key does not hold returns an error naming the
missing permission, never a partial result.
## Connect a client
Every client needs the URL and an `Authorization: Bearer` header carrying a management key. Keep the key in an environment variable (`VISIQ_API_KEY` below), not in a file you commit. The Claude Code, Cursor and Claude Desktop shapes follow those clients' own documentation at the time of writing; the `curl` call is tested against this server.
**Claude Code**
```bash theme={null}
claude mcp add --transport http visiq https://app.visiqlabs.com/api/mcp --header "Authorization: Bearer $VISIQ_API_KEY"
```
**Cursor** (`.cursor/mcp.json`)
```json theme={null}
{
"mcpServers": {
"visiq": {
"url": "https://app.visiqlabs.com/api/mcp",
"headers": {
"Authorization": "Bearer ${env:VISIQ_API_KEY}"
}
}
}
}
```
**Claude Desktop** has no field for a custom header on a remote server, so connect it through the community-maintained `mcp-remote` bridge (not a VisIQ project; check its current flags). The header value is passed through `env` because the bridge splits arguments on spaces:
```json theme={null}
{
"mcpServers": {
"visiq": {
"command": "npx",
"args": [
"mcp-remote",
"https://app.visiqlabs.com/api/mcp",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": {
"AUTH_HEADER": "Bearer "
}
}
}
}
```
**Any HTTP client** (check a key without an MCP client):
```bash theme={null}
curl -s https://app.visiqlabs.com/api/mcp \
-H "Authorization: Bearer $VISIQ_API_KEY" \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'
```
Then **start with `whoami`** to confirm which vendor, role and permissions your
key resolves to. Every other tool is gated by those permissions.
## Retries and audit
* **Retries.** Read-only tools are always safe to repeat. `create_rule`, `create_recall_rule` and `compile_rule` take an optional `idempotency_key`: repeat the call with the same key and the same arguments (in any key order) and you get the first result back instead of a second write. Without a key, a repeated `create_rule` or `create_recall_rule` with the same name is refused with `409 A rule with this name already exists`, but a repeated `compile_rule` can create a second rule under another name. `update_rule`, `delete_rule`, `register_agent`, `update_agent`, `respond_hitl`, `update_allow_settings` and `revoke_self` are safe to repeat: a second call changes nothing further or is refused. Key-order tolerance applies to calls made through this server; the REST API hashes the body exactly as sent.
* **Audit.** Tool calls your key makes are recorded in **Settings → Activity** with the tool, the outcome and the key that made it. The record holds the size of the arguments, never their content. A write is recorded before it runs and is not attempted if that record cannot be written; a read is recorded after it answers, on a best-effort basis. Not recorded: `describe_chat`, which reads nothing about your key; calls refused before a key resolves (no key, or an unknown, revoked or expired one); calls naming an unknown tool or sending invalid arguments.
## Tool catalog
Arguments are checked against each tool's `inputSchema` before the tool runs:
an argument the schema does not list, a missing required one, or a value of
the wrong type is refused with `-32602`.
The server advertises 26 tools through `tools/list`. Each one is gated by the same permission as its web API route; call `whoami` to see which your key holds.
### Discovery
#### `whoami`
Return the vendor, role and effective permissions of the management key you authenticated with, exactly as every other tool resolves them.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `any (a valid management key; reports what it holds)` |
| **Safe to retry** | Yes (read-only) |
Takes no arguments.
#### `describe_chat`
Discover the in-product AI chat surface: which chat\_\* tools exist, how to read the NlChatResult widgets (previews/final/clarify/navigate), and the catalog of pages a navigation proposal can point at.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `none (static discovery; a bearer must be present)` |
| **Safe to retry** | Yes (read-only) |
Takes no arguments.
### Governance rules (unified: action and retrieval)
#### `list_rules`
List the tenant's unified governance rules (action and retrieval), paginated. Filter by facet with `operations`.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_rules:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `operations` | `string` | no | Optional comma-separated facet filter, e.g. 'action' or 'action,retrieval'. |
| `page` | `integer` | no | 1-based page number. At least 1. |
| `limit` | `integer` | no | Page size. 1 to 200. |
#### `get_rule`
Fetch a single governance rule (action or retrieval) by id, including its Rego and applies\_to facets.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_rules:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `id` | `string` | yes | Rule id (uuid). |
#### `create_rule`
Create a governance rule. Author the logic in Rego and declare which operation facets it governs. Pass `idempotency_key` to make a retry safe.
| | |
| - | - |
| **Access** | Write (creates) |
| **Permission** | `allow_rules:create` |
| **Safe to retry** | Only with `idempotency_key` |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `operations` | `string[]` | yes | Which operation facets the rule applies to (must include 'action' or 'retrieval'). One of `action`, `retrieval`, `delegation`. 1 to 3 items. |
| `name` | `string` | yes | Human-readable rule name (unique per tenant). 1 to 255 characters. |
| `description` | `string` | no | Optional human description. At most 1000 characters. |
| `rego_source` | `string` | yes | The rule body, authored in Rego (not blank). At least 1 characters. |
| `natural_language` | `string` | no | Optional natural-language summary of the rule intent. At most 2000 characters. |
| `priority` | `integer` | no | Evaluation priority (higher wins ties). Default 0. |
| `enabled` | `boolean` | no | Whether the rule is active. Default true on create. |
| `target_app` | `string or null` | no | Action facet: the target application the rule is scoped to. At most 255 characters. |
| `action_pattern` | `string or null` | no | Action facet: the action pattern the rule is scoped to. At most 255 characters. |
| `trust_tier` | `string or null` | no | Retrieval facet: the trust tier the rule applies to. At most 64 characters. |
| `surface` | `string or null` | no | Retrieval facet: the surface the rule applies to. At most 128 characters. |
| `principal_exclusions` | `string[] or null` | no | Retrieval facet: principals the rule never applies to. |
| `redaction_spec` | `object or null` | no | Retrieval facet: the stored redaction specification (see the rules API reference). |
| `fail_closed_on_empty_spec` | `boolean` | no | Retrieval facet: deny when the redaction spec resolves to nothing. |
| `alert_spec` | `object or null` | no | Alert side-effect that rides the rule's decision; null clears it. |
| `idempotency_key` | `string` | no | Optional retry key (1-256 of A-Z a-z 0-9 \_ -). Retrying with the same key and the same arguments within 24 hours returns the first result instead of creating another rule; the same key with different arguments is refused. |
#### `update_rule`
Update an existing governance rule. Send any subset of its fields; omitted fields are unchanged.
| | |
| - | - |
| **Access** | Write (changes or removes existing state) |
| **Permission** | `allow_rules:update` |
| **Safe to retry** | Yes (a repeat has no further effect) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `id` | `string` | yes | Rule id (uuid). |
| `operations` | `string[]` | no | Which operation facets the rule applies to (must include 'action' or 'retrieval'). One of `action`, `retrieval`, `delegation`. 1 to 3 items. |
| `name` | `string` | no | Human-readable rule name (unique per tenant). 1 to 255 characters. |
| `description` | `string` | no | Optional human description. At most 1000 characters. |
| `rego_source` | `string` | no | The rule body, authored in Rego (not blank). At least 1 characters. |
| `natural_language` | `string` | no | Optional natural-language summary of the rule intent. At most 2000 characters. |
| `priority` | `integer` | no | Evaluation priority (higher wins ties). Default 0. |
| `enabled` | `boolean` | no | Whether the rule is active. Default true on create. |
| `target_app` | `string or null` | no | Action facet: the target application the rule is scoped to. At most 255 characters. |
| `action_pattern` | `string or null` | no | Action facet: the action pattern the rule is scoped to. At most 255 characters. |
| `trust_tier` | `string or null` | no | Retrieval facet: the trust tier the rule applies to. At most 64 characters. |
| `surface` | `string or null` | no | Retrieval facet: the surface the rule applies to. At most 128 characters. |
| `principal_exclusions` | `string[] or null` | no | Retrieval facet: principals the rule never applies to. |
| `redaction_spec` | `object or null` | no | Retrieval facet: the stored redaction specification (see the rules API reference). |
| `fail_closed_on_empty_spec` | `boolean` | no | Retrieval facet: deny when the redaction spec resolves to nothing. |
| `alert_spec` | `object or null` | no | Alert side-effect that rides the rule's decision; null clears it. |
#### `delete_rule`
Delete a governance rule by id.
| | |
| - | - |
| **Access** | Write (changes or removes existing state) |
| **Permission** | `allow_rules:delete` |
| **Safe to retry** | Yes (a repeat has no further effect) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `id` | `string` | yes | Rule id (uuid). |
#### `compile_rule`
Author a governance rule from a natural-language description via the in-product Rule Builder assistant. The assistant compiles the intent to Rego, simulates its impact, and PERSISTS the rule (write). An ACTION rule needs allow\_rules:create, a RETRIEVAL rule needs recall\_rules:create (whichever facet the assistant persists is enforced). Returns the compiled rule graph + related rules. Pass `idempotency_key` to make a retry safe: without it, a retried call can create a second rule.
| | |
| - | - |
| **Access** | Write (creates) |
| **Permission** | `allow_rules:create OR recall_rules:create` |
| **Safe to retry** | Only with `idempotency_key` |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `prompt` | `string` | yes | Natural-language description of the rule to create (e.g. 'block agents from emailing external domains without approval'). 1 to 5000 characters. |
| `surface` | `string` | no | Surface hint. Default auto (the assistant detects it). One of `allow`, `recall`, `auto`. |
| `mode` | `string` | no | generate (default) returns a full rule; edit returns changes to currentGraph. One of `generate`, `edit`. |
| `currentGraph` | `object` | no | The serialized rule graph being edited; needed for mode=edit. |
| `nodeRef` | `string` | no | The rule the edit concerns. At most 255 characters. |
| `selectedNodeId` | `string` | no | The graph node the edit is focused on. At most 128 characters. |
| `idempotency_key` | `string` | no | Optional retry key (1-256 of A-Z a-z 0-9 \_ -). Retrying with the same key and the same arguments within 24 hours returns the first result instead of creating another rule; the same key with different arguments is refused. |
### Retrieval-only rule aliases
#### `list_recall_rules`
List the tenant's retrieval-governance rules only, paginated. `list_rules` returns both facets.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `recall_rules:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `page` | `integer` | no | 1-based page number. At least 1. |
| `limit` | `integer` | no | Page size. 1 to 100. |
#### `create_recall_rule`
Create a retrieval-governance rule, authored in Rego (the retrieval facet is set for you). Pass `idempotency_key` to make a retry safe.
| | |
| - | - |
| **Access** | Write (creates) |
| **Permission** | `recall_rules:create` |
| **Safe to retry** | Only with `idempotency_key` |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `name` | `string` | yes | Human-readable rule name (unique per tenant). 1 to 255 characters. |
| `description` | `string` | no | Optional human description. At most 1000 characters. |
| `rego_source` | `string` | yes | The rule body, authored in Rego (not blank). At least 1 characters. |
| `natural_language` | `string` | no | Optional natural-language summary of the rule intent. At most 2000 characters. |
| `priority` | `integer` | no | Evaluation priority (higher wins ties). Default 0. |
| `enabled` | `boolean` | no | Whether the rule is active. Default true on create. |
| `target_app` | `string or null` | no | Action facet: the target application the rule is scoped to. At most 255 characters. |
| `action_pattern` | `string or null` | no | Action facet: the action pattern the rule is scoped to. At most 255 characters. |
| `trust_tier` | `string or null` | no | Retrieval facet: the trust tier the rule applies to. At most 64 characters. |
| `surface` | `string or null` | no | Retrieval facet: the surface the rule applies to. At most 128 characters. |
| `principal_exclusions` | `string[] or null` | no | Retrieval facet: principals the rule never applies to. |
| `redaction_spec` | `object or null` | no | Retrieval facet: the stored redaction specification (see the rules API reference). |
| `fail_closed_on_empty_spec` | `boolean` | no | Retrieval facet: deny when the redaction spec resolves to nothing. |
| `alert_spec` | `object or null` | no | Alert side-effect that rides the rule's decision; null clears it. |
| `idempotency_key` | `string` | no | Optional retry key (1-256 of A-Z a-z 0-9 \_ -). Retrying with the same key and the same arguments within 24 hours returns the first result instead of creating another rule; the same key with different arguments is refused. |
### Agents
#### `list_agents`
List the tenant's registered AI agents (across all connected frameworks) with governance metadata.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_agents:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `page` | `integer` | no | 1-based page number. At least 1. |
| `limit` | `integer` | no | Page size. 1 to 100. |
| `sort` | `string` | no | Sort column. One of `name`, `created_at`, `request_count_24h`, `deny_rate_1h`. |
| `dir` | `string` | no | Sort direction. Default desc. One of `asc`, `desc`. |
#### `get_agent`
Fetch a single agent by id with its full governance profile.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_agents:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `id` | `string` | yes | Agent row id (uuid). |
#### `register_agent`
Register/connect an AI agent from a supported framework (LangChain, OpenAI Agents SDK, LlamaIndex, Vercel AI SDK, Mastra, VoltAgent, Semantic Kernel, Pydantic AI, OpenClaw, CrewAI, AutoGen, LangGraph, Google ADK, Strands Agents, …). agent\_id is the natural key: registering the same agent\_id again is refused (409), never duplicated.
| | |
| - | - |
| **Access** | Write (creates) |
| **Permission** | `allow_agents:create` |
| **Safe to retry** | Yes (a repeat has no further effect) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `agent_id` | `string` | yes | Stable external agent identifier (the natural key). 1 to 255 characters. |
| `name` | `string` | yes | Display name. 1 to 255 characters. |
| `description` | `string` | no | At most 1000 characters. |
| `agent_type` | `string` | no | The framework/runtime this agent runs on. One of `langchain`, `openai_agents`, `llamaindex`, `vercel_ai`, `mastra`, `voltagent`, `semantic_kernel`, `pydantic_ai`, `openclaw`, `crewai`, `autogen`, `langgraph`, `google_adk`, `strands`, `unknown`. |
| `kind` | `string` | no | sdk (default) or cli\_harness. One of `sdk`, `cli_harness`. |
| `mode` | `string or null` | no | Governance mode. Omit or null to inherit the org default. One of `enforce`, `monitor`, `audit`, `off`. |
| `mode_by_operation` | `object or null` | no | Per-operation mode override; each key is optional. null clears it. |
| `owner_email` | `string` | no | The owning human's email. At most 320 characters. |
| `hitl_pathway` | `string` | no | Channel the owner is asked on. One of `slack`, `teams`, `email`. |
| `trust_tier` | `string` | no | One of `tier1`, `tier2`, `tier3`. |
| `categories` | `string[]` | no | One of `read_only`, `transactional`, `data_processor`, `external_comms`, `code_exec`, `privileged`, `internal`, `customer_facing`, `experimental`, `third_party`. At most 10 items. |
| `no_coverage` | `string or null` | no | Fail-safe when no rule governs an event; null inherits the tenant default. One of `open`, `closed`. |
#### `update_agent`
Update an agent's governance settings (mode, trust tier, categories, business function, owner, HITL pathway, shutdown, …). Unknown fields are refused.
| | |
| - | - |
| **Access** | Write (changes or removes existing state) |
| **Permission** | `allow_agents:update` |
| **Safe to retry** | Yes (a repeat has no further effect) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `id` | `string` | yes | Agent row id (uuid). |
| `name` | `string` | no | 1 to 255 characters. |
| `description` | `string` | no | At most 1000 characters. |
| `mode` | `string or null` | no | null clears the override (inherit). One of `enforce`, `monitor`, `audit`, `off`. |
| `mode_by_operation` | `object or null` | no | Per-operation mode override; each key is optional. null clears it. |
| `owner_email` | `string` | no | At most 320 characters. |
| `hitl_pathway` | `string or null` | no | One of `slack`, `teams`, `email`. |
| `trust_tier` | `string or null` | no | One of `tier1`, `tier2`, `tier3`. |
| `categories` | `string[]` | no | Replaces the whole set. One of `read_only`, `transactional`, `data_processor`, `external_comms`, `code_exec`, `privileged`, `internal`, `customer_facing`, `experimental`, `third_party`. At most 10 items. |
| `business_function` | `string or null` | no | Pins a single business function (snake\_case id). At most 48 characters. |
| `business_functions` | `string[]` | no | Pins the full business-function set (wins over business\_function). At most 64 items. |
| `business_function_source` | `string` | no | 'ai' releases a human pin; 'user' pins without changing the set. One of `ai`, `user`. |
| `no_coverage` | `string or null` | no | One of `open`, `closed`. |
| `fail_mode` | `string or null` | no | One of `open`, `closed`. |
| `shutdown` | `boolean` | no | true shuts the agent down (fail-closed); false reactivates it. |
### Decisions and action schemas
#### `get_decision_outcomes`
Read governance decision outcomes (the decision audit log): each agent action or retrieval and whether it was permitted, denied, sent to approval, or masked.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_audit_log:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `agent_id` | `string` | no | 1 to 255 characters. |
| `decision` | `string` | no | One of `permit`, `deny`, `approval_required`, `mask`. |
| `start_date` | `string` | no | ISO date/time lower bound. |
| `end_date` | `string` | no | ISO date/time upper bound. |
| `page` | `integer` | no | 1-based page number. At least 1. |
| `limit` | `integer` | no | Page size. 1 to 100. |
#### `list_action_schemas`
List the discovered action/tool schemas and their field mappings for the tenant's agents.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_rules:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `limit` | `integer` | no | Page size. 1 to 200. |
| `source_type` | `string` | no | Only action or only retrieval schemas. One of `action`, `retrieval`. |
### Human-in-the-loop
#### `list_hitl_queue`
List human-in-the-loop approval requests (actions routed for human review). Defaults to open requests.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_hitl:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `status` | `string` | no | Canonical statuses (open, approved, rejected, timed\_out) or a legacy raw status, or all. One of `open`, `approved`, `rejected`, `timed_out`, `pending`, `timeout`, `expired`, `dismissed`, `resolved`, `all`. |
| `category` | `string` | no | One of `enduser`, `engineer`, `finance`, `security`, `compliance`, `other`. |
| `page` | `integer` | no | 1-based page number. At least 1. |
| `limit` | `integer` | no | Page size. 1 to 100. |
#### `respond_hitl`
Respond to a human-in-the-loop request. For an end-user approval send `decision`; for other items send `action` (dismiss or create\_rule). The approver is recorded from the authenticated management key you call with. It cannot be supplied or spoofed.
| | |
| - | - |
| **Access** | Write (changes or removes existing state) |
| **Permission** | `allow_hitl:respond` |
| **Safe to retry** | Yes (a repeat has no further effect) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `id` | `string` | yes | HITL queue entry id. |
| `decision` | `string` | no | The decision on a queued end-user action. machine\_decide only when the org enabled it for the agent. One of `approved`, `rejected`, `machine_decide`. |
| `action` | `string` | no | For non-end-user items. One of `dismiss`, `create_rule`. |
| `linked_rule_id` | `string` | no | A rule to link the item to (uuid). |
### Settings
#### `get_allow_settings`
Read the tenant's governance settings (no-coverage defaults, autopilot, HITL timeout, default agent mode, fail mode, redaction floor, Context Firewall policy, auto-disable).
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_settings:view` |
| **Safe to retry** | Yes (read-only) |
Takes no arguments.
#### `update_allow_settings`
Update the tenant's governance settings. Send any subset of the fields; omitted fields are unchanged.
| | |
| - | - |
| **Access** | Write (changes or removes existing state) |
| **Permission** | `allow_settings:update` |
| **Safe to retry** | Yes (a repeat has no further effect) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `no_coverage_defaults` | `object` | no | What an uncovered event gets: any of \{read, write, delete, admin} each approve\|deny\|ask, and retrieval approve\|deny. |
| `autopilot_enabled` | `boolean` | no | |
| `hitl_timeout_seconds` | `integer` | no | 30 to 120. |
| `notification_channels` | `object[]` | no | \[\{type, config}] notification targets. |
| `enduser_hitl_enabled` | `boolean` | no | |
| `auto_create_proposed_rules` | `boolean` | no | |
| `recall_retain_masked_original` | `boolean` | no | |
| `firewall_store_masked_values` | `boolean or null` | no | null inherits (on for sandbox/demo orgs). |
| `firewall_egress_proxy_enabled` | `boolean` | no | |
| `firewall_browsers_behind_sensor` | `boolean` | no | |
| `firewall_record_clean_sends` | `boolean` | no | Store Context Firewall decisions no rule acted on. Default false: only masked, blocked or observe-mode would-have decisions are stored. |
| `firewall_mode` | `string` | no | One of `enforce`, `observe`. |
| `firewall_mask_text` | `string or null` | no | 1 to 32 characters. |
| `firewall_masked_value_ttl_days` | `integer or null` | no | 1 to 3650. |
| `firewall_governed_hosts` | `string[]` | no | Bare lowercase host names whose traffic the firewall governs. At most 50 items. |
| `auto_disable_high_interference` | `boolean` | no | |
| `system_prompt_leak_protection` | `boolean` | no | |
| `default_agent_mode` | `string` | no | One of `enforce`, `monitor`, `off`. |
| `fail_mode` | `string` | no | One of `open`, `closed`. |
| `recall_floor_disabled_detectors` | `string[]` | no | Legacy opt-out list, kept for compatibility. One of `private_key`, `jwt`, `aws_access_key`, `github_token`, `slack_token`, `stripe_secret`, `google_api_key`, `llm_api_key`, `bearer_token`, `connection_string`, `pan`, `iban`, `aba_routing`, `us_ein`, `swift_bic`, `ssn`, `uk_nino`, `in_aadhaar`, `in_pan_tax`, `br_cpf`, `es_dni_nie`, `ca_sin`, `uk_nhs_number`, `au_tfn`, `email`, `us_phone`, `date_of_birth`, `person_name`, `ipv4`, `mac_address`. At most 30 items. |
| `recall_floor_enabled_optin` | `string[]` | no | The masking-floor detectors the org enables (empty: the floor masks nothing). One of `private_key`, `jwt`, `aws_access_key`, `github_token`, `slack_token`, `stripe_secret`, `google_api_key`, `llm_api_key`, `bearer_token`, `connection_string`, `pan`, `iban`, `aba_routing`, `us_ein`, `swift_bic`, `ssn`, `uk_nino`, `in_aadhaar`, `in_pan_tax`, `br_cpf`, `es_dni_nie`, `ca_sin`, `uk_nhs_number`, `au_tfn`, `email`, `us_phone`, `date_of_birth`, `person_name`, `ipv4`, `mac_address`. At most 30 items. |
### Natural-language queries
#### `chat_rules_table`
Ask the in-product AI assistant about the governance RULES table in natural language ('which enabled rules block writes to external APIs?'). Returns a machine-friendly NlChatResult (previews/final query/clarify/navigate). Does not mutate. It resolves a query the Rules table would apply.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `recall_rules:view + allow_rules:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `prompt` | `string` | yes | The natural-language question or request, as a user would type it in the chat dock. 1 to 2000 characters. |
| `history` | `object[]` | no | Prior conversation turns for multi-turn (e.g. to answer a previous `clarify`). Append \{role:'assistant',content:\} then \{role:'user',content:\}. At most 40 items. |
| `currentRoute` | `string` | no | Optional route the chat is 'on'; lets the assistant propose navigating to another page for off-table requests. 1 to 256 characters. |
#### `chat_events_log`
Ask the in-product AI assistant about the EVENT LOG (governance decision outcomes) in natural language ('show denies on stripe in the last hour'). Returns a machine-friendly NlChatResult. Read-only query resolution.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `events:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `prompt` | `string` | yes | The natural-language question or request, as a user would type it in the chat dock. 1 to 2000 characters. |
| `history` | `object[]` | no | Prior conversation turns for multi-turn (e.g. to answer a previous `clarify`). Append \{role:'assistant',content:\} then \{role:'user',content:\}. At most 40 items. |
| `currentRoute` | `string` | no | Optional route the chat is 'on'; lets the assistant propose navigating to another page for off-table requests. 1 to 256 characters. |
| `agentId` | `string` | no | Optional allow\_agents UUID to hard-scope the query to one agent (Agent Dossier). At least 1 characters. |
#### `chat_agents_inventory`
Ask the in-product AI assistant about the AGENTS inventory in natural language ('which discovered agents are ungoverned shadow AI?'). Returns a machine-friendly NlChatResult. Read-only query resolution.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `events:view + allow_agents:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `prompt` | `string` | yes | The natural-language question or request, as a user would type it in the chat dock. 1 to 2000 characters. |
| `history` | `object[]` | no | Prior conversation turns for multi-turn (e.g. to answer a previous `clarify`). Append \{role:'assistant',content:\} then \{role:'user',content:\}. At most 40 items. |
| `currentRoute` | `string` | no | Optional route the chat is 'on'; lets the assistant propose navigating to another page for off-table requests. 1 to 256 characters. |
#### `chat_hitl_queue`
Ask the in-product AI assistant about the HUMAN-IN-THE-LOOP approval queue in natural language ('pending refunds over \$500 from the fraud bot'). Returns a machine-friendly NlChatResult. Read-only query resolution (use respond\_hitl to act on an entry).
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `allow_hitl:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `prompt` | `string` | yes | The natural-language question or request, as a user would type it in the chat dock. 1 to 2000 characters. |
| `history` | `object[]` | no | Prior conversation turns for multi-turn (e.g. to answer a previous `clarify`). Append \{role:'assistant',content:\} then \{role:'user',content:\}. At most 40 items. |
| `currentRoute` | `string` | no | Optional route the chat is 'on'; lets the assistant propose navigating to another page for off-table requests. 1 to 256 characters. |
#### `chat_data_table`
Ask the in-product AI assistant about ANY other data table by supplying that table's field schema (the generic dock every StandardTablePage carries). Returns a machine-friendly NlChatResult with the resolved query or a navigation proposal.
| | |
| - | - |
| **Access** | Read-only |
| **Permission** | `dashboard:view` |
| **Safe to retry** | Yes (read-only) |
| Argument | Type | Required | Notes |
| - | - | - | - |
| `prompt` | `string` | yes | The natural-language question or request, as a user would type it in the chat dock. 1 to 2000 characters. |
| `fields` | `object[]` | yes | The table's canonical column vocabulary the query may filter on. 1 to 60 items. |
| `surface` | `string` | no | Optional human label for the table. 1 to 64 characters. |
| `currentRoute` | `string` | no | Optional route the chat is 'on'; lets the assistant propose navigating to another page for off-table requests. 1 to 256 characters. |
| `history` | `object[]` | no | Prior conversation turns for multi-turn (e.g. to answer a previous `clarify`). Append \{role:'assistant',content:\} then \{role:'user',content:\}. At most 40 items. |
### Key lifecycle
#### `revoke_self`
Revoke THIS API key: the exact management key you are authenticated with (RFC 7009 self-revocation). No special permission is required, and no other key can be targeted: the credential is identified from the request's own bearer token. After this succeeds the key immediately stops authenticating, so it MUST be your final call with it. Irreversible.
| | |
| - | - |
| **Access** | Write (changes or removes existing state) |
| **Permission** | `none (self-authenticated by the presenting key)` |
| **Safe to retry** | Yes (a repeat has no further effect) |
Takes no arguments.
## Errors and limits
### Errors
A failure is reported in one of two places. A problem with the request itself is a JSON-RPC **error**. A tool that ran and was refused, or failed, is a normal **result** with `isError: true`, and its text carries the HTTP status and the route's error body.
| JSON-RPC code | When |
| - | - |
| `-32700` | The body is not valid JSON (HTTP 400). |
| `-32600` | Not a JSON-RPC 2.0 message; a batch (batching is not supported, HTTP 400); an unsupported `MCP-Protocol-Version` (HTTP 400); a browser `Origin` that is not allowed (HTTP 403). |
| `-32601` | An unknown method. |
| `-32602` | `tools/call` without a tool name, an unknown tool, or arguments that do not match the tool's `inputSchema` (unknown, missing or ill-typed, at any depth). `error.data.issues` lists each problem as `{path, code}` and never repeats a value. |
| `-32001` | `tools/call` without a management key. A session token (a three-part JWT) is not a management key. |
| `-32000` | Rate limited by this endpoint (HTTP 429 with `Retry-After`). |
| Tool result (`isError: true`) | Carries | Meaning |
| - | - | - |
| `400` | `Invalid request body` | The route refused the arguments after they passed the schema check (for example a blank or unsafe rule body). |
| `401` | `Invalid API key` | The key is not known. |
| `401` | `API key has been revoked` | The key was revoked. |
| `403` | `insufficient_permission` | The key lacks the tool's permission. A key holding no permission at all is told so, and can still call `revoke_self`. |
| `403` | `harness_key_not_permitted` | A harness (agent) key was used; this server needs a management key. |
| `403` | `exemplar_read_only` | A sandbox-reader key tried to write. |
| `403` | `experience_not_entitled` | The workspace's plan does not include that part of VisIQ. |
| `409` | `A rule with this name already exists` | A rule with that name exists. An un-keyed retry of `create_rule` or `create_recall_rule` ends here rather than creating a duplicate. |
| `409` | `idempotency_in_progress` | A request with the same `idempotency_key` is still running; retry shortly. |
| `422` | `idempotency_key_reused` | The `idempotency_key` was already used with different arguments. |
| `429` | `rate_limited` | The key's own request budget is spent (shared with the REST API); wait `retryAfter` seconds. |
| `503` | `Authentication unavailable` | Authentication is temporarily unavailable. Nothing was changed. |
| `503` | `idempotency_unavailable` | The retry-key store is unavailable, so a keyed write was not attempted. |
| no status | `The audit trail is unavailable` | A write was not attempted because it could not be recorded first. |
| no status | `failed with an internal error` | The tool failed unexpectedly. Quote the incident id in the message to support; no internal detail is returned. |
### Limits and transport
| | |
| - | - |
| **Requests per client IP** | 300 per minute by default |
| **`tools/call` per key** | 120 per minute by default |
| **AI tools** (`chat_*`, `compile_rule`) | also 60 per 60 seconds for the workspace's API keys together, and 240 for the whole workspace |
| **Rate-limited response** | HTTP 429, `Retry-After: 60` |
| **Protocol versions** | `2025-06-18`, `2025-03-26`. `initialize` answers with the version you ask for when it is supported, otherwise the newest. Later requests should send `MCP-Protocol-Version`; an absent header is read as `2025-03-26`. |
| **Transport** | Stateless Streamable HTTP: one JSON-RPC message per `POST`, a JSON response, no session id, no server-sent events (`GET` is 405), no batching. |
| **Browser clients** | A request without an `Origin` header (a server-side client) is accepted. A browser `Origin` must be the app's own or on the server's allowlist; others get 403. |
| **Retry keys** | `idempotency_key` is remembered for 24 hours per key and tool. |
The first two limits are per-deployment settings (`MCP_RATE_LIMIT_IP_PER_MIN` and `MCP_RATE_LIMIT_KEY_PER_MIN`) and are enforced on each server instance, as a guard against a single runaway client rather than as a billing quota. Every call also spends the key's own request budget, which it shares with the REST API.
## Related
* [Agent authentication](https://visiqlabs.com/auth.md) — the agent-facing auth guide (device flow + self-revoke).
* [Managing API keys](/automation/api-keys) — key lifecycle, formats, errors.
* [Agent self-registration](/automation/agent-device-flow) — obtain a key without a pre-provisioned credential.
# Changelog
Source: https://docs.visiqlabs.com/changelog
Release notes for the @visiq/harness SDK — what changed in each version, with every breaking change and removal called out. Read this before upgrading.
Release notes for the `@visiq/harness` SDK (and its Python peer `visiq`). The
SDK is **pre-1.0 (0.x): there is no wire-compatibility guarantee yet, and
breaking changes may ship in any minor release until v1.0 GA.** Pin an exact
version and read the `Breaking` / `Removed` entries below before upgrading — see
[SDK versioning & compatibility](/versioning) for the full posture.
**Canonical source.** The registry version history is the authoritative record of
what is live: [npmjs.com/package/@visiq/harness](https://www.npmjs.com/package/@visiq/harness?activeTab=versions).
Check it rather than this page for the current build — these notes are
hand-maintained and have run behind the registry before (`0.2.7` and `0.2.8`
published with no entry here). The package's own `CHANGELOG.md` lives in the repo
and is **not** shipped inside the tarball, whose `files` allowlist is
`dist`, `README.md` and `LICENSE`.
**Every minor may break at 0.x.** A `0.x → 0.x` minor bump is allowed to remove
or rename a field, change a default, or tighten a shape. Don't float a version
range across a minor boundary in an unattended pipeline — pin, then upgrade
deliberately after reading the entries below.
***
## 0.2.10
**Metadata only — no runtime behaviour changed.** The package description was
rewritten onto one shape shared across the whole VisIQ harness/SDK family (npm,
PyPI, Maven Central, RubyGems), so every package now describes itself in the
current action-governance / retrieval-governance / audit-trail vocabulary. No
API, decision, or wire change — a republish is required only because registry
metadata is frozen at publish time.
**Why this is `0.2.10` and not `0.2.9`.** The rewrite was authored against
`0.2.9`, but a concurrent promotion published `0.2.9` from a tree that did not
yet contain it. A published version is immutable, so `0.2.9` on the registry
carries the OLD description permanently and the rewrite needed the next number.
The same collision hit `@visiq/openclaw-plugin` (`0.1.10` → **`0.1.11`**) and
`@visiq/claude-code-harness` (`0.1.5` → **`0.1.6`**). `NPM Content Freshness`
is the gate that caught it: it rebuilds each would-be-published tarball and
compares its whole content hash against the registry artifact at the same
version, so "edited but not bumped" fails a PR instead of silently no-op'ing at
publish time.
`0.2.7` and `0.2.8` shipped without an entry on this page. Their contents are in
the git history; this note records the gap rather than reconstructing them.
***
## 0.2.6
**Republish note.** The security and compatibility changes below were merged
after `0.2.5` was published (2026-07-12) but no version bump followed, so `0.2.5`
on the registry does **not** contain them. `0.2.6` is the first published build
that ships them. (Versions `0.2.1`–`0.2.5` are backfilled separately.)
### Changed
* **Fail-closed on an unrecognized retrieval outcome (security).** The retrieval
application layer now **withholds content** — drops the document or returns the
blocked sentinel — on any outcome verb this build does not recognize, instead
of passing it through. This is defense-in-depth behind the engine's own
fail-closed mapping; `allow` / `escalate` passthrough is unchanged. An unaware
upgrader gets strictly **safer** behavior.
* **Every control-plane request now carries `X-VisIQ-SDK` (version) and
`X-VisIQ-Dialect` (wire-contract capability) headers.** Telemetry only — not
load-bearing for any decision.
* **MUST-UNDERSTAND bundle refuse.** A rule bundle that declares a `min_dialect`
newer than this SDK speaks is refused wholesale (fail-closed, deny-all) rather
than partially applied. Dormant today (`min_dialect` is `1` everywhere); it is
the forward-compatibility safety net for the first wire-contract bump. The
refuse also lives in the shared engine (`@visiq/core-wasm ^0.1.3`) so every
language binding enforces it uniformly.
### Removed
* **Breaking — the `@visiq/harness/record` subpath export is removed.** The audit
layer is now a server-side proof layer only: every action and retrieval
decision is automatically signed (Ed25519) and persisted by the backend. No
SDK import is required to produce or read receipts — query the dashboard or the
REST API instead. The previous types (`RecordEnvelope`, `RecordSource`,
`RecordDecisionPayload`, `SigningResult`, `ReceiptVerificationInput`) have been
deleted from the SDK.
### Deprecated
* The `recall_receipts` Postgres view is deprecated in favor of querying
`decision_receipts` with `source = 'recall'` for complete, consistent views
across all decision sources. The view is retained for backward compatibility
and will be removed in a future release.
***
## 0.2.0
### Added
* `AgentVendor` union type (`'visiq' | 'crowdstrike' | 'intune' | 'sentinelone' | 'unknown'`).
* `FleetInstance.last_scanned: string | null` — ISO timestamp of the last
per-instance heartbeat.
* `FleetInstance.agent_vendor: AgentVendor` — vendor of the agent reporting
heartbeats for the instance.
* `HeartbeatInput { instanceId: string; vendor: AgentVendor }` input type for
`fleet.heartbeat()`.
* `HeartbeatResponse.instance_id` — echoes the heartbeated instance id back to
the caller.
### Breaking
* `fleet.heartbeat()` now requires a `HeartbeatInput` argument
(`{ instanceId, vendor }`). Callers that previously invoked `fleet.heartbeat()`
with no arguments must iterate `fleet.status().instances` and call once per
instance.
***
## Where to look next
The full pre-1.0 posture, the invariants held even at 0.x, and the v1.0
compatibility window.
The `visiq()` surface, options, framework detection, and error behavior.
# Secure ChatGPT
Source: https://docs.visiqlabs.com/connectors/chatgpt
How VisIQ secures ChatGPT. Its reasoning runs on OpenAI's servers, so you secure each form your people use it in.
ChatGPT runs its reasoning loop on OpenAI's servers. There is no agent on your side to install a harness into, so VisIQ does not connect *into* ChatGPT. It secures what your people **send** to it, in each form they use it in, with products you deploy on your own machines.
## Pick the forms your fleet uses
Each row is a separate VisIQ product. You only need the ones for the forms your people actually use.
| Form | Secured by | Set it up |
| - | - | - |
| **Web app** (chatgpt.com) | The Context Firewall browser extension, in the browser, before a message is sent | [Context Firewall browser extension](/connectors/context-firewall) |
| **Native app** (desktop) | The Discovery Sensor's host egress proxy | [Discovery Sensor (Standalone)](/discovery/deployed-sensor) |
| **Extension** (a ChatGPT browser extension) | The Discovery Sensor's host egress proxy | [Discovery Sensor (Standalone)](/discovery/deployed-sensor) |
The desktop app and the extension share one row's product: **one Discovery Sensor install covers both.**
## ChatGPT coverage
* **Supported:** interception on the ChatGPT web app, with decisions recorded in your tenant.
* **Confirm it in your environment:** the ChatGPT desktop app and ChatGPT browser extensions are governed through the Discovery Sensor's egress proxy; run [Verify it is actually working](/connectors/context-firewall#verify-it-is-actually-working) after you deploy the sensor to confirm interception for the form you use.
In the product, open **Connectors → SaaS Apps → ChatGPT → Secure it**. It shows the same pathways as the table above, and each one opens that product's own setup guide.
# Install the Context Firewall extension
Source: https://docs.visiqlabs.com/connectors/context-firewall
Install the VisIQ Context Firewall browser extension — by hand for a single machine, or fleet-wide through your existing managed-browser policy.
**The browsers are not equally served. Read this table before you plan a rollout.**
| Route | State today |
| - | - |
| **Chrome / Edge: self-hosted CRX** | **The route for Chrome and Edge**, on one machine or across a fleet. VisIQ serves the signed package from its own update manifest, so no store review sits between a published fix and your estate. There is no store listing. |
| **Safari on macOS** | **Available**, as a signed and notarized `.pkg` carrying a **universal (Apple silicon and Intel)** build. Safari rolls out as that package plus a managed-extension declaration, never as `ExtensionInstallForcelist`. |
| **Firefox** | **No install path.** The Gecko build exists, but nothing signs an `.xpi` and no Firefox artifact is served, so there is nothing to install. Govern Firefox with the Deployed Sensor's egress proxy instead: see the Firefox card below. |
Chrome and Edge take **one** extension id and **one** update URL, and they must be used together: see [Fleet deployment](#fleet-deployment-managed-browsers).
**Where this lives in the product, and how the rollout screen is organised.**
The card is **Context Firewall (Browser Extension)**, in the **Context Firewall** section of
the Connectors page, beside the Discovery Sensor.
Its rollout studio asks **which browser** before it asks which console, because the browser
decides the mechanism and the console only decides where you paste the result. That ordering
is what stops the screen offering an Intune-shaped flow for Safari, which has no such flow.
It also means the work is done **once per browser you run**: there is no cross-browser
extension-deployment primitive in any MDM, and there is not going to be one.
* **Chrome & Edge** are a single target. Edge reads the same Chromium policy as Chrome at
its own path, and every artifact the studio generates carries both, so there is nothing
separate to deploy for Edge.
* **Safari** is its own target: a signed package plus a declaration, never
`ExtensionInstallForcelist`.
* **Firefox** is shown but **cannot be selected**, and its steps render read-only with no
copy or download control, because there is no artifact to roll out. See the Firefox card
above for exactly what is missing.
The Context Firewall runs **inside the browser, before a message is sent** to a browser-hosted AI assistant (ChatGPT, Gemini, Copilot, Claude, Perplexity). It masks personal data and blocks secrets in what your staff type, with no message content ever leaving the tab. This page covers getting it onto machines — one at a time, or across a fleet through the same managed-policy channel you already use for other extensions.
## Supported browsers
The extension is a Manifest V3 build, and its interception relies on reaching the page's `MAIN` world. Chrome and Edge run `world: "MAIN"` content scripts natively; Firefox has since 128, so it gets its own Gecko build. Safari does not support that manifest key at all — so the Safari build reaches `MAIN` the classic way instead, by having its isolated content script inject a `