Skip to content

Discovery Source Observations

This chapter covers the core and plugin split for discovery evidence: observation storage, scoped reads and the console presentation.

Discovery evidence storage

Optional versioned evidence extends the existing discovery report via the named discovery_evidence plugin service. Generic immutable observation storage and tenant-scoped CRUD live in preloop.models; commercial correlation/read policy lives in the optional plugin. Candidate deduplication and discovered events remain unchanged. Collector assertions preserve provenance and scan gaps, and do not establish runtime verification or device attestation.

Expected-device inventory

Expected-device inventory uses the shared ExpectedDevice model and crud_expected_device persistence boundary. An account-local roster is separate from reported discovery candidates, immutable source observations and device credentials. Atomic roster replacement serializes on the account, preserves receipt history for retained entries and never grants a credential or enrollment. Latest collection windows remain independent per authenticated reporting source; commercial import policy and coverage endpoints are provided by the optional fleet service.

Agents page and agent detail

The Agents page shows managed and discovered agents only. Shared fleet lifecycle models, CRUD and lazy console components live in this repository, but fleet routes, evaluation and delivery belong to the optional service plugin. The fleet_coverage capability and owner/admin permissions gate Settings > People & access > Device fleet, its runner links and Attention items. Without the capability these surfaces are hidden and direct navigation redirects. Device identity is an exact tenant-salted fingerprint match; legacy runners remain separate and hostnames are hints only. Fleet findings and audited alert settings persist in core; device projections are computed by the plugin. None of this enables or advertises a fleet feature in the open-source edition. The device table combines runner, governed/ungoverned agent and roster-state filters over the bounded projection. Its limits disclosure explains observation freshness and identity hints. Roster administration requires manage_device_fleet: the console reviews a selected CSV and obtains explicit consent before complete replacement through the optional service's multipart endpoint. Person/group display-name retention is opt-in; backend validation rejects an invalid snapshot atomically.

Agent detail obtains device links from the optional fleet service, using the same projection and fleet read permission as the device page. Standalone instances and older plugins return no device links. Model detail reads configured consumers through /ai-models/{id}/used-by: active explicit agent bindings, legacy enrollment configuration, and saved flow model selections (default, label routing and enabled retry). Gateway auto-registration and historical traffic remain observed usage, separate from configuration. Consumer reads stay in the current account and apply both resource visibility and the permission to view each consumer kind.

Fleet map

GET /api/v1/fleet/map combines the optional device projection with bounded, tenant-owned agent, runner, model and user reads, plus the available hosted model when enabled. Hosted operator credentials and routing stay private. Each resource kind retains its read permission and account visibility checks. The map, /fleet/code-activity and /fleet/usage carry no whole-route permission: any member may call them, a source the caller may not read is skipped and named in unavailable and restricted (devices need view_device_fleet), code activity drops calls from agents the resource authorizer hides before work items are built, and usage keys work totals only by the work items that code activity returned. Account realtime topics are not filtered per member on the server, so the map drops a member's events that name an agent or flow it does not hold. Credential references group model accounts without resolving secrets. Configured and observed edges stay distinct; explicit impact directions support dependency and affected-owner traversal without crossing into unrelated accounts at the same provider. Decommissioned agents are never read (only a visibility-filtered count is returned), and each agent carries an agent_state (onboarded, discovered, or inactive when suspended or not seen for 14 days) with last_seen_at. Model rows collapse into canonical models keyed by identifier; the agent-to-model edge names the provider account it goes through, an account names its provider as parent_id, and an agent's provider facet follows its account, so one model offered by two providers does not join their blast radius. Omitted visible nodes are summarized by type, and source/output truncation and unavailable projections remain explicit so a partial graph never claims complete blast-radius coverage. active_within_minutes (1 to 527,040) narrows the graph to recent activity: agents, devices and runners count by their own last-seen evidence or runtime sessions in the window; models, provider accounts and providers need their own gateway ledger rows in the window (being configured on, or bound to, an active agent is not activity), and people and devices stay when something they hold is active. Configured idle resources return only without a window or with include_idle=true, flagged idle; every node carries last_active_at. Ledger rows of an agent's runtime principal also draw an observed uses_model edge. People nodes carry the stored avatar_url (SSO picture or manual upload), never a third-party lookup. The response echoes the window. The console defaults to 30 days, lights the line live gateway traffic and approvals travelled (throttled, a state change rather than an animation), and offers instance administrators a spatial arrangement, with positions stored per user, also as an alternative to the Overview inventory table.

