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