> For the complete documentation index, see [llms.txt](https://help.nightfall.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.nightfall.ai/developer-api/nightfall_apis/ai-governance-apis.md).

# AI Governance API's

## List MCP servers

> List the inventory of MCP servers, aggregated per server (one row per fingerprint + version) with device counts, usage metrics, and risk.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"McpServer":{"type":"object","properties":{"id":{"type":"string","description":"Opaque server id. Use this value in path parameters; it encodes the (fingerprint, version) key."},"serverId":{"type":"string","description":"The server fingerprint (e.g. \"npm:@scope/pkg\", \"oci:image:tag\", \"url:domain\")."},"canonicalName":{"type":"string"},"version":{"type":"string"},"transport":{"type":"string","description":"Transport mechanism (stdio, http, sse, etc.)"},"risk":{"type":"integer"},"riskLabel":{"type":"string"},"isShadow":{"type":"boolean","description":"True when the server is not associated with any known client."},"remoteUrl":{"type":"string"},"clients":{"type":"array","description":"The AI clients this server is configured in, normalized to the `AiClient` vocabulary and de-duplicated — one value per client however the agent spelled it.","items":{"$ref":"#/components/schemas/AiClient"}},"unrecognizedClients":{"type":"array","description":"Names observed against this server that are not a known AI client, verbatim. On this inventory they are mostly process names seen spawning a server — `xpcproxy`, `com.docker.backend`, `iTerm2`, `node-<hash>` — kept here rather than folded into the vocabulary or dropped. Omitted when empty.","items":{"type":"string"}},"provenance":{"type":"string","description":"Provenance classification. UNKNOWN means the server's identity did not resolve to a registry entry, and is also the value for a row whose classification has not run yet — it is never reported as an empty value. FIRST_PARTY appears only where an administrator has set it.","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"provenanceOverridden":{"type":"boolean","description":"True when an operator has pinned the provenance, rather than it being derived."},"riskOverridden":{"type":"boolean","description":"True when an operator has pinned the risk level, rather than it being derived."},"isManaged":{"type":"boolean","description":"True when the server is declared by an MDM/enterprise-managed config file."},"deviceCount":{"type":"integer"},"clientCount":{"type":"integer","description":"Number of distinct clients that have used this server — always equal to `clients.length + unrecognizedClients.length`, since both are counted after spelling normalization."},"totalSpawns":{"type":"integer"},"totalNetworkCalls":{"type":"integer"},"totalRequestBytes":{"type":"integer"},"totalResponseBytes":{"type":"integer"},"firstSeen":{"type":"integer","description":"Unix timestamp (seconds) of first observation."},"lastUsed":{"type":"integer","description":"Unix timestamp (seconds) of last activity. Omitted when never used."},"removedAt":{"type":"integer","description":"Unix timestamp (seconds) at which the server's declaration was found to have disappeared from its config file. Omitted while the server is still declared — presence is signalled by the field's absence, not by a zero value. Only ever populated when the request set includeRemoved=true."},"registry":{"$ref":"#/components/schemas/McpRegistryMetadata"}}},"McpRegistryMetadata":{"type":"object","description":"Public registry metadata; only populated on the single-server detail endpoint.","properties":{"name":{"type":"string"},"description":{"type":"string"},"isOfficial":{"type":"boolean"},"isVerified":{"type":"boolean"},"popularityScore":{"type":"integer"},"repositoryUrl":{"type":"string"},"websiteUrl":{"type":"string"},"license":{"type":"string"},"packageName":{"type":"string"},"packageRegistry":{"type":"string","description":"Source registry (npm, pypi, oci, etc.)"}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/mcp-servers":{"get":{"summary":"List MCP servers","description":"List the inventory of MCP servers, aggregated per server (one row per fingerprint + version) with device counts, usage metrics, and risk.","parameters":[{"name":"limit","in":"query","description":"Maximum number of records to return (default 20, max 100)","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","description":"Opaque pagination cursor returned as nextPageToken in a prior response","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort order (default DEVICE_COUNT_DESC)","required":false,"schema":{"type":"string","enum":["DEVICE_COUNT_DESC","DEVICE_COUNT_ASC","NAME_ASC","NAME_DESC","RISK_DESC","RISK_ASC","LAST_USED_DESC","LAST_USED_ASC"]}},{"name":"search","in":"query","description":"Case-insensitive match on the server name","required":false,"schema":{"type":"string","maxLength":255}},{"name":"transport","in":"query","description":"Exact match on transport (e.g. stdio, http, sse)","required":false,"schema":{"type":"string","maxLength":64}},{"name":"client","in":"query","description":"Filter to these AI clients. Repeatable, max 20. Uses the normalized vocabulary in `AiClient`, not the raw name an agent reported — one value matches every spelling of that client, so `CLAUDE_CODE` covers rows stored as \"Claude Code\", \"claude_code\" and \"claudecode\" alike. A value outside the enum is rejected rather than ignored, so a typo cannot silently widen the result to everything.","required":false,"schema":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/AiClient"}}},{"name":"riskLevel","in":"query","description":"Filter to these risk levels. Repeatable.","required":false,"schema":{"type":"array","items":{"type":"string","enum":["KNOWN","LOW","MEDIUM","HIGH","CRITICAL"]}}},{"name":"minLastActiveAt","in":"query","description":"Unix timestamp in seconds; exclude servers not active since this time","required":false,"schema":{"type":"integer"}},{"name":"provenance","in":"query","description":"Filter to these provenance classifications. Repeatable. OFFICIAL is a server whose registry identity resolves to the service vendor that publishes it. COMMUNITY resolves to a registry entry that is not vendor-published (open-source, third party). UNKNOWN did not resolve to a registry identity at all, which is the interesting case for shadow AI. FIRST_PARTY means an administrator has explicitly classified the server as customer-internal — it is never derived automatically, so it only appears where someone has set it. UNKNOWN is a filterable value, not the absence of a filter.","required":false,"schema":{"type":"array","maxItems":4,"items":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]}}},{"name":"managed","in":"query","description":"Restrict to servers declared by an MDM/enterprise-managed config file (MANAGED) or by a user-writable one (UNMANAGED). Omit to return both.","required":false,"schema":{"type":"string","enum":["MANAGED","UNMANAGED"]}},{"name":"includeRemoved","in":"query","description":"Include servers whose declaration has disappeared from its config file. Such rows carry `removedAt` and keep the state they were in when they disappeared. Defaults to false, and `total` moves with it. A `pageToken` is only valid for the filter set that produced it: changing this value (or any other filter) mid-pagination silently restarts from the first page with a freshly recomputed `total`, rather than returning an error. Finish a pagination run before changing filters. Has no effect on tenants where removal reconciliation is not yet enabled.","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Successful response","headers":{"X-Rate-Limit-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next second before being throttled"}},"X-Quota-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next quota period"}},"X-Quota-Period-End":{"schema":{"type":"string","format":"date-time","description":"When the current quota period expires"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"mcpServers":{"type":"array","items":{"$ref":"#/components/schemas/McpServer"}},"total":{"type":"integer","description":"Total matching servers (present on the first page)"},"nextPageToken":{"type":"string","description":"Opaque cursor for the next page; omitted when there are no more results"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate Limit Exceeded or Daily Quota Exceeded","headers":{"Retry-After":{"schema":{"type":"integer","description":"Seconds after which to retry the request"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## Get an MCP server

> Fetch a single MCP server by its opaque id, including registry metadata when available.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"McpServer":{"type":"object","properties":{"id":{"type":"string","description":"Opaque server id. Use this value in path parameters; it encodes the (fingerprint, version) key."},"serverId":{"type":"string","description":"The server fingerprint (e.g. \"npm:@scope/pkg\", \"oci:image:tag\", \"url:domain\")."},"canonicalName":{"type":"string"},"version":{"type":"string"},"transport":{"type":"string","description":"Transport mechanism (stdio, http, sse, etc.)"},"risk":{"type":"integer"},"riskLabel":{"type":"string"},"isShadow":{"type":"boolean","description":"True when the server is not associated with any known client."},"remoteUrl":{"type":"string"},"clients":{"type":"array","description":"The AI clients this server is configured in, normalized to the `AiClient` vocabulary and de-duplicated — one value per client however the agent spelled it.","items":{"$ref":"#/components/schemas/AiClient"}},"unrecognizedClients":{"type":"array","description":"Names observed against this server that are not a known AI client, verbatim. On this inventory they are mostly process names seen spawning a server — `xpcproxy`, `com.docker.backend`, `iTerm2`, `node-<hash>` — kept here rather than folded into the vocabulary or dropped. Omitted when empty.","items":{"type":"string"}},"provenance":{"type":"string","description":"Provenance classification. UNKNOWN means the server's identity did not resolve to a registry entry, and is also the value for a row whose classification has not run yet — it is never reported as an empty value. FIRST_PARTY appears only where an administrator has set it.","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"provenanceOverridden":{"type":"boolean","description":"True when an operator has pinned the provenance, rather than it being derived."},"riskOverridden":{"type":"boolean","description":"True when an operator has pinned the risk level, rather than it being derived."},"isManaged":{"type":"boolean","description":"True when the server is declared by an MDM/enterprise-managed config file."},"deviceCount":{"type":"integer"},"clientCount":{"type":"integer","description":"Number of distinct clients that have used this server — always equal to `clients.length + unrecognizedClients.length`, since both are counted after spelling normalization."},"totalSpawns":{"type":"integer"},"totalNetworkCalls":{"type":"integer"},"totalRequestBytes":{"type":"integer"},"totalResponseBytes":{"type":"integer"},"firstSeen":{"type":"integer","description":"Unix timestamp (seconds) of first observation."},"lastUsed":{"type":"integer","description":"Unix timestamp (seconds) of last activity. Omitted when never used."},"removedAt":{"type":"integer","description":"Unix timestamp (seconds) at which the server's declaration was found to have disappeared from its config file. Omitted while the server is still declared — presence is signalled by the field's absence, not by a zero value. Only ever populated when the request set includeRemoved=true."},"registry":{"$ref":"#/components/schemas/McpRegistryMetadata"}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"McpRegistryMetadata":{"type":"object","description":"Public registry metadata; only populated on the single-server detail endpoint.","properties":{"name":{"type":"string"},"description":{"type":"string"},"isOfficial":{"type":"boolean"},"isVerified":{"type":"boolean"},"popularityScore":{"type":"integer"},"repositoryUrl":{"type":"string"},"websiteUrl":{"type":"string"},"license":{"type":"string"},"packageName":{"type":"string"},"packageRegistry":{"type":"string","description":"Source registry (npm, pypi, oci, etc.)"}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/mcp-servers/{serverId}":{"get":{"summary":"Get an MCP server","description":"Fetch a single MCP server by its opaque id, including registry metadata when available.","parameters":[{"name":"serverId","in":"path","required":true,"description":"Opaque MCP server id from a list response","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","headers":{"X-Rate-Limit-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next second before being throttled"}},"X-Quota-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next quota period"}},"X-Quota-Period-End":{"schema":{"type":"string","format":"date-time","description":"When the current quota period expires"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServer"}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"MCP server does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate Limit Exceeded or Daily Quota Exceeded","headers":{"Retry-After":{"schema":{"type":"integer","description":"Seconds after which to retry the request"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List devices for an MCP server

> List the devices using a specific MCP server, with a per-client usage breakdown.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"McpServerDevice":{"type":"object","description":"One device using a server. Client attribution is per-entry in clientBreakdown; the launch command line is deliberately not exposed.","properties":{"deviceId":{"type":"string"},"userEmail":{"type":"string"},"userName":{"type":"string"},"machineName":{"type":"string"},"totalSpawns":{"type":"integer"},"totalNetworkCalls":{"type":"integer"},"totalRequestBytes":{"type":"integer"},"totalResponseBytes":{"type":"integer"},"lastUsed":{"type":"integer","description":"Unix timestamp (seconds) of last activity on this device."},"clientBreakdown":{"type":"array","items":{"$ref":"#/components/schemas/McpServerDeviceClient"}}}},"McpServerDeviceClient":{"type":"object","properties":{"client":{"$ref":"#/components/schemas/AiClient"},"unrecognizedClient":{"type":"string","description":"The reported client name, verbatim, when `client` is UNKNOWN. Omitted otherwise."},"configuredName":{"type":"string"},"stdioTimesSpawned":{"type":"integer"},"timesNetworkCalled":{"type":"integer"},"lastUsedAt":{"type":"integer","description":"Unix timestamp (seconds) of last activity for this client."},"isShadow":{"type":"boolean"}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/mcp-servers/{serverId}/devices":{"get":{"summary":"List devices for an MCP server","description":"List the devices using a specific MCP server, with a per-client usage breakdown.","parameters":[{"name":"serverId","in":"path","required":true,"description":"Opaque MCP server id from a list response","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort order (default LAST_USED_DESC)","required":false,"schema":{"type":"string","enum":["LAST_USED_DESC","LAST_USED_ASC","DEVICE_ID_ASC"]}},{"name":"search","in":"query","description":"Match on device id, machine name, user name, or user email","required":false,"schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"Successful response","headers":{"X-Rate-Limit-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next second before being throttled"}},"X-Quota-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next quota period"}},"X-Quota-Period-End":{"schema":{"type":"string","format":"date-time","description":"When the current quota period expires"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/McpServerDevice"}},"total":{"type":"integer"},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate Limit Exceeded or Daily Quota Exceeded","headers":{"Retry-After":{"schema":{"type":"integer","description":"Seconds after which to retry the request"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List devices with MCP servers

> List devices that have any MCP server installed, aggregated per device.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"McpDevice":{"type":"object","properties":{"deviceId":{"type":"string"},"serverCount":{"type":"integer"},"hookCount":{"type":"integer","description":"Number of AI agent hooks configured on the device. Excludes marketplace-catalog hooks, which are advertised by a plugin catalog but not installed on the machine."},"pluginCount":{"type":"integer","description":"Number of AI agent plugins on the device, including plugins known only from a marketplace catalog entry."},"extensionCount":{"type":"integer","description":"Number of distinct installed (extensionId, version) pairs on the device. Equals the `total` of GET /ai-governance/v1/ide-extensions?deviceId=<deviceId> with no other filter, so the badge and that drill-down always agree."},"totalSpawns":{"type":"integer"},"totalNetworkCalls":{"type":"integer"},"lastActive":{"type":"integer","description":"Unix timestamp (seconds) of last activity across all servers."},"firstSeen":{"type":"integer","description":"Unix timestamp (seconds) of first observation across all servers."},"machineName":{"type":"string"},"os":{"type":"string"},"osVersion":{"type":"string"},"userEmail":{"type":"string"},"agentStatus":{"type":"string"}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/devices":{"get":{"summary":"List devices with MCP servers","description":"List devices that have any MCP server installed, aggregated per device.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort order (default SERVER_COUNT_DESC)","required":false,"schema":{"type":"string","enum":["SERVER_COUNT_DESC","LAST_ACTIVE_DESC","FIRST_SEEN_DESC"]}},{"name":"search","in":"query","description":"Match on device machine name","required":false,"schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"Successful response","headers":{"X-Rate-Limit-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next second before being throttled"}},"X-Quota-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next quota period"}},"X-Quota-Period-End":{"schema":{"type":"string","format":"date-time","description":"When the current quota period expires"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/McpDevice"}},"total":{"type":"integer"},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate Limit Exceeded or Daily Quota Exceeded","headers":{"Retry-After":{"schema":{"type":"integer","description":"Seconds after which to retry the request"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## Get a device and its MCP servers

> Fetch a device and the full list of MCP servers running on it. The server list is returned inline and is not paginated.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"McpDevice":{"type":"object","properties":{"deviceId":{"type":"string"},"serverCount":{"type":"integer"},"hookCount":{"type":"integer","description":"Number of AI agent hooks configured on the device. Excludes marketplace-catalog hooks, which are advertised by a plugin catalog but not installed on the machine."},"pluginCount":{"type":"integer","description":"Number of AI agent plugins on the device, including plugins known only from a marketplace catalog entry."},"extensionCount":{"type":"integer","description":"Number of distinct installed (extensionId, version) pairs on the device. Equals the `total` of GET /ai-governance/v1/ide-extensions?deviceId=<deviceId> with no other filter, so the badge and that drill-down always agree."},"totalSpawns":{"type":"integer"},"totalNetworkCalls":{"type":"integer"},"lastActive":{"type":"integer","description":"Unix timestamp (seconds) of last activity across all servers."},"firstSeen":{"type":"integer","description":"Unix timestamp (seconds) of first observation across all servers."},"machineName":{"type":"string"},"os":{"type":"string"},"osVersion":{"type":"string"},"userEmail":{"type":"string"},"agentStatus":{"type":"string"}}},"McpServer":{"type":"object","properties":{"id":{"type":"string","description":"Opaque server id. Use this value in path parameters; it encodes the (fingerprint, version) key."},"serverId":{"type":"string","description":"The server fingerprint (e.g. \"npm:@scope/pkg\", \"oci:image:tag\", \"url:domain\")."},"canonicalName":{"type":"string"},"version":{"type":"string"},"transport":{"type":"string","description":"Transport mechanism (stdio, http, sse, etc.)"},"risk":{"type":"integer"},"riskLabel":{"type":"string"},"isShadow":{"type":"boolean","description":"True when the server is not associated with any known client."},"remoteUrl":{"type":"string"},"clients":{"type":"array","description":"The AI clients this server is configured in, normalized to the `AiClient` vocabulary and de-duplicated — one value per client however the agent spelled it.","items":{"$ref":"#/components/schemas/AiClient"}},"unrecognizedClients":{"type":"array","description":"Names observed against this server that are not a known AI client, verbatim. On this inventory they are mostly process names seen spawning a server — `xpcproxy`, `com.docker.backend`, `iTerm2`, `node-<hash>` — kept here rather than folded into the vocabulary or dropped. Omitted when empty.","items":{"type":"string"}},"provenance":{"type":"string","description":"Provenance classification. UNKNOWN means the server's identity did not resolve to a registry entry, and is also the value for a row whose classification has not run yet — it is never reported as an empty value. FIRST_PARTY appears only where an administrator has set it.","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"provenanceOverridden":{"type":"boolean","description":"True when an operator has pinned the provenance, rather than it being derived."},"riskOverridden":{"type":"boolean","description":"True when an operator has pinned the risk level, rather than it being derived."},"isManaged":{"type":"boolean","description":"True when the server is declared by an MDM/enterprise-managed config file."},"deviceCount":{"type":"integer"},"clientCount":{"type":"integer","description":"Number of distinct clients that have used this server — always equal to `clients.length + unrecognizedClients.length`, since both are counted after spelling normalization."},"totalSpawns":{"type":"integer"},"totalNetworkCalls":{"type":"integer"},"totalRequestBytes":{"type":"integer"},"totalResponseBytes":{"type":"integer"},"firstSeen":{"type":"integer","description":"Unix timestamp (seconds) of first observation."},"lastUsed":{"type":"integer","description":"Unix timestamp (seconds) of last activity. Omitted when never used."},"removedAt":{"type":"integer","description":"Unix timestamp (seconds) at which the server's declaration was found to have disappeared from its config file. Omitted while the server is still declared — presence is signalled by the field's absence, not by a zero value. Only ever populated when the request set includeRemoved=true."},"registry":{"$ref":"#/components/schemas/McpRegistryMetadata"}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"McpRegistryMetadata":{"type":"object","description":"Public registry metadata; only populated on the single-server detail endpoint.","properties":{"name":{"type":"string"},"description":{"type":"string"},"isOfficial":{"type":"boolean"},"isVerified":{"type":"boolean"},"popularityScore":{"type":"integer"},"repositoryUrl":{"type":"string"},"websiteUrl":{"type":"string"},"license":{"type":"string"},"packageName":{"type":"string"},"packageRegistry":{"type":"string","description":"Source registry (npm, pypi, oci, etc.)"}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/devices/{deviceId}":{"get":{"summary":"Get a device and its MCP servers","description":"Fetch a device and the full list of MCP servers running on it. The server list is returned inline and is not paginated.","parameters":[{"name":"deviceId","in":"path","required":true,"description":"Device id from a device list response","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","headers":{"X-Rate-Limit-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next second before being throttled"}},"X-Quota-Remaining":{"schema":{"type":"integer","description":"How many remaining requests you can make within the next quota period"}},"X-Quota-Period-End":{"schema":{"type":"string","format":"date-time","description":"When the current quota period expires"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"device":{"$ref":"#/components/schemas/McpDevice"},"mcpServers":{"type":"array","items":{"$ref":"#/components/schemas/McpServer"}}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Device does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate Limit Exceeded or Daily Quota Exceeded","headers":{"Retry-After":{"schema":{"type":"integer","description":"Seconds after which to retry the request"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List AI agent plugins

> List the plugin inventory, aggregated per plugin (one row per client, marketplace, name and version) with a device count and what the plugin bundles. A plugin is a governance object because of what it brings with it — MCP servers, hooks and app declarations — not merely because it is installed.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"Plugin":{"type":"object","description":"Company-rollup view of an AI agent plugin.","properties":{"id":{"type":"string","description":"Opaque plugin id. Use in path parameters; encodes the (client, marketplace, name) key."},"client":{"$ref":"#/components/schemas/AiClient"},"unrecognizedClient":{"type":"string","description":"The reported client name, verbatim, when `client` is UNKNOWN. Omitted otherwise."},"marketplace":{"type":"string","description":"Marketplace reference the plugin was sourced from. Empty for a plugin declared inline."},"name":{"type":"string"},"version":{"type":"string"},"status":{"type":"string","description":"Lifecycle state, reported as the highest state any declaring file claims. ENABLED means a settings entry enables it; INSTALLED means it is present on disk but not enabled; AVAILABLE is the bottom of the ladder.\n\nUNKNOWN means the state could not be determined. It is a response value only and is not accepted by the `status` filter — which is precisely why it exists: a displayed value that the filter cannot reach would make rows disappear from the filter that shows them.","enum":["INSTALLED","ENABLED","AVAILABLE","UNKNOWN"]},"trustState":{"type":"string","description":"BLOCKED means the plugin matched an enterprise blocklist entry. UNKNOWN is everything else — nothing in this inventory asserts a positive trust decision.","enum":["BLOCKED","UNKNOWN"]},"provenance":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"isManaged":{"type":"boolean","description":"True when declared by an MDM/enterprise-managed config file."},"description":{"type":"string"},"author":{"type":"string"},"repositoryUrl":{"type":"string"},"deviceCount":{"type":"integer"},"bundledMcpCount":{"type":"integer","description":"MCP servers the plugin declares."},"bundledHookCount":{"type":"integer","description":"Hooks the plugin declares."},"bundledAppCount":{"type":"integer","description":"App declarations the plugin carries."},"removedAt":{"type":"integer","description":"Unix timestamp (seconds) at which the last file declaring this plugin stopped declaring it. Omitted while it is still declared — presence is signalled by the field's absence, not by a zero value."}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/plugins":{"get":{"summary":"List AI agent plugins","description":"List the plugin inventory, aggregated per plugin (one row per client, marketplace, name and version) with a device count and what the plugin bundles. A plugin is a governance object because of what it brings with it — MCP servers, hooks and app declarations — not merely because it is installed.","parameters":[{"name":"limit","in":"query","description":"Maximum number of records to return (default 20, max 100)","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","description":"Opaque pagination cursor returned as nextPageToken in a prior response","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Case-insensitive match on the plugin name","required":false,"schema":{"type":"string","maxLength":255}},{"name":"client","in":"query","description":"Filter to these AI clients. Repeatable, max 20. Uses the normalized `AiClient` vocabulary, so one value matches every spelling of that client.","required":false,"schema":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/AiClient"}}},{"name":"status","in":"query","description":"Filter to these lifecycle states. Repeatable. The rollup reports the highest state any declaring file claims, so a plugin present on disk and enabled in settings reads ENABLED; losing only the settings entry demotes it to INSTALLED, which is a disable rather than an uninstall.\n\nUNKNOWN is deliberately not accepted here: it is a response value for a state that could not be determined, and every value this filter does accept is one the response can display.","required":false,"schema":{"type":"array","maxItems":3,"items":{"type":"string","enum":["INSTALLED","ENABLED","AVAILABLE"]}}},{"name":"managed","in":"query","description":"Restrict to plugins declared by an MDM/enterprise-managed config file (MANAGED) or by a user-writable one (UNMANAGED). Omit to return both.","required":false,"schema":{"type":"string","enum":["MANAGED","UNMANAGED"]}},{"name":"includeRemoved","in":"query","description":"Include plugins whose declaration has disappeared from every file that declared them. Such rows carry `removedAt` and keep the state they were in when they disappeared. Defaults to false, and `total` moves with it. A `pageToken` is only valid for the filter set that produced it: changing this value (or any other filter) mid-pagination silently restarts from the first page with a freshly recomputed `total`, rather than returning an error. Has no effect on tenants where removal reconciliation is not yet enabled.","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"plugins":{"type":"array","items":{"$ref":"#/components/schemas/Plugin"}},"total":{"type":"integer","description":"Total matching plugins (present on the first page)"},"nextPageToken":{"type":"string","description":"Opaque cursor for the next page; omitted when there are no more results"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate Limit Exceeded or Daily Quota Exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List devices declaring a plugin

> List the devices that declare a given plugin. The response also carries a small plugin-level header, so a caller deep-linking to this route can render a title without a second request.\
> \
> There is no separate get-plugin endpoint, and little would be gained by one: the \`/plugins\` rollup row already carries status, trust state, provenance, description, author, repository URL and the bundled counts. The header here adds only \`provenanceOverridden\` on top of that.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"PluginSummaryHeader":{"type":"object","description":"Plugin-level context returned alongside its devices — enough to render a header. Not a detail payload: everything here except `provenanceOverridden` is also on the `/plugins` rollup row.","properties":{"id":{"type":"string"},"client":{"$ref":"#/components/schemas/AiClient"},"unrecognizedClient":{"type":"string","description":"The reported client name, verbatim, when `client` is UNKNOWN. Omitted otherwise."},"marketplace":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","description":"See `Plugin.status`. UNKNOWN is a response value only, never a filter value.","enum":["INSTALLED","ENABLED","AVAILABLE","UNKNOWN"]},"trustState":{"type":"string","enum":["BLOCKED","UNKNOWN"]},"provenance":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"provenanceOverridden":{"type":"boolean","description":"True when an operator has pinned the provenance rather than it being derived."},"description":{"type":"string"},"author":{"type":"string"},"repositoryUrl":{"type":"string"}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"PluginDevice":{"type":"object","description":"One device that declares a given plugin.","properties":{"deviceId":{"type":"string"},"userEmail":{"type":"string"},"userName":{"type":"string"},"machineName":{"type":"string"},"status":{"type":"string","description":"See `Plugin.status`. UNKNOWN is a response value only, never a filter value.","enum":["INSTALLED","ENABLED","AVAILABLE","UNKNOWN"]},"trustState":{"type":"string","enum":["BLOCKED","UNKNOWN"]},"version":{"type":"string"},"bundledMcpCount":{"type":"integer"},"bundledHookCount":{"type":"integer"},"bundledAppCount":{"type":"integer"}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/plugins/{pluginId}/devices":{"get":{"summary":"List devices declaring a plugin","description":"List the devices that declare a given plugin. The response also carries a small plugin-level header, so a caller deep-linking to this route can render a title without a second request.\n\nThere is no separate get-plugin endpoint, and little would be gained by one: the `/plugins` rollup row already carries status, trust state, provenance, description, author, repository URL and the bundled counts. The header here adds only `provenanceOverridden` on top of that.","parameters":[{"name":"pluginId","in":"path","required":true,"description":"Opaque plugin id from a list response; it encodes the (client, marketplace, name, version) key. Version is part of the identity because the rollup is per version — one plugin name can occupy several rows, each with its own deviceCount — so pass the id from the row you want, unmodified.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Match on device id, machine name, user name, or user email","required":false,"schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"plugin":{"$ref":"#/components/schemas/PluginSummaryHeader"},"devices":{"type":"array","items":{"$ref":"#/components/schemas/PluginDevice"}},"total":{"type":"integer"},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Plugin is not present on any device. Only returned for an unfiltered first page: a request carrying `search` or `pageToken` returns 200 with an empty list instead, because neither an empty filtered result nor a page past the end proves the plugin is absent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List plugins on a device

> List the plugins declared on a given device. Unlike the company rollup these are per-declaration rows, so they carry where the plugin came from on disk instead of a device count.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"DevicePlugin":{"type":"object","description":"One plugin declaration on a given device. Distinct from Plugin: this is the per-declaration row, so it carries where the plugin came from on disk rather than a rollup device count.","properties":{"id":{"type":"string","description":"The same opaque plugin id as the company rollup, so a caller can pivot between the two views."},"client":{"$ref":"#/components/schemas/AiClient"},"unrecognizedClient":{"type":"string","description":"The reported client name, verbatim, when `client` is UNKNOWN. Omitted otherwise."},"marketplace":{"type":"string"},"name":{"type":"string"},"version":{"type":"string"},"status":{"type":"string","description":"See `Plugin.status`. UNKNOWN is a response value only, never a filter value.","enum":["INSTALLED","ENABLED","AVAILABLE","UNKNOWN"]},"trustState":{"type":"string","enum":["BLOCKED","UNKNOWN"]},"provenance":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"sourceType":{"type":"string","description":"How the plugin was obtained (e.g. git, local)."},"isManaged":{"type":"boolean"},"bundledMcpCount":{"type":"integer"},"bundledHookCount":{"type":"integer"},"bundledAppCount":{"type":"integer"},"removedAt":{"type":"integer","description":"Unix timestamp (seconds) at which this declaration disappeared. Omitted while present."}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/devices/{deviceId}/plugins":{"get":{"summary":"List plugins on a device","description":"List the plugins declared on a given device. Unlike the company rollup these are per-declaration rows, so they carry where the plugin came from on disk instead of a device count.","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","required":false,"schema":{"type":"string","maxLength":255}},{"name":"managed","in":"query","required":false,"schema":{"type":"string","enum":["MANAGED","UNMANAGED"]}},{"name":"includeRemoved","in":"query","description":"See the same parameter on /plugins.","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"plugins":{"type":"array","items":{"$ref":"#/components/schemas/DevicePlugin"}},"total":{"type":"integer"},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List AI agent hooks

> List the hook inventory, aggregated per hook (one row per client, event and handler payload) with a device count. A hook is code an AI agent runs at a lifecycle point — before a tool call, after one, on prompt submission — so it is the inventory where "what can this agent execute on my endpoint" is answered.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"Hook":{"type":"object","description":"Company-rollup view of an AI agent hook.","properties":{"id":{"type":"string","description":"Opaque hook id. Use in path parameters; encodes the (client, event, handler hash) key."},"client":{"$ref":"#/components/schemas/AiClient"},"unrecognizedClient":{"type":"string","description":"The reported client name, verbatim, when `client` is UNKNOWN. Omitted otherwise."},"eventName":{"type":"string","description":"The agent lifecycle point the hook fires on (e.g. PreToolUse)."},"handlerType":{"type":"string","description":"What the hook does — command, prompt, mcp_tool or http. The defining payload for every handler type is carried in `command`, which is why this field matters for interpreting it."},"command":{"type":"string","description":"The hook's defining payload: the shell command for a command handler, the prompt text for a prompt handler, the tool reference or URL for the others."},"commandHash":{"type":"string","description":"Stable hash of the payload; part of the hook's identity."},"isManaged":{"type":"boolean"},"deviceCount":{"type":"integer"},"removedAt":{"type":"integer","description":"Unix timestamp (seconds) at which the last file declaring this hook stopped declaring it. Omitted while it is still declared."}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/hooks":{"get":{"summary":"List AI agent hooks","description":"List the hook inventory, aggregated per hook (one row per client, event and handler payload) with a device count. A hook is code an AI agent runs at a lifecycle point — before a tool call, after one, on prompt submission — so it is the inventory where \"what can this agent execute on my endpoint\" is answered.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Case-insensitive match on the hook's handler payload","required":false,"schema":{"type":"string","maxLength":255}},{"name":"client","in":"query","description":"Filter to these AI clients. Repeatable, max 20. Uses the normalized `AiClient` vocabulary, so one value matches every spelling of that client.","required":false,"schema":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/AiClient"}}},{"name":"event","in":"query","description":"Filter to these hook event names. Repeatable, max 20. Matched exactly, including case.\n\nThere is no fixed vocabulary: each AI client vendor names its own lifecycle points, so the set grows as vendors add events and as new clients appear. Values observed in the field, by client — Claude Code: PreToolUse, PostToolUse, SessionStart, Stop, UserPromptSubmit, Notification, PermissionRequest. Claude: PreToolUse, PostToolUse, SessionStart, Stop, UserPromptSubmit. Codex: PreToolUse, PostToolUse, Stop, UserPromptSubmit. Copilot: PreToolUse, PostToolUse, PreCompact, SessionStart, Stop, SubagentStart, SubagentStop, UserPromptSubmit. Cursor: beforeShellExecution, afterShellExecution, beforeReadFile, afterFileEdit, afterTabFileEdit, beforeTabFileRead, beforeMCPExecution, afterMCPExecution, beforeSubmitPrompt, afterAgentResponse, afterAgentThought, preToolUse, postToolUse, postToolUseFailure, preCompact, sessionStart, sessionEnd, stop, subagentStart, subagentStop.\n\nIMPORTANT — the same lifecycle point is spelled differently by different vendors and this filter does not normalize. Cursor uses camelCase where the others use PascalCase, so `preToolUse` and `PreToolUse` are distinct values, each matching only its own vendors' hooks. To ask \"show me all pre-tool-use hooks\" pass both spellings. The pairs differing only by case are preToolUse/PreToolUse, postToolUse/PostToolUse, preCompact/PreCompact, sessionStart/SessionStart, stop/Stop, subagentStart/SubagentStart and subagentStop/SubagentStop; Cursor's remaining events have no PascalCase equivalent.","required":false,"schema":{"type":"array","maxItems":20,"items":{"type":"string","maxLength":128}}},{"name":"managed","in":"query","description":"Restrict to hooks declared by an MDM/enterprise-managed config file (MANAGED) or by a user-writable one (UNMANAGED). Omit to return both.","required":false,"schema":{"type":"string","enum":["MANAGED","UNMANAGED"]}},{"name":"includeRemoved","in":"query","description":"Include hooks whose declaration has disappeared from every file that declared them. Such rows carry `removedAt`. Defaults to false, and `total` moves with it. A `pageToken` is only valid for the filter set that produced it: changing this value (or any other filter) mid-pagination silently restarts from the first page with a freshly recomputed `total`, rather than returning an error.","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"hooks":{"type":"array","items":{"$ref":"#/components/schemas/Hook"}},"total":{"type":"integer"},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List devices declaring a hook

> List the devices that declare a given hook. The response also carries a small hook-level header, so a caller deep-linking to this route can render a title without a second request.\
> \
> There is no separate get-hook endpoint, and little would be gained by one: the \`/hooks\` rollup row already carries the client, event, handler type and the handler payload itself. The header here adds only \`matcher\`.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"HookSummaryHeader":{"type":"object","description":"Hook-level context returned alongside its devices — enough to render a header. Not a detail payload: everything here except `matcher` is also on the `/hooks` rollup row.","properties":{"id":{"type":"string"},"client":{"$ref":"#/components/schemas/AiClient"},"unrecognizedClient":{"type":"string","description":"The reported client name, verbatim, when `client` is UNKNOWN. Omitted otherwise."},"eventName":{"type":"string"},"handlerType":{"type":"string"},"command":{"type":"string"},"commandHash":{"type":"string"},"matcher":{"type":"string","description":"What the hook is scoped to match (e.g. a tool name like Bash)."}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"HookDevice":{"type":"object","description":"One device that declares a given hook.","properties":{"deviceId":{"type":"string"},"userEmail":{"type":"string"},"userName":{"type":"string"},"machineName":{"type":"string"}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/hooks/{hookId}/devices":{"get":{"summary":"List devices declaring a hook","description":"List the devices that declare a given hook. The response also carries a small hook-level header, so a caller deep-linking to this route can render a title without a second request.\n\nThere is no separate get-hook endpoint, and little would be gained by one: the `/hooks` rollup row already carries the client, event, handler type and the handler payload itself. The header here adds only `matcher`.","parameters":[{"name":"hookId","in":"path","required":true,"description":"Opaque hook id from a list response; it encodes the (client, event, handler hash) key","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Match on device id, machine name, user name, or user email","required":false,"schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"hook":{"$ref":"#/components/schemas/HookSummaryHeader"},"devices":{"type":"array","items":{"$ref":"#/components/schemas/HookDevice"}},"total":{"type":"integer"},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Hook is not present on any device. Only returned for an unfiltered first page: a request carrying `search` or `pageToken` returns 200 with an empty list instead, because neither an empty filtered result nor a page past the end proves the hook is absent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List hooks on a device

> List the hooks declared on a given device. Unlike the company rollup these are per-declaration rows, so they carry which file on disk declared the hook and the matcher that scopes it — neither of which survives the rollup.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"DeviceHook":{"type":"object","description":"One hook declaration on a given device. Distinct from Hook: this is the per-declaration row, so it carries the file that declared it and the matcher scoping it, neither of which survives the company rollup.","properties":{"id":{"type":"string","description":"The same opaque hook id as the company rollup, so a caller can pivot between the two views."},"client":{"$ref":"#/components/schemas/AiClient"},"unrecognizedClient":{"type":"string","description":"The reported client name, verbatim, when `client` is UNKNOWN. Omitted otherwise."},"eventName":{"type":"string"},"handlerType":{"type":"string"},"command":{"type":"string"},"commandHash":{"type":"string"},"matcher":{"type":"string"},"isManaged":{"type":"boolean"},"removedAt":{"type":"integer","description":"Unix timestamp (seconds) at which this declaration disappeared. Omitted while present."}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/devices/{deviceId}/hooks":{"get":{"summary":"List hooks on a device","description":"List the hooks declared on a given device. Unlike the company rollup these are per-declaration rows, so they carry which file on disk declared the hook and the matcher that scopes it — neither of which survives the rollup.","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","required":false,"schema":{"type":"string","maxLength":255}},{"name":"managed","in":"query","required":false,"schema":{"type":"string","enum":["MANAGED","UNMANAGED"]}},{"name":"includeRemoved","in":"query","description":"See the same parameter on /hooks.","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"hooks":{"type":"array","items":{"$ref":"#/components/schemas/DeviceHook"}},"total":{"type":"integer"},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List IDE extensions

> List the IDE-extension inventory, aggregated per extension and version across every IDE and device. An IDE carries hundreds of extensions and a handful of them can talk to a model, so \`aiOnly\` is usually the filter you want.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"IdeExtension":{"type":"object","description":"Company-rollup view of an IDE extension, one row per extension and version.","properties":{"id":{"type":"string","description":"Opaque extension id. Use in path parameters; encodes the (extension id, version) key."},"extensionId":{"type":"string","description":"The marketplace identifier, \"publisher.name\"."},"publisher":{"type":"string"},"displayName":{"type":"string"},"version":{"type":"string"},"clients":{"type":"array","description":"The AI clients this extension is installed in, normalized to the `AiClient` vocabulary and de-duplicated.","items":{"$ref":"#/components/schemas/AiClient"}},"unrecognizedClients":{"type":"array","description":"Reported names that are not a known AI client, verbatim. Omitted when empty.","items":{"type":"string"}},"provenance":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"provenanceOverridden":{"type":"boolean"},"isAi":{"type":"boolean","description":"True when the extension is classified as AI-capable."},"deviceCount":{"type":"integer"}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/ide-extensions":{"get":{"summary":"List IDE extensions","description":"List the IDE-extension inventory, aggregated per extension and version across every IDE and device. An IDE carries hundreds of extensions and a handful of them can talk to a model, so `aiOnly` is usually the filter you want.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Case-insensitive match on the extension id or display name","required":false,"schema":{"type":"string","maxLength":255}},{"name":"client","in":"query","description":"Filter to these AI clients. Repeatable, max 20. Uses the normalized `AiClient` vocabulary, so one value matches every spelling of that client.","required":false,"schema":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/AiClient"}}},{"name":"provenance","in":"query","description":"Filter to these provenance classifications. Repeatable. OFFICIAL resolves to an owned or verified Open VSX namespace, or a curated known-AI extension. COMMUNITY is listed on Open VSX but not owner-verified. UNKNOWN did not resolve at all — a sideloaded VSIX lands here, which is the interesting case. FIRST_PARTY only appears where an administrator has set it.","required":false,"schema":{"type":"array","maxItems":4,"items":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]}}},{"name":"deviceId","in":"query","description":"Restrict to extensions installed on this device.","required":false,"schema":{"type":"string","maxLength":255}},{"name":"aiOnly","in":"query","description":"Restrict to extensions classified as AI-capable.","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"extensions":{"type":"array","items":{"$ref":"#/components/schemas/IdeExtension"}},"total":{"type":"integer"},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## Get an IDE extension

> Fetch a single extension by its opaque id, including its Open VSX registry record and a per-IDE, per-profile breakdown. VS Code profiles are separate extension sets, so the same extension can be present in one profile of a machine and absent from another.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"IdeExtensionDetail":{"type":"object","description":"Single-extension view, with the registry record and per-IDE breakdown.","properties":{"id":{"type":"string"},"extensionId":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"displayName":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"provenance":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"provenanceOverridden":{"type":"boolean"},"isAi":{"type":"boolean"},"deviceCount":{"type":"integer"},"firstInstalledAt":{"type":"integer","description":"Unix timestamp (seconds) of the earliest observed install."},"lastSeenAt":{"type":"integer","description":"Unix timestamp (seconds) of the most recent observation."},"registry":{"$ref":"#/components/schemas/IdeExtensionRegistry"},"clients":{"type":"array","items":{"$ref":"#/components/schemas/IdeExtensionClient"}}}},"IdeExtensionRegistry":{"type":"object","description":"The extension's Open VSX record.","properties":{"source":{"type":"string"},"namespace":{"type":"string"},"name":{"type":"string"},"verified":{"type":"boolean"},"deprecated":{"type":"boolean"},"latestVersion":{"type":"string"},"lookupStatus":{"type":"string","description":"Whether the registry lookup succeeded. NOT_FOUND means the registry does not list this extension, which is a finding; ERROR means the registry could not be reached, which is a transient miss. Both leave provenance UNKNOWN, so this field is what distinguishes them.","enum":["OK","NOT_FOUND","ERROR","UNSPECIFIED"]},"checkedAt":{"type":"integer"}}},"IdeExtensionClient":{"type":"object","description":"One IDE that has the extension, broken down by the profiles within it. VS Code profiles are separate extension sets, so presence is per profile rather than per machine.","properties":{"client":{"$ref":"#/components/schemas/AiClient"},"unrecognizedClient":{"type":"string","description":"The reported client name, verbatim, when `client` is UNKNOWN. Omitted otherwise."},"profiles":{"type":"array","items":{"$ref":"#/components/schemas/IdeExtensionProfile"}}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"IdeExtensionProfile":{"type":"object","properties":{"profileId":{"type":"string"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/IdeExtensionVersion"}}}},"IdeExtensionVersion":{"type":"object","properties":{"version":{"type":"string"},"provenance":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]},"anyPrerelease":{"type":"boolean"},"anyPinned":{"type":"boolean"},"firstInstalledAt":{"type":"integer"}}},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/ide-extensions/{extensionId}":{"get":{"summary":"Get an IDE extension","description":"Fetch a single extension by its opaque id, including its Open VSX registry record and a per-IDE, per-profile breakdown. VS Code profiles are separate extension sets, so the same extension can be present in one profile of a machine and absent from another.","parameters":[{"name":"extensionId","in":"path","required":true,"description":"Opaque extension id from a list response; it encodes the (extension id, version) key","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdeExtensionDetail"}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Extension or version does not exist on any device","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```

## List devices with an IDE extension

> List the devices on which a given extension is installed.

```json
{"openapi":"3.0.3","info":{"title":"Nightfall AI Governance API","version":"1.0.0"},"servers":[{"url":"https://api.nightfall.ai/ai-governance/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"IdeExtensionHeader":{"type":"object","description":"Extension-level context returned alongside its devices.","properties":{"id":{"type":"string"},"extensionId":{"type":"string"},"publisher":{"type":"string"},"displayName":{"type":"string"},"provenance":{"type":"string","enum":["FIRST_PARTY","OFFICIAL","COMMUNITY","UNKNOWN"]}}},"IdeExtensionDevice":{"type":"object","description":"One device with the extension installed.","properties":{"deviceId":{"type":"string"},"userEmail":{"type":"string"},"userName":{"type":"string"},"machineName":{"type":"string"},"clients":{"type":"array","description":"The AI clients this extension is installed in, normalized to the `AiClient` vocabulary and de-duplicated.","items":{"$ref":"#/components/schemas/AiClient"}},"unrecognizedClients":{"type":"array","description":"Reported names that are not a known AI client, verbatim. Omitted when empty.","items":{"type":"string"}},"installedAt":{"type":"integer"}}},"AiClient":{"type":"string","description":"An AI coding client, normalized. Endpoint agents report client names inconsistently — the same client arrives with different casing, spacing and separators depending on which config file or process reported it — so every client field and client filter on this API uses this closed vocabulary instead of the raw name.\n\nUNKNOWN is a response value only, never a filter value: it means the reported name did not match a known client, and the raw name is then returned alongside in the accompanying `unrecognizedClient` field or `unrecognizedClients` list.\n\nCLAUDE covers the Claude desktop app as well as the Claude client. VSCODE means Visual Studio Code itself; a Code-based product with its own identity, such as Cursor or Windsurf, reports as itself.","enum":["ANTIGRAVITY","CLAUDE","CLAUDE_CODE","CLAUDE_COWORK","CODEX","COPILOT","CURSOR","UNKNOWN","VSCODE","WINDSURF"]},"Error":{"type":"object","properties":{"code":{"type":"integer","description":"A status code to uniquely describe this error."},"message":{"type":"string","description":"A message associated with the status code."},"description":{"type":"string","description":"Additional details to explain what may have occurred. Omitted if empty."}}}}},"paths":{"/ide-extensions/{extensionId}/devices":{"get":{"summary":"List devices with an IDE extension","description":"List the devices on which a given extension is installed.","parameters":[{"name":"extensionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"pageToken","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Match on device id, machine name, user name, or user email","required":false,"schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"extension":{"$ref":"#/components/schemas/IdeExtensionHeader"},"devices":{"type":"array","items":{"$ref":"#/components/schemas/IdeExtensionDevice"}},"total":{"type":"integer","description":"Total matching devices. Only meaningful on the first page; a request carrying a pageToken does not recompute it."},"nextPageToken":{"type":"string"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Extension or version is not installed on any device. Only returned for an unfiltered first page: a request carrying `search` or `pageToken` returns 200 with an empty list instead, because neither an empty filtered result nor a page past the end proves the extension is absent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Nightfall Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://help.nightfall.ai/developer-api/nightfall_apis/ai-governance-apis.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