Fleet usage

GET /api/v1/fleet/usage adds gateway request, input/output token, optional cache read/write token and estimated cost totals keyed by the graph's canonical model and provider ids. A bounded visible model configuration mapping applies the same tenant, model permission and resource visibility rules as the graph, including available hosted models without exposing operator credentials. CRUD performs one SQL GROUP BY per grouping over account-scoped model_gateway ledger rows, using the gateway account/window indexes and excluding replay validation. The echoed window includes UTC bounds; omitted "any" and windows over 90 days read at most 90 days. A 1,000-configuration cap sets truncation and partial flags. Missing input/output/cost data stays explicit, cache sums remain nullable, and mixed currencies yield no combined cost rather than an unlabelled conversion. Explicit unpriced markers also exclude legacy numeric zero costs and mark totals partial. Resolved prices take precedence over preflight budget pricing metadata. Currency and pricing markers require heap reads through the existing gateway window indexes; this endpoint does not claim an index-only scan. Deleted or invisible model configurations contribute no node totals.

Code activity

GET /api/v1/fleet/code-activity is the map's code layer: pull and merge requests opened, commented on, reviewed and merged in the same window, counted per agent, flow and owning person, plus the flows to place and the 40 most recent events. It reads successful code tool calls (runtime_session_activity, served by the partial index ix_runtime_session_activity_work_tools, capped at 5,000 rows) attributing a call in a flow run to the flow and any other call to the managed agent whose runtime principal made it, and forge-reported merges (issue_cost_pull_request.merged_at, capped at 500 merged PRs, joined through ix_issue_cost_execution_account_pr_key) attributed to the flows whose runs touched the pull request. A flow's tool-call merge and the forge's webhook for it count once. The generic add_comment counts only when the wrapper recorded a pull request target (code_target, a boolean kept instead of the target); issue comments and operator notes never count. "Any time" reads 90 days. Live, the code_activity realtime topic carries each successful code tool call and each first merge. The collection toolbar shares list-toolbar with the other console collections; accounts are an optional layer. The hosts give the canvas its height. Device fleet and full screen pan on wheel, while the Overview card lets plain wheel scroll the page. Where the wheel pans (Device fleet, full screen), Fit keeps spatial scale at least 0.85 and leaves overflow reachable by panning; on the Overview card, where it does not, Fit contains every node (down to the 0.2 zoom-out limit). Code activity toasts sit in the free spot nearest the stage's top right that covers no node.

Work history and classified calls

The optional work and work_truncated fields seed history with at most 200 distinct issues and pull requests, newest activity first, in that same bounded window. CRUD reads each source once: successful issue/PR tools (5,000 rows, ix_runtime_session_activity_work_tools), PR milestones (separate 500-row active and merged windows in one query), current tracker snapshots and lifecycle recency (201 rows), cost/lifecycle run links (5,000 rows), and compact execution subjects/publication URLs (5,000 rows). Sentinel rows expose source truncation. Existing references enrich old tracker/PR snapshots within those queries, with no per-object lookup. Titles come from stored tracker rows or compact subjects, labels from tracker snapshots; bodies, prompts, diffs and tool results are excluded. A stored opening or earliest approval is not proof of current open state or latest review verdict, so unknown values stay null. Work actors require explicit in-window run or tool links, are visibility filtered, deduplicated and capped at five; agent ownership and forge merge stamps alone never identify a human actor. Ambiguous and unassigned cost links supply neither actors nor issue associations. The PR query keeps independently capped active and merged windows, so new openings cannot crowd an older merge out of the counts. Stored milestone merge counts retain their contributing-flow semantics, with bounded fan-out and explicit truncation. Issue snapshot recency is not a delivery journal or a guarantee that every creation path preserved current labels. The normal tool summary producer records argument sizes, not references. Such rows cannot identify work or actors; only an explicitly stored reference can do so. This projection neither adds argument capture nor infers an issue's creator.

Classified work calls close that gap without argument capture. The MCP call recorder (dynamic_fastmcp) and the native permission hook (/agents/permission-check, PreToolUse) store a compact work_ref in the activity metadata (services/fleet_work_refs.py): the source (built-in tool, gh/glab CLI, GitHub or GitLab MCP server by server-name pattern), the action, the object reference (#N/!N), the repository, the forge host and up to five issue keys parsed from a title or body as closing or plain references. Never the arguments, the body or the command line. Native shell calls are classified at PreToolUse with the decision's status, so an allowed command counts even if it later failed. The PostToolUse and PostToolUseFailure hooks that Claude Code onboarding installs report only a content-free tool_end row (tool id, name, exit code, error flag, duration) through /agents/session-event. That row moves the session's derived turn_state (crud/runtime_session_turn_state.py) and does not reclassify work. The partial index ix_runtime_session_activity_work_ref serves these rows next to the tool-name index. Code activity then returns typed, deduplicated relations (at most 500) among the returned work items, flows and agents: touched (agent or flow to an item, with actions and call count), resolves (rollup PR/issue pair, closing run links, parsed closing references), mentions, triggered (a run's trigger subject) and produced (a run's published PR). flows lists every flow with a run, a gateway request or code activity in the window, with model_node_ids from its ledger rows (api_usage.flow_id or the execution's flow), falling back to the configured model. Issue snapshots enter a window by forge update time, creation or lifecycle revision, never by the local updated_at bookkeeping that every sync bumps. GET /fleet/usage adds agents, people, flows, devices and work totals keyed by node id: gateway tokens and estimated cost with the same honesty rules, tool calls, approvals, PR actions, review comments and distinct issues and PRs touched, one grouped, account-scoped query per source.

Layout and presentation

Spatial (canvas) layout packs measured owner/machine islands first, then flows, then providers holding their accounts and models, then work, with block gaps of at most 48px and shelf gaps of at most 96px.

Every fleet reader resolves a runtime principal to its managed agent the way the session explorer does (services/fleet_principals.py): a managed_agent principal by id, the exact source pair, then the base before the first : of a per-conversation id (claude-code-<install>:<conversation>[:<subagent>], codex-<id>:<thread>), so hook calls, approvals and gateway rows of every conversation count for their agent. GitHub issues store the forge's own update time in last_updated_external and Jira issues keep Jira's created/updated (read from fields in REST results), so a sync never makes an untouched issue look active. All three endpoints accept users=<id,...> (at most 50, services/fleet_scope.py): the scope is computed once on the unwindowed graph (selected people, the devices they own, the agents and runners they own or that run on those devices, the models and provider accounts those agents use and their providers) and then intersected with the window. Code activity keeps the in-scope agents' work, counts, events and relations; usage keeps in-scope node totals and counts model/provider totals only from those agents' principals and the people's direct requests. Flows are outside a person's scope because a run stores no triggering user. Model nodes carry provider_id (the provider of most of their configurations) and provider_account_ids; provider nodes carry model_ids and account_ids, provider accounts provider_id and model_ids, so the console can draw provider > account > model containment without a lookup.

Spatial layout packs measured owner/machine islands beside the model/provider core, with attached issue/PR rows in the work lane. The first drag stores the complete visible arrangement per user and scope. Membership updates preserve those positions and the camera, place arrivals beside their relationships and retain inactive slots for seven days. A growing machine moves only the following boxes in its island. Arrange (an icon button beside Fit, canvas only, hidden on an empty map) clears manual positions and repacks the current nodes. Reset layout additionally clears folded groups and expanded columns. Both spatial actions offer a six-second Undo, with reduced-motion support. Unowned layouts repack on every membership change (people islands, flows, provider core, work) and the camera refits when a node joins, unless the user has recently navigated. Columns (the lanes view) puts flows under the agents in one lane with a sub-header (FleetMapLayout.columns[].groups, titled by laneTitle) and stretches the row pitch (10px to 64px) to the stage height; where the wheel scrolls the page, Fit also contains the tallest lane. containers() and providerContents() nest models in their provider (optional provider_id, then provided_by, then provider_ids) whenever the provider is shown. scopeToUsers() applies the server's users= rule in the browser until a response echoes users; hosts refetch on fleet-map-users-change. The words "Lanes" and "Canvas" live in utils/fleet-map-words.ts; stored arrangement values stay columns/spatial. Bubbles are placed by utils/fleet-map-bubbles.ts: beside their node, never on another node's box, at most two per node. Lit lines carry the specific label of what travelled them (model and tokens, native tool and status, approval, or the work action), never a kind; MCP calls light no device line. Gateway calls carrying flow_id add an observed flow-to-model line. Code activity seeds the window's merged PRs from forge merge events, and up to 200 work items from the optional FleetCodeActivity.work list. Optional relations join canonical work identities and draw resolves, mentions, triggered, produced and touched edges, deduplicated by kind and endpoints. Resolves is solid, mentions dashed; selected or live lines carry at most six bounded labels. Explicit relations keep PRs and issues separate; older responses retain legacy containment. model_node_ids adds every observed flow model, falling back to model_node_id. Missing new fields remain supported.

The frontend honours optional per-node idle evidence. Idle nodes only appear when Show includes Idle (configured), which makes the host request include_idle=true; the active count excludes them. Nodes with explicit activity flags do not depend on agent-binding reachability to survive a facet filter. Optional avatar_url supplies a lazy 24px round image for people and their panel headers, falling back to initials and then the generic icon.

GET /fleet/usage populates an In this window section for every selection. The client accepts optional agents, people, flows, devices and work buckets alongside models/providers, displaying only reported counters and preserving null token or cost values as unknown. Cost is labelled est., including four decimal places below one cent. Work usage joins live URL identities to server work ids. The work panel also lists attributed actors, available action counts and related work links. A window change clears old fetched totals before loading the next window.

The shared list-toolbar has an opt-in condensed mode for canvas hosts. Fleet Map keeps search, Show, Users, Window (each trigger names its value), count, arrangement and More on one row when space permits; below 1280px or inside a narrow card it uses a search popup, Filters and the arrangement switch. The Overview keeps one stable Inventory component in its grid, giving Map a bounded full-width row. Embedded sizing excludes full screen so either host can cover the viewport and restore body scrolling on exit. The Show choice remains stored per user and scope.

Live activity and flow context

Gateway starts light the sender; real completions light the model and show measured input/output tokens. Native permission activity identifies its managed agent explicitly, without treating a runtime principal ID as a registry ID. Tool activity lights the node and relationship and feeds a bounded client tally and Recent list, labelled as events seen since the map opened. Permission allowed is not execution success. Bubbles show four bounded metadata notes for four seconds, with a neutral hairline and reduced-motion support. Approval commands use the existing display payload with additional credential scrubbing; model prompts, replies and generic argument bags are never selected by this layer. Product and model-family marks remain in utils/fleet-map-icons.ts. The bare property hides presentation chrome.

Flow start/status messages project fleet_context from already-loaded execution metadata: compact trigger refs, safe object URLs and actual parent/retry/resume/thread IDs. They add no reads, migrations or content capture. The live story folds concurrent runs per flow, expires completed runs after 60 seconds and retains observed work associations as hairlines after their six-second highlight. Controlled publication's existing result.pr_url attaches a PR to its recorded issue; structured reviewer verdicts are explicitly labelled reported. Forge merge metadata confirms a merge, but contributing flows are not a human merger. Richer normalized issue/PR events can supply titles, label changes, confirmed review states and verified actors. Missing fields remain unknown. This in-memory view is not a durable tracker event journal.

Provider callbacks

Provider callback adapters can reuse account-owned encrypted secret references and durable callback receipts to commit content-free verdicts and audit entries atomically across workers.