Skip to content

API Reference

All endpoints are served at /api/v1/* by agenthound-server. The default bind is 127.0.0.1:8080 (loopback only).

The canonical, machine-readable spec is served at GET /api/v1/docs (OpenAPI 3.0 YAML). CI verifies it stays in sync with the route map. This document is a human-readable summary.

Authentication

AgentHound is single-user. The server has no application-layer login — no JWT, no users table, no RBAC. Network scope (127.0.0.1 by default) is the primary access control.

A second control catches browser drive-by attacks: mutating endpoints are gated by an Origin allowlist (OriginGuard). Browser requests must originate from an allowlisted origin; non-browser callers (no Origin header) pass through.

Endpoint group Auth
Read endpoints (GET /api/v1/...) Open.
Mutating endpoints (see table below) Browser Origin must be in AGENTHOUND_CORS_ORIGINS (default http://localhost:8080, http://127.0.0.1:8080). No Origin header (curl, CLI) → pass.

The default allowlist covers the embedded UI. Stream-ingest from the host (curl --data-binary @scan.json http://127.0.0.1:8080/api/v1/ingest) works zero-config. The agenthound-server CLI (ingest, query) bypasses HTTP entirely.

If you need to expose the server beyond loopback, do so at the network layer (VPN / SSH tunnel / Tailscale) — never by binding 0.0.0.0:8080 to a public interface. See security.md.

Mutating endpoints (Origin-gated)

  • POST /api/v1/ingest
  • POST /api/v1/query
  • POST /api/v1/scans
  • DELETE /api/v1/scans/{id}
  • POST /api/v1/analysis/shortest-path
  • POST /api/v1/analysis/all-paths
  • POST /api/v1/analysis/weighted-path
  • POST /api/v1/analysis/topology/shortest-path
  • POST /api/v1/analysis/topology/all-paths
  • POST /api/v1/analysis/topology/weighted-path
  • PATCH /api/v1/findings/triage/{fingerprint}

A request to any of these with a foreign or null Origin returns 403 Forbidden.

Error format

All errors return a structured JSON response:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "description of the problem"
  }
}

Error codes: VALIDATION_ERROR (400), FORBIDDEN (403), NOT_FOUND (404), REVISION_CONFLICT (409), PROJECTION_CONFLICT (409), SERVICE_UNAVAILABLE (503), STORAGE_BINDING_UNAVAILABLE (503), INGEST_FAILED (500), SCAN_DELETE_CONFLICT (409), and INTERNAL_ERROR (500). Graph-backed reads return PROJECTION_CONFLICT unless one stable, complete published projection is available for the entire read. error.details.reason is absent, updating, incomplete, or changed; clients must retry the whole read rather than interpret an empty result as an all-clear. Internal errors include a request ID for log correlation; raw error strings are not leaked to clients.


Health

GET /api/v1/health

Returns connectivity status for Neo4j and PostgreSQL.

{
  "status": "ok",
  "neo4j": "ok",
  "postgres": "ok"
}

status is ok or degraded; component fields are ok or unavailable.

GET /api/v1/docs

Serves the OpenAPI 3.0 specification (application/yaml).


Graph

GET /api/v1/graph/stats

Returns public node and edge counts by kind. Internal SchemaVersion nodes are excluded from stats, node lists, and search results.

{
  "node_counts": { "MCPServer": 12, "MCPTool": 47, "MCPResource": 23, "AgentInstance": 3 },
  "edge_counts": { "TRUSTS_SERVER": 15, "PROVIDES_TOOL": 47, "CAN_REACH": 8 },
  "total_nodes": 85,
  "total_edges": 70,
  "projection": {
    "scan_id": "scan-abc123",
    "revision": 12,
    "coverage_limited": false,
    "coverage_limitation_count": 0
  }
}

projection is required and identifies the immutable publication represented by the counts. coverage_limited qualifies absence-based conclusions, while coverage_limitation_count reports the number of active limited scopes.

GET /api/v1/graph/search

Free-text search across node names, IDs, and identifying properties.

Param Type Default Description
q string (required) Search term. Must be at least 2 characters; shorter values return 400 VALIDATION_ERROR.
limit int 20 Max results (1–100; values above the cap are silently clamped).

GET /api/v1/graph/nodes

Param Type Default Description
kind string (all) Filter by node label
limit int 100 Max results (1–10000)
offset int 0 Nonnegative pagination offset
revision string Opaque page.revision from the first page

Node, edge, and scan lists use typed JSON envelopes. For example:

{
  "nodes": [],
  "page": {
    "offset": 0,
    "limit": 100,
    "total": 0,
    "has_more": false,
    "complete": true,
    "revision": "...",
    "projection": {
      "scan_id": "scan-abc123",
      "revision": 12,
      "coverage_limited": false,
      "coverage_limitation_count": 0
    }
  }
}

Edge and scan responses use edges and scans respectively. Continue with the same revision token. A graph change returns 409 REVISION_CONFLICT with error.details.expected_revision and error.details.actual_revision; restart at offset 0. Every graph page also requires the same projection identity. A missing, incomplete, or changing publication returns 409 PROJECTION_CONFLICT. Page metadata describes read completeness only; it does not assert that collector coverage or analysis stages completed.

GET /api/v1/graph/nodes/{id}

Returns a single node with all connected edges.

{
  "node": {
    "id": "sha256:...",
    "kinds": ["MCPServer"],
    "properties": { "name": "...", "transport": "stdio" }
  },
  "edges": [
    { "source": "...", "target": "...", "kind": "PROVIDES_TOOL", "properties": {} }
  ]
}

GET /api/v1/graph/nodes/{id}/neighborhood

Returns the N-hop neighborhood subgraph rooted at the node.

Param Type Default Description
depth int 1 Hop count (1–3; values above the cap are silently clamped).

GET /api/v1/graph/nodes/{id}/blast-radius

Returns reachable nodes grouped by ring (1-hop, 2-hop, ...). Useful for "what can this agent touch?" questions.

GET /api/v1/graph/edges

Param Type Default Description
kind string (all) Filter by edge kind
source string Filter by source node ID
target string Filter by target node ID
limit int 100 Max results (1–100000)
offset int 0 Nonnegative pagination offset
revision string Opaque page.revision from the first page

Ingest

POST /api/v1/ingest (Origin-gated)

Max body: 100 MB.

Upload strict ingest-v1 collector JSON. Unknown structural fields, non-V1 artifacts, registry-contract mismatches, missing identity/collection/rules metadata, facts owned only by incomplete domains, omitted edge endpoint kinds, property aliases, and incomplete canonical credential/host/auth evidence are rejected before any mutation. Runs the serialized lifecycle: preflight the wire and registry contracts → verify both database binding markers → validate identity schema and internal consistency → apply graph and coverage scopes → normalize → freeze pre-write totals → write → reconcile complete observation domains → post-process → freeze post-analysis totals → snapshot → publish.

Every valid artifact is accepted; collection identity controls graph meaning, not admission. The server verifies the identity scheme/version, digest consistency, and evidence classification rules but cannot prove where the collector ran. A database marker that cannot be verified returns sanitized 503 STORAGE_BINDING_UNAVAILABLE before any graph write.

For MCPServer and A2AAgent, configured method, assurance, and evidence are an optional atomic provenance tuple: all three may be absent when the producer did not inspect configuration/declarations, but a present tuple must contain all three producer-compatible values. Method-to-assurance mapping is fixed; anonymous-probe evidence requires none/unauthenticated, local-process evidence requires unknown/unknown, known authenticated/custom methods require configured-credential or declared-scheme evidence, and the documented unknown/query/declaration/local cases remain accepted. A configured none/unauthenticated declaration with unknown or declared-scheme evidence is accepted as provenance but remains fail-closed during analysis. Even an exact raw configured anonymous-probe tuple remains fail-closed unless it is selected through validated observed provenance; it does not independently produce an unauthenticated effective assurance or exact weakness score.

For MCPServer, if any observed_auth_* field is present, method/assurance/evidence are required as one canonical tuple. Runtime declaration-only evidence is rejected because the MCP collector does not emit it as an observation. The anonymous tuple is valid only for a reachable HTTP server and must be exactly none/unauthenticated/anonymous_probe_succeeded; partial, unreachable, or contradictory anonymous claims return 400 VALIDATION_ERROR. Current unreachable (unknown/unknown/unknown), stdio (unknown/unknown/local_process), and unknown configured-request-material (unknown/unknown/configured_credential) tuples remain valid.

For A2AAgent, observed authentication is accepted only as the atomic exact tuple none/unauthenticated/anonymous_probe_succeeded with auth_probe_method=get_task_nonexistent and auth_probe_status=anonymous_protocol_access, with auth_probe_detail equal to task_not_found_v1 or task_not_found_v0_3. The lookup is read-only and must return the protocol's exact nonexistent-task result. Method, status, and bounded fixed detail are required together. Card access, protected responses, and inconclusive results may publish the canonical diagnostic triple but must omit observed_auth_*; orphan positive status, partial/wrongly typed metadata, arbitrary status/detail, generic observed tuples, and observed fields on a non-positive status return 400 VALIDATION_ERROR.

The A2A probe diagnostic fields auth_probe_method, auth_probe_status, and auth_probe_detail are themselves optional and atomic. Sparse protocol discovery may omit the entire probe/observed set. If any diagnostic is present, all three are required. anonymous_protocol_access requires the exact observed anonymous tuple above; authentication_required and unknown preserve their bounded diagnostic triple and must omit every observed_auth_* field.

A2A signature posture is likewise optional and atomic: signature_verification_status, signature_key_source, signature_key_trust, and is_signed must be either all absent or all present and mutually consistent. Full A2A card collection emits all four; sparse protocol discovery emits none.

Campaign submissions add a scenario-specific prevalidation stage immediately after generic validation and before normalization, BeginScan, graph writes, or coverage reconciliation. Both positive and negative artifacts must match their bounded witness/staged-observation envelope and current topology. A rejection cannot overwrite canonical campaign evidence or retire coverage. It creates only a failed Postgres scan-audit row whose metadata.campaign_rejection contains a random rejection ID, sanitized run/scenario/version/outcome fields, and fixed reason codes—never the raw artifact, witness, fingerprint, digest, endpoint, or credential material.

// Request body (abridged; see graph-model.md for the complete schema)
{
  "meta": {
    "version": 1,
    "type": "agenthound-ingest",
    "collector": "mcp",
    "collector_version": "1.0.0",
    "timestamp": "2026-07-11T00:00:00Z",
    "scan_id": "scan-abc123",
    "identity": {
      "scheme": "agenthound_collection_v1",
      "version": 1,
      "collection_point_id": "sha256:...",
      "network_context_id": "sha256:...",
      "quality": "strong",
      "network_quality": "strong",
      "network_class": "private",
      "display": {"hostname": "target-01", "os": "linux", "architecture": "amd64"},
      "evidence": [
        {"kind": "os_instance", "digest": "hmac-sha256:..."},
        {"kind": "principal", "digest": "hmac-sha256:..."}
      ],
      "network_evidence": [
        {"kind": "route_private", "digest": "hmac-sha256:..."}
      ]
    },
    "collection": {
      "state": "complete",
      "coverage_keys": ["mcp:target:sha256:..."],
      "outcomes": [{
        "collector": "mcp",
        "coverage_key": "mcp:target:sha256:...",
        "target": "https://example.test/mcp",
        "method": "mcp",
        "state": "complete"
      }]
    },
    "ruleset": { "...": "required" },
    "identity_schemes": [{ "...": "required" }]
  },
  "graph": { "nodes": [], "edges": [] }
}

// Response (200)
{
  "scan_id": "scan-abc123",
  "outcome": "complete",
  "projection_status": "complete",
  "submitted": { "nodes": 47, "edges": 82 },
  "write_rows": { "nodes": 47, "edges": 82 },
  "findings": 6,
  "graph_totals": {
    "before": { "node_counts": {}, "edge_counts": {}, "total_nodes": 0, "total_edges": 0 },
    "after": { "node_counts": {}, "edge_counts": {}, "total_nodes": 47, "total_edges": 82 }
  },
  "stages": [
    { "name": "write_nodes", "state": "complete", "required": true, "duration": 1200000 }
  ],
  "published_revision": 12,
  "warnings": [],
  "normalization_status": "complete",
  "normalization_warnings": [],
  "collection": {
    "state": "complete",
    "coverage_keys": ["mcp:target:sha256:..."],
    "outcomes": [{
      "collector": "mcp",
      "coverage_key": "mcp:target:sha256:...",
      "target": "https://example.test/mcp",
      "method": "mcp",
      "state": "complete"
    }]
  },
  "identity": {
    "collection_point_id": "sha256:...",
    "network_context_id": "sha256:...",
    "quality": "strong",
    "network_quality": "strong",
    "network_class": "private",
    "display": {"hostname": "target-01", "os": "linux", "architecture": "amd64"},
    "recognition": "new"
  },
  "duration": 1230000000
}

meta.collection is required on every ingest request, and collection is required on every successful ingest result. A missing collection report is a validation error, not an implicit complete scan. The response contains the finalized server-scoped coverage keys, outcomes, and authoritative roots after identity scoping—not the producer-local key space. The receipt owns an independent copy of that report. Coverage warnings qualify both a limited current artifact and any unrelated coverage limitation that remains active after publication; a clean artifact clears the warning only when no active limitation remains. Nodes may set property_semantics: "reference_only" only with an empty properties object. This records ID/kind ownership without authoring or replacing managed properties; omitted property_semantics is authoritative. Completed dynamic exhaustive runs may include authoritative_roots, each with a stable root coverage_key and the complete child_coverage_keys active set. Every child outcome includes its explicit parent_coverage_key; the server does not infer root membership from opaque key syntax. The server reconciles prior children omitted from that set as complete-empty. Targeted and non-exhaustive runs omit this field and cannot retire siblings.

submitted counts literal input contributions. write_rows counts unique logical nodes and relationships affected by successful Neo4j writes (including matches of existing facts); multiple owner or property-semantics contributions to the same database fact are counted once. graph_totals freezes public inventory before and after processing. A batch failure returns 500 INGEST_FAILED with the same typed partial result under error.details, including committed write rows. The scan and global projection state are marked incomplete.

identity.recognition is new when no earlier scan used that collection point and recognized otherwise. quality describes collection-point evidence; network_quality independently reports whether route/interface visibility was complete. If network quality is unknown, local point-scoped facts retain their normal scope while remote/network-scoped facts become artifact-local and additive-only. Weak collection-point identities localize all authoritative facts. display contains optional bounded, non-authoritative hostname/OS/architecture labels.

Only explicitly complete, attributable target/config coverage keys can retire prior raw observations. Incomplete coverage never retires omitted data. A safe partial, failed, truncated, or unknown collection may still publish the facts it did confirm; those writes are additive and preserve omitted properties, labels, owners, edges, and child scopes. The server persists each incomplete scope as an active coverage limitation, disables comparison, and does not permit an empty-findings all-clear. A later published complete or not_applicable outcome clears the corresponding limitation and restores normal absence reconciliation. Lossless normalization coercions are persisted as warning and may publish; only warnings explicitly marked publication_unsafe produce degraded and withhold publication.


Analysis

POST /api/v1/analysis/shortest-path (Origin-gated)

Find a bounded hop-shortest path using the explicit directed security relationship policy. Unknown request fields, including scope, are rejected. Use the separate /analysis/topology/... operations for undirected topology.

// Request
{
  "source": "my-agent",
  "source_kind": "AgentInstance",
  "target": "postgres://prod",
  "target_kind": "MCPResource",
  "max_hops": 10
}

// Response
{
  "paths": [
    {
      "nodes": [{ "id": "sha256:...", "name": "my-agent", "kinds": ["AgentInstance"] }],
      "edges": [{ "kind": "TRUSTS_SERVER", "source": "sha256:...", "target": "sha256:..." }],
      "hops": 3
    }
  ],
  "metadata": {
    "scope": "security",
    "direction": "out",
    "relationship_kinds": ["TRUSTS_SERVER", "PROVIDES_TOOL", "HAS_ACCESS_TO"],
    "max_hops": 10,
    "algorithm": "bounded-min-weight",
    "complete": true
  },
  "projection": {
    "scan_id": "scan-abc123",
    "revision": 12,
    "coverage_limited": false,
    "coverage_limitation_count": 0
  }
}

Every traversal result requires projection; it identifies the stable published graph used for endpoint resolution and traversal. An unavailable or changing projection returns 409 PROJECTION_CONFLICT.

POST /api/v1/analysis/all-paths (Origin-gated)

Enumerate directed security paths between two nodes, bounded by max_hops, limit, and the server expansion cap. Same request as shortest-path, plus:

Field Type Default Description
limit int 10 Max paths returned (1–100)

POST /api/v1/analysis/weighted-path (Origin-gated)

Find the bounded minimum-risk-weight path with one deployment-independent algorithm. APOC availability does not change results. Missing risk_weight fails the request; negative or non-finite values also fail. The response includes metadata.algorithm: "bounded-min-weight" and traversal completeness metadata. On a TRUSTS_SERVER hop, the returned edge risk_weight is the derived effective trust weight (falling back to the raw configured weight only when the effective weight is unavailable), and the path weight sums that same returned value. Exact finding-detail path costs follow the same rule while retaining both raw risk_weight and effective_risk_weight in edge properties for provenance.

Explicit topology traversal (Origin-gated)

The undirected topology capability is available only through separate operations:

  • POST /api/v1/analysis/topology/shortest-path
  • POST /api/v1/analysis/topology/all-paths
  • POST /api/v1/analysis/topology/weighted-path

They accept the same scope-free request body and return metadata with scope: "topology" and direction: "both".

GET /api/v1/analysis/findings

List findings from the immutable currently published Postgres snapshot with inline triage state.

Param Type Description
severity string Filter: critical, high, medium, low
include_suppressed bool Default false. When true, include findings triaged accepted-risk / false-positive (hidden otherwise).
The response is { "findings": [...], "scope": {...} }. scope carries the
published scan ID, revision, publication time, projection/snapshot status,
availability, staleness, and active_coverage_limitations. A partial live
Neo4j projection does not move the published pointer. Clients require explicit
available/complete/non-stale metadata and coverage_limited: false before
interpreting an empty findings array as a current all-clear. The boolean also
covers an outdated registered instruction-source contract; the limitations
array carries generic incomplete-scope details. A failed refresh may show
cached rows only when labelled as cached.

Each finding carries owasp_map and atlas_map arrays. Detections without a confident ATLAS mapping return atlas_map: []. variant, typed evidence, and exact witness evidence are persisted with the same scan row, so a published credential reference cannot be silently reclassified from a later live graph. See the ATLAS crosswalk.

When evidence.state is verified, evidence.verification contains the persisted campaign contract: scenario ID/version, campaign run ID, verified time, oracle type/outcome, control and authenticated stages/statuses/resource- addressed flags, and cleanup_status (not_applicable for read-only cred-reach). The API never returns a witness endpoint, credential, payload, resource content, or receipt state.

GET /api/v1/analysis/findings/{id}

Return evidence detail for a specific finding in the same published snapshot. attack_path is the literal typed detector witness. It reports shape (linear, branched, disconnected, cyclic, or nodes_only), continuity, recorded relationship direction, completeness reasons, and synthetic-join provenance. linearization is present only when every supplied node and edge forms one complete directed source-to-target path. total_risk_weight and cost.value are nullable; any missing relationship weight produces cost.state: "incomplete" rather than a numeric zero. Non-linear, mixed-direction, and reverse-to-finding evidence has cost.state: "not_applicable" because summing branches is not an attack-path cost.

Detail starts from the same persisted finding row used by the list. The schema stores the detector witness graph with each finding snapshot. Detail serves it directly with snapshot.evidence_state: "persisted_exact_evidence" even when the mutable projection has advanced or is incomplete; detail does not run a second query or reconstruct evidence from mutable Neo4j.

GET /api/v1/analysis/findings/{id}/witness

Export a stable, sanitized campaign witness for a predicted credential-gated CAN_REACH finding (16-char fingerprint), so the collector-side campaign runner (agenthound campaign --scenario cred-reach) can verify it.

Witness V1 is built under a guarded read and is runnable only when the providing MCPServer transport is HTTP. It carries the explicit source agent, explicit agent/server/credential/resource kinds and IDs, credential value_hash + merge_key, the endpoint-derived server identity hash, opaque service scope, resource identity input, predicted edge kind, topology normalization version, and the actual ordered current CAN_REACH evidence node IDs/kinds. The endpoint remains out-of-band. It carries no Neo4j relationship IDs, arbitrary properties, or raw credential. Its unkeyed fingerprint is a consistency checksum, not authenticity or authorization.

Returns 400 for a malformed finding ID, 404 when no runnable credential-gated CAN_REACH prediction matches the finding (e.g. the credential is a synthetic identity with no observable material), and 409 when no stable published projection is available or the projection changes during the guarded read. The served OpenAPI contract defines the complete witness response and FindingVerification schemas; evidence.verification remains optional for non-verified findings.

{
  "witness": {
    "schema_version": 1,
    "topology_normalization_version": 1,
    "publication_revision": 7,
    "predicted_edge_kind": "CAN_REACH",
    "agent_id": "sha256:...",
    "agent_kind": "AgentInstance",
    "credential_id": "sha256:...",
    "credential_kind": "Credential",
    "credential_value_hash": "sha256-hex",
    "credential_merge_key": "value_hash",
    "server_id": "sha256:...",
    "server_kind": "MCPServer",
    "server_identity_id": "sha256:...",
    "service_scope": "network_context",
    "service_scope_id": "sha256:...",
    "resource_id": "sha256:...",
    "resource_kind": "MCPResource",
    "resource_identity_input": "postgres://prod/customers",
    "evidence_node_ids": ["sha256:agent", "sha256:entry-server", "sha256:entry-tool", "sha256:server", "sha256:credential", "sha256:identity", "sha256:resource-tool", "sha256:resource"],
    "evidence_node_kinds": ["AgentInstance", "MCPServer", "MCPTool", "MCPServer", "Credential", "Identity", "MCPTool", "MCPResource"]
  },
  "projection": {
    "scan_id": "scan-...",
    "revision": 7,
    "coverage_limited": false,
    "coverage_limitation_count": 0
  }
}

The same witness is produced by the agenthound-server witness --finding <id> CLI. When the verification is later ingested, the server re-correlates the witness fingerprint and every echoed field against current identities and the exact ordered source-agent CAN_REACH topology. A positive publication revision is provenance; equality to the current revision is not required.

GET /api/v1/findings/triage/{fingerprint}

Return the cross-scan triage decision for a finding fingerprint (16-char hex). Open read; a fingerprint with no recorded decision returns the implicit new state.

{ "status": "accepted-risk", "note": "Approved by sec-review", "updated_at": "2026-06-19T12:00:00Z" }

PATCH /api/v1/findings/triage/{fingerprint} (Origin-gated)

Update the triage decision for a finding fingerprint. Omitting note preserves the current note; sending note: "" clears it. Triage state has no foreign key to findings, so it survives scan deletion and re-detection.

// Request
{ "status": "accepted-risk", "note": "Approved by sec-review" }

status must be one of new, triaging, confirmed, accepted-risk, false-positive.

GET /api/v1/analysis/prebuilt

List all 19 pre-built queries with metadata.

GET /api/v1/analysis/prebuilt/{id}

Execute a pre-built query and return results.

{
  "query": {
    "id": "poisoned-tools",
    "name": "Poisoned Tool Descriptions",
    "severity": "high",
    "category": "Vulnerabilities",
    "owasp_map": ["MCP05", "ASI03"],
    "atlas_map": ["AML.T0051", "AML.T0110"]
  },
  "rows": [...],
  "projection": {
    "scan_id": "scan-abc123",
    "revision": 12,
    "coverage_limited": false,
    "coverage_limitation_count": 0
  }
}

Like findings, pre-built queries carry an optional atlas_map ([]string) of MITRE ATLAS technique IDs on confidently-mappable queries; it is omitted (omitempty) on queries with no ATLAS mapping (e.g. infrastructure/path/chokepoint queries). See the ATLAS crosswalk. projection is required on every result. The shortest-to-database result also requires traversal metadata; other pre-built results omit it. An unavailable or changing projection returns 409 PROJECTION_CONFLICT.

Authentication columns in agents-shell-access, no-auth-servers, no-auth-a2a, and chokepoint-servers are effective post-analysis values. Where returned, auth_source is observed or configured; configured and observed node properties remain separately available through graph endpoints and raw Cypher.


Query

POST /api/v1/query (Origin-gated)

Execute raw Cypher against Neo4j.

// Request
{
  "cypher": "MATCH (n:MCPServer) RETURN n.name LIMIT 10",
  "params": { }
}

The Origin gate protects against browser drive-by Cypher injection from a hostile origin: a browser request from outside AGENTHOUND_CORS_ORIGINS returns 403 Forbidden before Cypher is executed. CLI use (agenthound-server query) bypasses HTTP and never goes through the gate.


Posture publication

GET /api/v1/posture

Returns the mutable projection state separately from the latest published revision. When an ingest is updating or incomplete, scan_id identifies that attempt while published_scan_id / published_revision continue to identify the last safe snapshot. active_coverage_roots reports each current exact or deep instruction root by hashed coverage key, mode, state, owning scan, observation time, and registry contract. active_coverage_limitations reports every latest published unknown, partial, failed, or truncated collection scope with its optional parent, owning scan, and observation time. Positive findings and graph facts remain usable; consumers must not interpret an empty finding set as an all-clear while that list is non-empty or an active instruction root uses a non-current registry contract.

GET /api/v1/posture/export

Returns one persisted publication revision. The export is assembled inside the same PostgreSQL transaction that replaces the scan's finding snapshot and advances publication. The endpoint serves schema version 1 only; missing, null, or unsupported required fields fail closed rather than returning a response outside the OpenAPI contract. The V1 export includes exact scope, stage/coverage completeness, normalization warnings, managed-observation completeness, observation and publication timestamps, suppression policy, frozen public graph totals, comparison metadata, rules provenance, active coverage-root/limitation state, and all findings with the triage state observed at publication. scope.dirty_coverage is always an explicit empty array for a published revision; scope.active_coverage_keys lists all active coverage heads. completeness.observation_details reports property-incomplete public managed node/raw-relationship counts plus public tokenless-node and raw relationship-incident-to-tokenless-node counts. Any non-zero count withholds publication. health.state is not_captured because publication does not perform a timestamped dependency health probe. limits.findings declares returned/total counts and whether the finding result-set cardinality is complete. It does not assert complete collection coverage or an all-clear. The export never combines fresh reads from Neo4j, finding history, and scan history.

Returns 404 until a complete posture has been published.


Scans

Scan records serialize model.Scan verbatim. The status field is one of:

Status Meaning
pending Registered but not yet started.
running Ingest in progress.
completed Complete collection plus required graph, analysis, stats, snapshot, and publication stages succeeded.
completed_with_errors Either a safe limited-coverage revision published, or a required later stage failed and the prior published posture remains available. Check publication_status / published_revision.
failed A graph write failed. write_rows preserves rows committed before failure.

Additive lifecycle fields expose collection_status, graph_status, analysis_status, snapshot_status, projection_status, and publication_status. Detailed coverage, rules, identity, stage, and graph statistics payloads live under metadata. publication_status is published for the selected revision, superseded for prior published revisions, and unpublished otherwise.

Each scan has submitted: {nodes, edges}, write_rows: {nodes, edges}, and graph_totals: {before, after}. The frozen graph totals are public-inventory totals, not write counts. Deltas are comparable only when a non-empty comparison_key matches. The key includes canonical target/config coverage, rules and identity semantics, plus each active root's registry contract/state and the revisions of every other active coverage head. Any active coverage limitation or outdated/incomplete instruction root makes comparison unavailable. The server records comparable_to_scan_id only for matching non-empty comparison keys.

Collector artifacts and the server use ingest v1 in lockstep. Version mismatch returns UNSUPPORTED_V1_CONTRACT; instruction-registry mismatch returns REGISTRY_CONTRACT_MISMATCH. Both are rejected before scan lifecycle, audit, or graph mutation and include upgrade/recollection guidance.

GET /api/v1/scans

Param Type Default Description
limit int 50 Max results
offset int 0 Pagination offset
revision string Opaque page.revision from the first page
order string started Stable descending order: started; latest usable completed; or current/latest published

POST /api/v1/scans (Origin-gated)

Register a new scan (sets scan_id, started_at, status: pending). Used by the UI's "New scan" flow; CLI ingest creates scan records implicitly.

GET /api/v1/scans/{id}

Get scan details by ID. The Rules view uses this endpoint for an explicit ?scan=<id> selection, so provenance is not limited to the newest scan-history page.

DELETE /api/v1/scans/{id} (Origin-gated)

Delete PostgreSQL history only; this endpoint never mutates Neo4j or infers ownership from scalar scan_id. It returns 409 SCAN_DELETE_CONFLICT for pending/running scans, active coverage heads, active coverage limitations, and the currently published scan. Historical finding rows cascade with the scan; cross-scan triage state remains.


Rules

GET /api/v1/rules

List the server process's current active YAML detection-rule catalog. This is not a substitute for the scan-specific metadata.ruleset manifest.

GET /api/v1/rules/{id}

Return the parsed definition for one rule in the server's current catalog.

Imported scan records persist their own effective manifest under metadata.ruleset. Each entry includes type, id, version, semantic_sha256, source class, and a canonical effective_matcher object. Read/parse/validation/compile failures are retained in errors and reflected by load_state. digest and semantic_sha256 identify content only; authenticity: unverified explicitly disclaims signature/authenticity.