# 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 — 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`. # 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 — no API key. Requires scope `rules:evaluate` (or `allow:write`) and permission `allow_agents:create`. # 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 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`. # 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`. # 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`. # 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`. # 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`. New keys always use explicit permissions. 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. 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. 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 (rules, agents, audit/outcomes, action schemas, HITL responses, settings) — but **never account administration**: a device-flow key cannot manage API keys, the team, or billing. 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 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 — do not poll faster. Once approved, you receive the credential **exactly once**: ```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). * **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 authenticated request updates the key's **Last Used** timestamp, shown in the dashboard key table. 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. * 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. *** ## 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. 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 | | `401` | `API key has expired` | The key passed its expiry, or its rotation grace window lapsed | | `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` | 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 exposing 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 | | **Auth** | `Authorization: Bearer ` on every request | | **Protocol** | MCP `2025-06-18` | 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. The server is **fail-closed**: a missing or invalid key is rejected, and a tool whose required permission the key does not hold returns an error naming the missing permission (never a silent partial result). Nothing is anonymously accessible. ## Connect a client Point any MCP client at the endpoint with a bearer header. For example, a generic `mcpServers` configuration: ```json theme={null} { "mcpServers": { "visiq": { "type": "http", "url": "https://app.visiqlabs.com/api/mcp", "headers": { "Authorization": "Bearer vq_prod_..." } } } } ``` Then **start with `whoami`** to confirm which vendor and permissions your key resolves to — every other tool is gated by those permissions. ## Tool catalog The server advertises the tools below via `tools/list`. Each is permission-gated exactly like its web/API counterpart. **Discovery** * `whoami` — the vendor and effective permissions your key holds. Call this first. * `describe_chat` — how the in-product natural-language query tools work. **Action governance (rules)** * `list_rules`, `get_rule` — read action rules * `create_rule`, `update_rule`, `delete_rule` — author action rules * `compile_rule` — compile natural language into a rule (requires `allow_rules:create` **or** `recall_rules:create`) **Retrieval governance (rules)** * `list_recall_rules`, `create_recall_rule` — read and author retrieval rules **Agents** * `list_agents`, `get_agent` — inventory * `register_agent`, `update_agent` — register and manage agents across frameworks **Decisions & schemas** * `get_decision_outcomes` — decision/audit outcomes * `list_action_schemas` — the action schemas agents report **Human-in-the-loop** * `list_hitl_queue` — pending approvals * `respond_hitl` — approve or deny a queued action (authenticated approver identity) **Settings** * `get_allow_settings`, `update_allow_settings` — enforcement mode and settings **Natural-language queries** * `chat_rules_table`, `chat_events_log`, `chat_agents_inventory`, `chat_hitl_queue`, `chat_data_table` — ask questions over each surface in plain language **Key lifecycle** * `revoke_self` — revoke **this** key (no arguments, no permission required). It stops authenticating immediately, so make it your **final** call. See [self-revocation](/automation/api-keys#revoke-your-own-key-self-revocation). ## 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. # Stream logs to Datadog Source: https://docs.visiqlabs.com/connectors/datadog Continuously ship your governance decision and audit logs to your own Datadog organization. VisIQ can forward your governance **decision** and **audit** logs to your own Datadog organization as they happen, so your existing dashboards, monitors and retention policy cover AI governance the same way they cover everything else. Set it up at **Connectors → Log Streaming → Datadog**. It takes about three minutes. *** ## Before you start * A Datadog organization and permission to create an **API key** in it. * A VisIQ account with the **`settings:update`** permission — the log-destination endpoints are guarded by it. Streaming starts from the moment you save. There is **no historical backfill** — logs recorded before the destination existed stay in VisIQ's audit trail and are not replayed into Datadog. *** ## Step 1 — Create a Datadog API key In Datadog, go to **Organization Settings → API Keys → New Key** and copy the key value. An **API key** is all VisIQ needs, and it is deliberately the least-privileged option: * Log submission authenticates with the `DD-API-KEY` header and nothing else. * VisIQ never asks for an **Application key**, which is the credential that can read your Datadog data and change configuration. If you are about to paste something that starts with your user's app key, you have the wrong one. *** ## Step 2 — Pick your site Datadog runs several independent regions and your API key only works against yours. VisIQ asks you to pick it from a fixed list rather than accepting a URL: | Site | Region | | ------------------- | ----------------- | | `datadoghq.com` | US1 — the default | | `us3.datadoghq.com` | US3 | | `us5.datadoghq.com` | US5 | | `datadoghq.eu` | EU1 | | `ap1.datadoghq.com` | AP1 · Japan | | `ap2.datadoghq.com` | AP2 · Australia | | `uk1.datadoghq.com` | UK1 | | `ddog-gov.com` | US1-FED | | `us2.ddog-gov.com` | US2-FED | To find yours, match your browser's Datadog URL (`app.datadoghq.com` is US1, `us5.datadoghq.com` is US5, `app.datadoghq.eu` is EU1, and so on), or read it at the top of **My Preferences**. Note that EU1 is `datadoghq.eu` — **not** `eu1.datadoghq.com` — and US1 has no region prefix at all. ### Why a list and not a URL VisIQ **derives** every Datadog host from the site you pick and never accepts a customer-supplied URL for this connector: * logs go to `https://http-intake.logs./api/v2/logs` * the key check goes to `https://api./api/v1/validate` That removes a whole class of risk: because there is no URL field, there is nothing to point at an internal host, and a typo produces "wrong site" rather than a silent redirection of your compliance logs. *** ## Step 3 — Choose what to stream Pick at least one stream. Both are on by default. | Stream | Contents | | ------------- | -------------------------------------------------------------------- | | **Decisions** | Action, retrieval, and Human-in-the-Loop governance decision events. | | **Audit** | Platform configuration and access audit log. | You can also set an optional **source name** (Datadog's `source` attribute, e.g. `visiq`) to make the events easy to filter alongside your other log sources. *** ## Step 4 — Verify and save There is no **Test connection** button here. As soon as your key is entered and the configuration is complete, VisIQ sends a test event to your Datadog intake automatically, and **Save** unlocks once it is verified. If it fails, fix the key or the site and VisIQ re-verifies on its own. Once saved, the connector card shows delivered and failed counts so you can see the stream's health at a glance. *** ## How delivery behaves Understanding the retry model matters when you are reconciling counts. * Datadog's intake returns **202** for an accepted batch. That is what VisIQ counts as delivered. * A **bad key, wrong site, or oversized batch** is a permanent rejection. VisIQ **buffers and backs off** rather than discarding the batch, so you can fix the configuration without losing logs. * **Rate limits, timeouts, server errors and network failures** are retried. * Datadog **silently drops** logs older than roughly its 18-hour intake window while still answering 202. VisIQ will not report those as delivered: a batch that has aged past the window is counted as dropped and skipped, so a backlog can never be falsely attested as shipped. *** ## The API behind the card `GET` needs `settings:view`; `POST` needs `settings:update`. ```json POST /api/log-destinations theme={null} { "name": "Datadog — production", "type": "datadog", "datadog_site": "us5.datadoghq.com", "streams": ["decisions", "audit"], "auth_config": { "api_key": "…" }, "source_name": "visiq", "enabled": true } ``` Note the shape: Datadog carries `datadog_site` and **no** `endpoint_url`, because the host is derived. `streams` must be a non-empty subset of `decisions` and `audit`. On a later update, omitting `auth_config` keeps the stored key unchanged. *** ## Troubleshooting Almost always the wrong **site**. A key issued in EU1 does not authenticate against US1. Match the site to the Datadog URL you use in the browser, then let VisIQ re-verify. Open the destination — the recorded error names the cause. A permanent status (a rejected key, a site mismatch) means VisIQ is buffering rather than dropping: correct the setting and the buffered batches deliver on the next attempt. Datadog discards logs older than about 18 hours at intake. VisIQ counts anything that has aged past that window as dropped instead of reporting it delivered — the stream keeps moving rather than wedging on undeliverable history. Log submission takes an organization **API key**, not an Application key. Create one under Organization Settings → API Keys. *** ## Related The same streams into your own Elastic deployment, with a least-privilege API key. InsightIDR Custom Logs, where the webhook URL is the whole credential. # Stream logs to Elasticsearch Source: https://docs.visiqlabs.com/connectors/elastic Continuously ship your governance decision and audit logs to your own Elastic deployment with a least-privilege API key. VisIQ can forward your governance **decision** and **audit** logs into your own Elasticsearch deployment as they happen, indexed as ordinary log documents you can search, alert on and retain under your existing policy. Set it up at **Connectors → Log Streaming → Elasticsearch**. It takes about three minutes. *** ## Before you start * An Elasticsearch deployment reachable over **https from the public internet**, and Kibana access to create an API key. * A VisIQ account with the **`settings:update`** permission — the log-destination endpoints are guarded by it. Streaming starts from the moment you save. There is **no historical backfill** — logs recorded before the destination existed stay in VisIQ's audit trail and are not replayed into Elastic. *** ## Step 1 — Create a least-privilege API key In Kibana, go to **Stack Management → Security → API keys → Create API key**. Turn on **Control security privileges** and paste this role descriptor: ```json Elastic role descriptor theme={null} { "visiq_ingest": { "cluster": [ "monitor" ], "indices": [ { "names": [ "logs-*-*" ], "privileges": [ "auto_configure", "create_doc" ] } ] } } ``` Then copy the **Base64 encoded** value — Kibana shows it only once. This is the whole grant, and it is narrow on purpose: | Privilege | What it allows | What it cannot do | | ------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------- | | `create_doc` on `logs-*-*` | Add new log documents to your `logs-*` data streams. | Update, overwrite, read, or delete anything already in your cluster. | | `auto_configure` on `logs-*-*` | Let the data stream create itself on first write. | Touch indices outside the pattern. | | `cluster: monitor` | Let the connection check confirm the cluster is reachable. | Any write or admin operation. | If your organisation prefers to provision the key another way, the only requirement is that it can create documents under `logs-*-*`. A key with more privilege than this works but is not needed. *** ## Step 2 — Point VisIQ at your deployment Enter your **Elasticsearch endpoint URL** — the deployment's base URL, for example `https://my-deployment.es.us-central1.gcp.cloud.es.io`. Then paste the encoded API key. The endpoint has to clear two checks: * **In the browser**, a shape check: it must be `https`, and it must not carry embedded credentials (`https://user:pass@host` is refused, because such a URL would be stored in plain text and breaks the HTTP client anyway). * **On the server**, the authoritative one: the host is DNS-resolved and rejected if it lands in a private, reserved or link-local range. This runs at the route boundary **and again immediately before every single send**, so an endpoint that later resolves inward cannot become an egress path. A self-hosted cluster therefore has to be reachable at a public address; an endpoint on a private network cannot be used. **Changing the endpoint host requires re-entering your API key.** VisIQ refuses to ship a stored credential to a host it was not issued for, so editing the endpoint to a different host locks Save until you paste a fresh key and it verifies against the new host. Changing the path or port of the same host does not trigger this. *** ## Step 3 — Choose what to stream Pick at least one stream. Both are on by default. | Stream | Contents | | ------------- | -------------------------------------------------------------------- | | **Decisions** | Action, retrieval, and Human-in-the-Loop governance decision events. | | **Audit** | Platform configuration and access audit log. | You can also set an optional **dataset override** (for example `logs-visiq.decisions-default`) if you want the documents to land somewhere other than the default target. *** ## Step 4 — Verify and save There is no **Test connection** button. Once the endpoint and key are in, VisIQ writes a test event automatically and **Save** unlocks when it is verified. If it fails, correct the endpoint or the key and VisIQ re-verifies on its own. Once saved, the connector card shows delivered and failed counts. *** ## How delivery behaves VisIQ writes through Elasticsearch's `_bulk` API with `create` operations and a **deterministic document id** per event. That is what makes retries safe: a redelivered document comes back as a `409` version conflict, which VisIQ counts as already-delivered rather than inserting a duplicate. The response handling is deliberate, because `_bulk` can return `200` while individual documents failed: | Outcome | What VisIQ does | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Document accepted, or `409` conflict | Counted delivered. | | Per-document `429` | Retried — the batch is not advanced past. | | Per-document `400` (mapping or parse error) | Counted as a permanent drop, so one poison document cannot wedge the stream forever. | | Whole-request `401` / `403` / `413` | Buffered with back-off, never dropped — a fixable credential or sizing problem must not cost you logs. | | Whole-request `429` / `5xx` / network failure | Retried. | | `200` with a body VisIQ cannot parse | **Retried, not counted delivered.** An unparseable `200` usually means a proxy answered instead of Elastic, and VisIQ will not attest delivery it cannot confirm. | *** ## The API behind the card `GET` needs `settings:view`; `POST` needs `settings:update`. ```json POST /api/log-destinations theme={null} { "name": "Elastic — production", "type": "elasticsearch", "endpoint_url": "https://my-deployment.es.us-central1.gcp.cloud.es.io", "streams": ["decisions", "audit"], "auth_config": { "api_key": "…" }, "index_name": "logs-visiq.decisions-default", "enabled": true } ``` Note that the persisted type is `elasticsearch` even though the connector card reads "Elasticsearch". `streams` must be a non-empty subset of `decisions` and `audit`. On a later update, omitting `auth_config` keeps the stored key unchanged. *** ## Troubleshooting The API key is wrong, or it lacks `create_doc` on `logs-*-*`. Re-create it with the role descriptor above and make sure you copied the **encoded** value, not the key id. The host resolved to a private, reserved or link-local address. VisIQ only streams to publicly-resolvable endpoints, and it re-checks before every send rather than trusting the value that was validated at save time. You changed the host, so a fresh API key is required. This is intentional — a key issued for one cluster is never shipped to another. Check the recorded error on the destination. Documents rejected with a mapping or parse error are counted as dropped rather than retried forever; everything else is buffered and retried, so the shortfall is usually temporary. *** ## Related The same streams into Datadog, with the intake host derived from your site. InsightIDR Custom Logs, where the webhook URL is the whole credential. # Email approvals Source: https://docs.visiqlabs.com/connectors/email Set the catch-all address that receives Human-in-the-Loop approvals for agents with no registered owner. Email is the simplest connector VisIQ has: one address, no credential, about a minute of work. It is also the safety net that keeps an approval from going nowhere. Configure it at **Connectors → Human-in-the-Loop → Email**. *** ## What this address is for Email is your organisation's **catch-all**, not your only delivery path. When you connect an agentic framework or tool, you choose that agent's approval pathway and register its owner. Approvals for an agent with an owner go straight to that person. This address is where an approval lands when the agent has **no owner registered** — so an admin can see what is running, decide it, and assign someone. | Situation | Where the approval goes | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | **Claimed agent** — an owner is registered | Directly to that owner, over the pathway chosen for the agent. | | **Unclaimed agent** — no owner yet | To this catch-all address, flagged with the agent's id and reported environment so you can attribute and claim it. | The dashboard queue is always active regardless of what you configure here, so an approval is never lost because a channel is unset. *** ## Set it up 1. Open **Connectors → Human-in-the-Loop → Email**. 2. Enter the **catch-all address** — a shared mailbox such as `approvals@yourcompany.com` works well, because unclaimed-agent approvals are an admin task rather than one person's. 3. **Save.** There is no API key, no webhook, and no credential of any kind: VisIQ sends these from its own outbound mail infrastructure. The connector is treated as configured as soon as it holds a non-empty address, which is why the card flips to **Connected** immediately after saving. Once saved, **Send test** delivers a short plain-text probe — subject `VisIQ connector test`, body `✅ VisIQ connector test — this channel is wired up correctly.` — so you can confirm mail from VisIQ arrives and is not caught by a spam filter or a mailbox rule. It is deliberately **not** a rendered approval card: it carries no decision buttons and no signed links, so it proves *deliverability*, not the approval round-trip. *** ## What the email contains Each approval email renders, in order: a branded header, the rule or policy reason that routed the call to approval, an AI-generated risk summary, an action-details table, and the call-to-action links. For an unclaimed agent the message also names the agent id and the host it reported from, which is the information you need to assign an owner. | Link | What happens | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | **Approve** | The tool call runs with its full arguments. | | **Deny** | The tool never runs; the agent receives the standard block message as the tool's output. | | **Investigate** | Opens the pending item in the VisIQ dashboard — an auth-gated, read-only deep link that takes no governance action. Always present. | | **Let VisIQ decide** | Hands the pending item to VisIQ's automated reviewer. Shown only when that is available for the agent. | The links carry signed, single-use, expiring tokens. Clicking one after somebody else has already resolved the item is safe — the decision is not applied twice. *** ## The API behind the card Both endpoints require a session and the `connectors:manage` permission. | Method | Path | Body | Purpose | | -------- | ------------------------------------------ | -------------------------------------- | ------------------------------------------------ | | `PUT` | `/api/connectors/hitl-channels/email` | `{ enabled?, display_label?, config }` | Create or update the connector. | | `DELETE` | `/api/connectors/hitl-channels/email` | — | Remove the connector. | | `POST` | `/api/connectors/hitl-channels/email/test` | — | Send a test message through the saved connector. | Unlike Slack, `config` is **required** on an email upsert, and it holds exactly one field: ```json PUT /api/connectors/hitl-channels/email theme={null} { "enabled": true, "display_label": "Approvals mailbox", "config": { "address": "approvals@yourcompany.com" } } ``` The body is validated strictly: `address` must be a valid email address, and any key VisIQ does not recognise is rejected rather than ignored. `display_label` is capped at 120 characters. *** ## Troubleshooting Email is marked connected as soon as a valid address is stored — that is a statement about configuration, not about deliverability. Use **Send test** to prove delivery end to end, then check the mailbox's spam folder and any routing rules on a shared mailbox. That is the intended behaviour for an agent that has an owner registered. This address only receives approvals for agents nobody owns yet. To change where a specific agent's approvals go, set its owner and pathway on the Agents page. The approval window is capped at 120 seconds and an unanswered request fails closed, so email suits review-after-the-fact and low-volume gates better than time-critical ones. For interactive, in-the-moment approvals use [Slack](/connectors/slack). *** ## Related Interactive Approve and Deny buttons in a channel, with owner direct messages. How approvals pause a tool call, owner routing, and the timeout model. # Stream logs to Rapid7 Source: https://docs.visiqlabs.com/connectors/rapid7 Continuously ship your governance decision and audit logs to a Rapid7 InsightIDR Custom Logs event source — with no Rapid7 API key. VisIQ can forward your governance **decision** and **audit** logs to a Rapid7 InsightIDR **Custom Logs** event source, so AI governance events sit in Log Search alongside the rest of your detection estate. Set it up at **Connectors → Log Streaming → Rapid7**. It takes about three minutes, and the last step needs you to check something in Rapid7 yourself — that is deliberate, and explained below. *** ## Before you start * A Rapid7 InsightIDR account where you can create an event source. * A VisIQ account with the **`settings:update`** permission — the log-destination endpoints are guarded by it. Streaming starts from the moment you save. There is **no historical backfill** — logs recorded before the destination existed stay in VisIQ's audit trail and are not replayed into Rapid7. *** ## VisIQ never asks for a Rapid7 API key This connector has no API key field, and that is a security decision rather than a limitation. A Rapid7 platform key would be **more** privileged than this integration needs, not less. A read-only platform key can list every log in the account **together with its ingest tokens** — handing over every write credential in your estate — and an Organization key is documented as a super key across all products. A Custom Logs webhook URL is strictly narrower: it can append events to **one** event source and do nothing else. So the URL is the whole credential. Treat it exactly like a password: * It is stored **envelope-encrypted** at rest and is never returned to the browser. * The copy VisIQ keeps in its plain-text display column has **every path segment masked**, so no part of the credential reaches any surface that merely reads destination metadata. * To revoke it, generate a new Webhook URL for the event source in InsightIDR. The old one stops working immediately. *** ## Step 1 — Create the InsightIDR event source In InsightIDR: **Data Collection → Setup Event Source → Add Event Source → Add Raw Data → Custom Logs**. 1. Set the collection method to **Webhook**. 2. Name the event source. That name becomes the log name you will select in Log Search, so make it something you will recognise — `VisIQ Governance`, for example. 3. **Save**, then click **Copy** on the Webhook URL it generates. There is no key to create and nothing else to configure on the Rapid7 side. *** ## Step 2 — Paste the Webhook URL Back in VisIQ, paste the URL into **InsightIDR Webhook URL** and pick at least one stream. | Stream | Contents | | ------------- | -------------------------------------------------------------------- | | **Decisions** | Action, retrieval, and Human-in-the-Loop governance decision events. | | **Audit** | Platform configuration and access audit log. | The URL is checked before it is accepted, and again before every send: * It must be `https`. * It must not carry an embedded username or password. * Its host must be `insight.rapid7.com` or a subdomain of it — nothing else, ever. VisIQ also refuses to follow redirects, so a tampered URL cannot bounce your compliance logs to another host. Copy the URL rather than typing it. A host like `evil-insight.rapid7.com` is **not** a subdomain of the Rapid7 apex and is rejected, as is anything with the apex buried in the middle of another domain. **Do not re-paste the masked URL the interface shows you.** A configured destination displays the redacted form. It is a structurally valid Rapid7 URL, so pasting it back would look accepted — VisIQ rejects it with an explicit error instead, because saving it would silently replace a working webhook with one that can never deliver. Leave the field blank to keep the stored URL, or re-copy a real one from InsightIDR. *** ## Step 3 — Confirm the test event actually arrived This is the step other destinations do not have. Rapid7 issues **write-only** webhook URLs with no read-back, so VisIQ cannot check that an accepted event was indexed. Worse, Rapid7's sibling ingest endpoint returns a success code for a token that does not exist — which means "the POST succeeded" does not even prove the URL is real. Showing you a green *Connected* on that basis would be a lie about a compliance stream. So VisIQ says only that Rapid7 **accepted** the test event, hands you the unique marker it carried, and keeps **Save locked** until you confirm you found it: 1. In Rapid7, open **Log Search** and select the event source you created. 2. Run the query VisIQ shows you — it is of the form `event_id = ""`. 3. When the event is there, tick **I found the test event in Rapid7 Log Search**. Save unlocks. If you change the URL or the streams after confirming, VisIQ re-runs the test with a **new** marker and clears your confirmation — the old attestation was about an event that is no longer the one in flight. *** ## The API behind the card `GET` needs `settings:view`; `POST` needs `settings:update`. ```json POST /api/log-destinations theme={null} { "name": "Rapid7 — InsightIDR", "type": "rapid7", "streams": ["decisions", "audit"], "auth_config": { "webhook_url": "https://us.api.insight.rapid7.com/…" }, "enabled": true } ``` Note the shape, which differs from the other destinations: Rapid7 sends **no** `endpoint_url` at all. The URL is the credential, so it travels inside `auth_config` where it is encrypted; the endpoint value you see on list surfaces is the redacted display form VisIQ derives server-side. On a later update, omitting `auth_config` keeps the stored URL unchanged. Events are posted as newline-delimited JSON. *** ## Troubleshooting Copy the Webhook URL from InsightIDR with its **Copy** button rather than retyping it. Every Rapid7 Insight ingest host is a subdomain of `insight.rapid7.com`; a look-alike host is refused by design. Confirm you are searching the event source you just created — the name you gave it is the log name — and that the collection method is **Webhook** rather than one of the other Custom Logs methods. Do not tick the confirmation box until you have actually seen the marker; that checkbox is the only evidence VisIQ has that delivery works. For Rapid7, a passing test alone does not unlock Save. You also need at least one stream selected and the delivery confirmation ticked. That was the masked display form, not the credential. Re-copy the real URL from InsightIDR, or leave the field blank to keep streaming with the one already stored. *** ## Related The same streams into Datadog, with the intake host derived from your site. The same streams into your own Elastic deployment, with a least-privilege API key. # Slack approvals Source: https://docs.visiqlabs.com/connectors/slack Connect Slack so Human-in-the-Loop approvals arrive as an interactive message with working Approve and Deny buttons. Slack is the richest approval channel VisIQ ships: an `approval_required` decision posts a message into a channel you choose, and a reviewer resolves it with **Approve** or **Deny** without leaving Slack. Setup takes about two minutes and produces three values you paste into VisIQ. The dashboard walks you through it at **Connectors → Human-in-the-Loop → Slack**; this page is the same flow written down, plus the one setting people most often miss. **Do not skip the Interactivity Request URL.** It is the only thing that tells Slack where to send a button click. Without it, VisIQ's message still posts and the buttons still render — but pressing one never reaches VisIQ, the approval sits unresolved, and the agent's call times out and fails closed. The manifest below sets it for you; if you build the app by hand instead, set it yourself. *** ## Before you start * A Slack workspace where you can **create and install an app**. Installing grants workspace-admin OAuth consent, so if you are not an admin you will need one to approve the install. * A VisIQ account with the **`connectors:manage`** permission — the save and delete endpoints are guarded by it. * A channel for approvals. A public channel is easiest; the app can post to a public channel without being invited. *** ## Step 1 — Create the Slack app from VisIQ's manifest Go to [api.slack.com/apps](https://api.slack.com/apps?new_app=1), choose **Create New App**, and pick **From an app manifest**. Select your workspace, then paste this: ```json Slack app manifest theme={null} { "display_information": { "name": "VisIQ Approvals", "description": "Human-in-the-loop approvals from VisIQ", "background_color": "#4a154b" }, "features": { "bot_user": { "display_name": "VisIQ", "always_online": true } }, "oauth_config": { "scopes": { "bot": [ "chat:write", "chat:write.public", "users:read.email", "users:read", "im:write" ] } }, "settings": { "interactivity": { "is_enabled": true, "request_url": "https://app.visiqlabs.com/api/integrations/slack/interactions" }, "org_deploy_enabled": false, "socket_mode_enabled": false, "token_rotation_enabled": false } } ``` The manifest pre-fills the permissions **and** the Interactivity Request URL in one step, which is why it is the recommended path. **Self-hosted or non-production VisIQ?** Replace the `request_url` host with your own VisIQ origin, keeping the path exactly `/api/integrations/slack/interactions`. The dashboard's setup panel renders the correct URL for the environment you are signed in to — copy it from there rather than typing it. Create the app, then **install it to your workspace** and approve the consent screen. ### What each permission is for Every scope in the manifest is there for a specific delivery behaviour. None of them can read your channel history, your DMs, or your files. | Scope | What it does | Why VisIQ asks for it | | ------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | `chat:write` | Send messages as the VisIQ bot. | Posts each approval request into your chosen channel. | | `chat:write.public` | Post to a public channel without joining it first. | Saves you from inviting the bot to every channel by hand. | | `users:read.email` | Resolve a teammate's Slack account from their email. | Lets VisIQ DM an agent's owner directly instead of the shared channel. | | `users:read` | Read a teammate's basic Slack profile (the id and name behind an email). | Slack requires it alongside `users:read.email` — the email lookup reads the base user profile first. | | `im:write` | Open a direct-message channel with a teammate. | Lets VisIQ open the owner's DM (`conversations.open`) after resolving them by email. | Two of these are worth calling out: * **`users:read` is not optional if you want `users:read.email`.** Slack documents that the two must be requested together, so a hand-built app that asks only for `users:read.email` is incomplete. * **A missing `im:write` degrades silently.** Owner direct messages need both `users:read.email` (to find the person) and `im:write` (to open the DM). Drop either one and approvals for a claimed agent quietly fall back to the shared channel and the owner's email instead of failing loudly. VisIQ's connection test reads the granted scopes back from Slack and warns you when this is the case — it is a warning, not an error, because channel-only installs are fully supported. *** ## Step 2 — Collect the three values VisIQ needs Open **Connectors → Human-in-the-Loop → Slack** in the VisIQ dashboard and fill in these three fields. They are the only inputs the connector takes. | Field | Where to find it in Slack | What VisIQ accepts | | ------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | **Bot User OAuth Token** | **OAuth & Permissions → Bot User OAuth Token** | Must begin with `xoxb-`. Stored encrypted; never shown again. | | **Signing Secret** | **Basic Information → App Credentials → Signing Secret** (click *Show*) | At least 16 characters. Stored encrypted; never shown again. | | **Channel ID** | Open the channel → **View channel details** → copy the **Channel ID** at the bottom | The channel id itself, e.g. `C0A1B2C3D4E` — not the channel name. | For a **private** channel, invite the bot first with `/invite @VisIQ`. Public channels are already covered by `chat:write.public`. Both secrets are envelope-encrypted at rest and are never returned by any VisIQ API — the connector list endpoint projects rows to a secret-free shape. *** ## Step 3 — Run **Test connection**, then save **Test connection** is a required step on first setup, not a nicety: the Save button stays disabled until a test passes. It does four things, and the third is the one that makes the buttons work. 1. Calls Slack's `auth.test` with your bot token to confirm the token is valid, and reports the workspace name back to you. 2. Posts a real test message to your channel, so `chat:write` and channel reachability are proven by delivery rather than assumed. 3. **Reads your workspace's team id from Slack and stores it with the connector.** This is how inbound button clicks find your tenant — see below. 4. Checks that your app was granted `users:read.email` and `im:write`, and warns (without blocking) if owner direct messages will be unavailable. Two things it deliberately cannot check, and says so rather than claiming otherwise: * **The signing secret** is only format-checked. Slack signs requests *to* your Request URL, so there is no API call that validates a signing secret — it is proven live the first time someone clicks Approve or Deny. * **Whether the Interactivity Request URL is set** cannot be read back from Slack at all. Confirm it yourself in the app's **Interactivity & Shortcuts** page. This is exactly why the manifest path is recommended. Once the test passes, click **Save**. The card shows **Connected**. ### Why the team id matters An approval message posted by VisIQ carries buttons. When a reviewer clicks one, Slack posts the interaction to your Interactivity Request URL — a public endpoint that carries no VisIQ session. VisIQ therefore identifies the tenant by the **workspace team id** in Slack's payload, looks up the matching connector, decrypts *that tenant's* signing secret, and verifies the request signature before it will resolve anything. That chain only works if a team id is stored against your connector, and **Test connection is what puts it there**. A connector saved without a passing test has no team id, so every button click is answered with an opaque `401` and the approval is never recorded. *** ## How VisIQ authenticates a button click The interactions endpoint is public by design — it is a webhook, and Slack does not carry a user session. Authenticity comes from cryptography instead, in this order: 1. The raw request bytes are read **before** any parsing, because Slack signs the exact bytes it sent. 2. The workspace team id in the payload resolves the tenant's connector. 3. That connector's signing secret verifies the `X-Slack-Signature` HMAC, and a request whose timestamp is more than **5 minutes** off is rejected as a replay. 4. Only after the signature verifies does VisIQ check that the signed button value names this connector's own tenant. An unknown workspace and an invalid signature return a byte-identical `401`, so the endpoint cannot be used to discover which workspaces have a VisIQ connector. *** ## The buttons a reviewer sees | Button | What happens | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Approve** | The tool call runs with its full arguments. The Slack message is replaced with a confirmation. | | **Deny** | The tool never runs; the agent receives the standard block message as the tool's output. | | **Let VisIQ decide** | Hands the pending item to VisIQ's automated reviewer, which either resolves it or leaves it pending for a human. It can never manufacture an independent approver. This third button is added to the message **only** when automated review is enabled for the agent — otherwise it is not rendered at all. | Clicking a button on an item somebody already resolved is safe — Slack shows *"This request was already resolved"* rather than double-applying a decision. *** ## Routing approvals to an agent's owner By default approvals go to the shared channel you configured. Set `owner_email` and a `hitl_pathway` of `slack` on an agent and its approvals are direct-messaged to that person instead, with the shared channel kept as the fail-safe if the DM cannot be delivered. Owner DMs are the feature that needs `users:read.email` and `im:write`; without them the approval still arrives, just in the channel. *** ## The API behind the card The dashboard is a client of the same endpoints you can drive yourself. All three require a session and the `connectors:manage` permission. | Method | Path | Body | Purpose | | -------- | -------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------- | | `POST` | `/api/connectors/hitl-channels/slack/verify` | `{ config, secret }` | Test candidate credentials. Persists nothing. | | `PUT` | `/api/connectors/hitl-channels/slack` | `{ enabled?, display_label?, config?, secret? }` | Create or update the connector. | | `DELETE` | `/api/connectors/hitl-channels/slack` | — | Remove the connector. | | `POST` | `/api/integrations/slack/interactions` | Slack's signed payload | Slack's callback for button clicks. Public; HMAC-authenticated. | The `PUT` body is validated strictly — an unrecognised key is rejected, not ignored: ```json PUT /api/connectors/hitl-channels/slack theme={null} { "enabled": true, "display_label": "Security approvals", "config": { "team_id": "T0A1B2C3D4E", "default_channel_id": "C0A1B2C3D4E" }, "secret": { "slack_bot_token": "xoxb-…", "slack_signing_secret": "…" } } ``` Two separate conditions are checked before the row is written, and **both** answer `400`: * **A secret on first configuration.** `secret` may be omitted on a later update to keep the stored credentials, but the first save of a Slack connector is rejected unless a secret is present. * **A routing key on the effective config.** The write is refused unless the *effective* config — the stored config patched by this body — carries `team_id`, the only key an inbound Approve/Deny click is resolved by. (A legacy one-way `legacy_webhook_url` row passes the same check instead: it has no inbound click path at all, so there is nothing for a team id to route.) The dashboard fills `team_id` in for you when **Test connection** passes; if you drive the API yourself, send it. Omitting it succeeds only when a previous save already stored one — the check runs on the merged config, never on this body alone. `display_label` is capped at 120 characters. *** ## Troubleshooting Slack is not reaching VisIQ, or VisIQ cannot identify your workspace. Check, in order: the app's **Interactivity & Shortcuts** page has interactivity **on** with the Request URL pointing at your VisIQ origin's `/api/integrations/slack/interactions`; and that you ran **Test connection** successfully, which is what stores the workspace team id the callback needs. That is a warning for a **private** channel — run `/invite @VisIQ` in it. Public channels do not need the invite because `chat:write.public` covers them. The **Channel ID** field wants the id (something like `C0A1B2C3D4E`), not the `#channel-name`. Copy it from the bottom of the channel's details panel. Your installed app is missing `users:read.email`, `im:write`, or both. Re-install or re-consent the app from the current manifest to pick them up. Approvals keep working in the meantime — they deliver to the channel and the owner's email instead of a DM. The approval window is capped at 120 seconds, and an unanswered request fails closed. That ceiling is a property of the harness, not of Slack — see [Human-in-the-Loop](/rules/action/hitl) for the pause-and-resume model and the mask fallback. *** ## Related How approvals pause a tool call, what the queue contains, and the timeout model. The catch-all channel for agents that have no registered owner. # Install the Deployed Sensor Source: https://docs.visiqlabs.com/discovery/deployed-sensor Install the visiq-discovery sensor persistently on a host or across a managed fleet, with an authenticated download and a verified checksum. The **Deployed Sensor** is the `visiq-discovery` binary installed to stay: it lives on the host, re-scans on a schedule, and reports what it finds to your Discover dashboard. It is the recommended way to run [Discovery](/discovery/introduction) — on a single machine or across a managed fleet. It is distinct from an **On-Demand Scan**, which runs the same binary once through a device-management tool's own execution channel and leaves nothing installed. The mnemonic: *the sensor stays, the scan vanishes.* *** ## Before you start * A **harness key** for your tenant (**Settings → Harness Keys**, or the Connectors page). The same key both authorizes the download and links the scan's findings to your tenant. Without a key the sensor still runs, but only prints a local report. * Administrative access on the target host — root on Linux and macOS, an elevated shell or SYSTEM on Windows. **The binary is not code-signed yet.** Apple notarization and Windows Authenticode signing are being provisioned. Until they land the **SHA-256 checksum is the integrity control**, which is why every command below verifies the digest *before* executing the binary — never skip that step. On Defender-hardened Windows fleets you may also need to allowlist the binary by SHA-256 or path, or grant it managed-installer trust: the opt-in ASR prevalence rule and Defender cloud reputation can block a fresh binary regardless of signature. (This is not SmartScreen, which does not fire for a SYSTEM-context download — and Authenticode signing alone would not satisfy the prevalence rule either.) *** ## Install on one host The dashboard generates this command with your key already spliced in — copy it from **Connectors → Discovery Agent** rather than transcribing it. The shape is: ```bash Linux / macOS theme={null} export VISIQ_API_KEY="" sh <<'VISIQ_INSTALL' set -eu # Registers what this host discovers to your VisIQ tenant. Everything between # the markers is plain text to your shell, read by sh - so a failed integrity # check aborts the INSTALL, not your terminal session. # Resolve THIS host's release slug; refuse to run the wrong platform's command. case "$(uname -s)-$(uname -m)" in Linux-x86_64) plat=linux-x64 ;; Linux-aarch64|Linux-arm64) plat=linux-arm64 ;; Darwin-arm64) plat=darwin-arm64 ;; Darwin-x86_64) plat=darwin-x64 ;; *) echo "VisIQ: unsupported platform $(uname -s)/$(uname -m) — aborting" >&2; exit 1 ;; esac # Resolve a short-lived download URL + its integrity digest (your tenant key authorizes it) hdr="$(mktemp)"; trap 'rm -f "$hdr"' EXIT url=$(curl -fsSL -D "$hdr" -H "Authorization: Bearer $VISIQ_API_KEY" "https://api.visiqlabs.com/api/discovery/download/$plat") sha=$(awk 'tolower($1)=="x-visiq-sha256:"{gsub(/\r/,"",$2);print $2}' "$hdr"); rm -f "$hdr" # FAIL CLOSED: the locator 503s rather than answer without a digest, so a missing # URL or digest means the response was tampered with — never fetch or run unverified. case "$url" in https://*) ;; *) echo "VisIQ: locator did not return an https download URL — aborting" >&2; exit 1;; esac [ -n "$sha" ] || { echo "VisIQ: no integrity digest (X-Visiq-Sha256 missing) — aborting" >&2; exit 1; } curl -fsSL -o visiq-discovery "$url" # Verify the binary BEFORE running it; abort on a checksum mismatch. # Pick ONE sha256 tool up front, then run exactly one check. if command -v sha256sum >/dev/null 2>&1; then printf '%s %s\n' "$sha" visiq-discovery | sha256sum -c - || { echo "VisIQ: checksum verification FAILED — aborting" >&2; exit 1; } elif command -v shasum >/dev/null 2>&1; then printf '%s %s\n' "$sha" visiq-discovery | shasum -a 256 -c - || { echo "VisIQ: checksum verification FAILED — aborting" >&2; exit 1; } else echo "VisIQ: no sha256 tool (sha256sum/shasum) available — aborting" >&2; exit 1 fi chmod +x visiq-discovery ./visiq-discovery scan VISIQ_INSTALL ``` The verification **fails closed**: it aborts if the locator returns no digest, if the download URL is not `https:`, if no SHA-256 tool is present, or if the bytes do not match. A `200` from the locator always carries the digest — it answers `503` rather than serve without one — so a 200 with no digest means the response was tampered with. On **macOS** add `xattr -d com.apple.quarantine visiq-discovery 2>/dev/null || true` before `chmod`. A binary fetched with `curl` never carries the quarantine attribute, so Gatekeeper does not block it — the command strips the attribute defensively anyway, which matters if the file ever arrives by another route. On **Windows**, use the PowerShell form the dashboard generates. It enables TLS 1.2 first (Windows PowerShell 5.1 on older images omits it and every HTTPS call fails before anything else), passes `-UseBasicParsing` (required under the SYSTEM/Server Core hosts that device-management tools run), verifies the digest with `Get-FileHash`, and runs `Unblock-File` before the scan. **Pass the key as an environment variable, never as a command-line flag.** Anything on the command line is visible in the process list and lands in shell history. Every command VisIQ generates exports `VISIQ_API_KEY`; there is no `--key` option, on purpose. Confirm it worked: `./visiq-discovery --version` prints the sensor version, and a successful tenant report ends with `visiq-discovery: reported to tenant (id …)`. If that line is absent the scan stayed local — the key did not reach the process. *** ## Pick the right platform The download path ends in a platform slug. Requesting one VisIQ does not publish returns `400` with the valid list. | Slug | Binary | | -------------- | --------------------------------- | | `linux-x64` | `visiq-discovery-linux-x64` | | `linux-arm64` | `visiq-discovery-linux-arm64` | | `darwin-x64` | `visiq-discovery-darwin-x64` | | `darwin-arm64` | `visiq-discovery-darwin-arm64` | | `windows-x64` | `visiq-discovery-windows-x64.exe` | *** ## Roll it out to a fleet The same command hardens into a script your device-management console pushes. It installs to a stable path, runs as root or SYSTEM, and is **idempotent** — safe to re-run on every check-in, because it downloads only when the binary is absent and re-scans every time. The dashboard carries console-specific walkthroughs for Microsoft Intune, Jamf Pro, Kandji, Group Policy, Configuration Manager, Workspace ONE, Ansible, JumpCloud and NinjaOne, each with the exact field to paste into and that console's own pitfalls. Two rules hold across all of them: * **Assign to a device group, never a user group.** A root/SYSTEM install targeted at users silently no-ops. * **Exit 0 and stay idempotent.** Consoles read a non-zero exit as a failed deployment and will retry. ### Microsoft Intune, the persistent way On Windows the robust path is a **Win32 managed app** — the app model is what makes the sensor persistent and self-healing, rather than a one-shot script: 1. **Package it.** Wrap `install.ps1` and `uninstall.ps1` with the Microsoft Win32 Content Prep Tool (`IntuneWinAppUtil.exe`) into a single `.intunewin`. 2. **Create the Win32 app** under **Apps → Windows → Add → Windows app (Win32)**. Install command `powershell.exe -ExecutionPolicy Bypass -File install.ps1`, uninstall command the same with `uninstall.ps1`, and on the Program page set **Install behavior = System**. 3. **Add a detection rule** — for example the presence of the run-scan wrapper under `%ProgramData%\VisIQ\discovery`, or the *VisIQ Discovery Sensor* scheduled task. Without one, Intune re-offers the installer in a roughly 24-hour loop. 4. **Assign it to an Entra device security group as Required**, starting with a pilot ring. The sensor installs on the next device check-in and self-heals on every scheduled scan after that. The `.intunewin` should be Authenticode-signed or managed-installer-trusted before a broad rollout — see the signing caveat above. For **macOS and Linux**, Intune runs the bootstrap script instead, and the details differ per platform in ways that bite: | Platform | Where it lives in Intune | Run context | Cadence | | ------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | **Windows** (script path) | Devices → Manage devices → Scripts and remediations → Platform scripts → Windows 10 and later | Set *Run this script using the logged on credentials* = **No**. The default is Yes, so you must change it. | Runs once, with retries — not on every check-in. | | **macOS** | Devices → macOS → Manage devices → Scripts (its own node, **not** under Platform scripts) | *Run script as signed-in user* = No is already the default, so it runs as root. | Runs once unless you set a script frequency. Check-in is roughly every 8 hours. | | **Linux** | Devices → Manage devices → Scripts and remediations → Platform scripts → Linux | Set *Execution context* = **Root**. The default is User, which only runs when somebody signs in. | Recurring only — the default frequency is every 15 minutes, so idempotency matters. | Both script forms must begin with a `#!` shebang on macOS and Linux. Intune's Linux support is desktop-only and narrow — corporate-enrolled Ubuntu Desktop and RHEL on x86-64, GNOME, no servers and no ARM. For Linux servers or an agentic-AI fleet, install the Deployed Sensor directly with a configuration-management tool instead. *** ## How the authenticated download works The binary lives in a **private bucket**. There is no public download and no public GitHub release — the same pattern commercial endpoint vendors use. ```http theme={null} GET /api/discovery/download/ Authorization: Bearer ``` A successful call returns **`200` with a short-lived pre-signed URL as the plain-text body**, plus these headers: | Header | Meaning | | ------------------------- | ---------------------------------------------------------------------------------- | | `X-Visiq-Sha256` | The digest of the binary the URL will serve. Verify against this before executing. | | `X-Visiq-Filename` | The released asset name for the platform. | | `X-Visiq-Version` | The exact version being served. | | `X-Visiq-Release-Channel` | The channel the version was resolved from. | | `X-Visiq-Url-Ttl-Seconds` | How long the pre-signed URL stays valid. | The heavy bytes come straight from object storage and are never proxied through VisIQ, and your `Authorization` header is never forwarded to the storage host — the pre-signed URL is the only credential it sees. **The digest and the bytes cannot disagree.** VisIQ resolves the channel pointer to a version, then reads both the checksum manifest and the binary from that *immutable* version prefix. A release published mid-download cannot leave you verifying one build and running another. Two optional, validated query parameters let a self-updating agent fetch exactly what its policy targets: * `?version=` — pin an immutable version. * `?channel=` — take a specific channel's head instead of the default. Anything path-unsafe is rejected with a `400` rather than reaching storage. | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------- | | `200` | Pre-signed URL in the body, digest in the headers. | | `400` | Unknown platform, or an invalid `version` / `channel`. | | `401` | Missing or invalid harness key. | | `503` | The release could not be resolved. VisIQ fails closed here — it never returns a fabricated or partial URL. | *** ## What happens after the scan The sensor performs a read-only scan and posts one structured report per host to your tenant, where it appears under **Discover → Findings**. Re-run `visiq-discovery scan` any time to refresh a host's inventory; a fleet install does this on its own schedule. See [Discovery](/discovery/introduction) for what each scanner detects and the read-only guarantees the sensor holds to. # Discovery Source: https://docs.visiqlabs.com/discovery/introduction Discover every custom AI agent, MCP server, AI framework, and tool in your environment — plus local models, coding agents, and shadow AI — before you decide what to govern. Discovery is evolving — the sensor and findings model are still changing, and surfaces may change between releases. You can't govern what you can't see. Discovery answers the first question of any AI governance program — *what agentic AI is actually running in my environment?* — with a lightweight endpoint sensor that scans each host and reports what it finds to your Discover dashboard. *** ## What the sensor finds Each scan runs seven scanners and emits one structured report: | Scanner | What it detects | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Agent frameworks** | npm and pip installs classified against a curated agentic taxonomy — orchestrators (LangGraph, CrewAI, AutoGen, Haystack, DSPy, smolagents), agent SDKs (OpenAI Agents SDK, Mastra, Pydantic AI, Claude Agent SDK, LlamaIndex Agents), agent runtimes, memory backends, and tool routers. | | **MCP servers** | Servers declared in client configs (Claude Desktop, Cursor, Windsurf, VS Code, Zed) and visible in the process list, with their tool surface read by static introspection. | | **Local model runtimes** | The Ollama daemon and binary, plus HTTP probes for LM Studio, vLLM, llama.cpp, GPT4All, and text-generation-webui. | | **Coding & CLI agents** | Claude Code, Cursor, Windsurf, Aider, and OpenClaw installed on the host. | | **Shadow AI** | Stray provider API keys in environment files (OpenAI, Anthropic, Google, Groq, Hugging Face), AI browser extensions, and AI IDE extensions and plugins. | | **Data-store reachability** | Which vector stores (Chroma, Pinecone, Weaviate, Qdrant, Milvus, LanceDB, FAISS), SQL and NoSQL databases, caches, and object stores the host's code can reach — from SDK dependencies, config files, and connection-string environment variables. | | **Code projects** | Which individual code projects on the host build agents, and which of those already have the VisIQ harness installed — so governance coverage is measured per project, not just per machine. This is the one scanner that looks inside developer directories; see [Code-project scanning](#code-project-scanning) below for exactly what it reads and reports. | The framework taxonomy separates signal from noise: a bare LLM SDK or a local model runtime only counts as agentic when it's linked to a genuine agent anchor on the same host, so a plain RAG dependency doesn't light up your fleet as autonomous agents. Static scanning of model artifacts and pickle opcodes is **not** part of any scan. It is deferred roadmap work, and the scanner is unwired from the sensor — it never runs. *** ## Code-project scanning The **Code projects** scanner is the only one that traverses developer directories, so it is worth being precise about what it does. It is **on by default** and can be turned off per organization (**Settings → Discovery → Project-level governance detection**) or at deploy time. **Where it looks.** Under each scanned user's home directory: the conventional code roots `code`, `git`, `work`, `projects`, `src`, `dev`, `repos`, `Developer`, and `www` (to a depth of 6), plus the home directory itself (to a depth of 4). The traversal is bounded — at most 12,000 directories per scan and a 20-second deadline — and stops early rather than running long. If it runs out of budget, the report records the truncation instead of silently claiming full coverage. **What it reads.** Dependency manifests only — `package.json`, `requirements.txt`, `pyproject.toml`, `go.mod`, `Gemfile`, `Cargo.toml`, `pom.xml`, Gradle files — and the installed package metadata beside them. **Your source code is never read, parsed, or transmitted.** **What it reports.** A project is included only if it has at least one anchor-class agent framework or the VisIQ harness. A project that merely imports an LLM SDK is not reported at all. For each included project the sensor sends: * the project directory path and the paths of the manifests it matched, * the ecosystems in use (npm, pip, maven, gradle, go, gem, cargo), * **only** the dependency names that match the agentic-framework taxonomy, and **only** the VisIQ harness package names — no other dependency name is ever serialized, * whether the VisIQ harness is declared and whether it is actually installed, * the owning local account (uid/SID and username) and install scope. **Turning it off.** Toggle **Project-level governance detection** off in Discovery settings, or set `VISIQ_PROJECT_SCANNING=false` in the sensor's environment at deploy time — the environment variable is a hard opt-out that wins over the tenant setting. One caveat worth knowing: because this scanner is read-only detection, the sensor resolves an unreachable or unanswered settings lookup as **enabled**, the opposite of the harness auto-install gate, which fails closed. If you need the setting to hold on an endpoint regardless of connectivity, use the environment variable rather than the tenant toggle. *** ## Read-only by design The sensor observes; it never runs what it finds. * **Discovered MCP servers are never executed.** Their tool surface is read by static analysis of the on-disk package. When a server's tools can't be read statically (HTTP transport, dynamic registration, minified code), it's reported as an *unreadable tool surface* — a visibility finding — rather than launched to find out. * **Package managers are never invoked.** Framework detection is a static walk of `node_modules` and Python `site-packages` directories — no `npm` or `pip` subprocesses. * **Secrets never leave the host.** A detected API key is reported as a SHA-256 fingerprint of its value, never the key itself. * **Coverage is honest.** The report records per-scanner coverage, so "zero findings" is distinguishable from "couldn't look." A degraded scan surfaces as *Needs coverage* — it never silently reads as safe. *** ## Two ways to deploy The sensor ships as a single self-contained binary per platform — Linux (x64, arm64), macOS (Intel, Apple silicon), and Windows (x64) — with zero runtime dependencies. No Node, Python, or agent framework is required on the endpoint. **Recommended.** The sensor installed persistently as a managed app through your device-management platform. It scans on the schedule you set in Discovery settings and can update itself in place with `visiq-discovery update`. **[Install it →](/discovery/deployed-sensor)** The same binary run once through your existing endpoint tooling. Nothing stays installed on the host, and the scan schedule is whatever your own tooling defines. Both approaches deliver the identical payload and produce the identical report. To get started, open **Integration → Connectors** in the dashboard and pick the **VisIQ Discovery Sensor** card — it's included with every plan and offers a fleet-rollout walkthrough, a single-host CLI install, or a copy-paste prompt your coding agent can run for you. The generated commands resolve a short-lived, authenticated download URL for your platform and verify the binary's published SHA-256 digest before it ever runs. The sensor authenticates to your tenant with a harness key: with `VISIQ_API_KEY` and `VISIQ_ENDPOINT` set it reports each scan to your dashboard; without them (or with `--no-report`) it runs local-only and prints the JSON report. *** ## Where results land Scan results power the **Discover** section in the dashboard sidebar: * **Reach Map** — every agentic surface in your fleet, arranged by how far VisIQ governance can reach it today. * **Inventory** — one row per scanned host, with the custom AI agents, MCP servers, AI frameworks, and tools found on each. * **Findings** — one row per detected risk factor per host, with severity, evidence, and remediation status. *** ## How risk is scored Every host gets a 0–100 risk score from an **additive point budget**: each exposure signal the sensor looks for is a catalog factor with a fixed weight, the weights sum to exactly 100, and a host's score is simply the sum of the factors detected on it. No opaque formula — the score always reconciles against the factor grid you see in the dashboard. | Factor | Dimension | Points | | ------------------------------------------------------ | -------------- | ------ | | Live tool surface (MCP tools the agent can invoke) | Action surface | 30 | | Agentic host (an agent framework installed and active) | Autonomy | 18 | | Coding / CLI agent | Action surface | 14 | | Running agent (live right now, not latent) | Autonomy | 14 | | Framework concentration (3+ frameworks stacked) | Reach | 10 | | Unreadable tool surface | Visibility | 8 | | Incomplete coverage | Visibility | 6 | Scores band as **Low** (0–39), **Medium** (40–64), **High** (65–84), and **Critical** (85–100). A host the sensor couldn't fully assess lands in a fifth lane, **Needs coverage**, which sorts above Low — degraded visibility is itself a finding, never a clean bill of health. Findings map 1:1 to these factors, and remediation is derived automatically from scan history: when a factor stops appearing in a host's latest scan, its finding flips to *remediated* on its own. Nobody has to close tickets by hand. *** ## Settings Discovery is configured per organization under **Settings → Discovery**: * **Auto-install harness plugin** — off by default. When enabled, deployed sensors may automatically install the VisIQ harness plugin into supported agent tools they detect (such as OpenClaw), wiring up runtime governance with no manual step. VisIQ never modifies your agents unless you opt in. * **Project-level governance detection** — **on by default.** Enables the Code projects scanner described in [Code-project scanning](#code-project-scanning): a bounded walk of conventional developer directories that reads dependency manifests (never source code) to report which projects build agents and which already have the VisIQ harness. Turn it off here, or set `VISIQ_PROJECT_SCANNING=false` at deploy time for an opt-out that does not depend on the endpoint reaching the control plane. * **Scan frequency** — how often the Deployed Sensor scans each endpoint: hourly, every 6 hours, every 12 hours, daily (the default), or weekly. This governs only the Deployed Sensor; an On-Demand Scan runs on your own tooling's schedule. * **Harness status** — how many discovery-installed harnesses are registered for your tenant and when one last reported. *** ## Next steps Put the agents Discovery found under governance with one `visiq()` call. How tool-call authorization works once an agent is harnessed. # API Examples — Notebooks Source: https://docs.visiqlabs.com/examples/notebooks Runnable Jupyter notebooks that do real data science over your governance data — including an independent, from-scratch re-verification of the transparency log. Two standalone, **off-platform** notebooks built on the VisIQ management API — the kind of cross-cutting, statistical, cryptographic analysis you would deliberately *not* bake into the product UI. They run against the read-only **sandbox showcase** org, so you can explore real seeded governance data (or, for the transparency notebook, run with **no credentials at all**). Re-derive VisIQ's transparency log **from scratch** — recompute every Merkle root, hash-chain link and Ed25519 signature yourself, trusting none of the product's own flags. **Runs with zero credentials** (demo mode). Exploratory data science over the harness corpus — a business-function atlas, a rule-attribution meta-audit, an egress graph and volume seasonality. Needs a sandbox-reader key. ## Run them A fully static **JupyterLite** build runs the notebooks entirely in your browser via Pyodide (WASM Python) — no install, no server, no account, and for the transparency notebook **no credentials**. It is live below — give it a few seconds to boot, then choose **Run → Run All Cells** to re-derive every Merkle root, hash-chain link and Ed25519 signature yourself: