Device Agent Privacy and Consent
Status: PRIVATE PREVIEW
This page is for enterprise administrators and security teams who deploy the
Control Zero device agent (the cz-agent binary, service cz-agentd) and need
to know exactly what data leaves a device and what consent or privacy controls
exist. It describes the device agent's current behavior and states where a
control does not exist.
This page is about the device agent only. The separate shadow-AI discovery component has its own documentation.
The collection boundary
The device agent emits seven groups of structured data. Each one is a closed, typed wire structure, and each one is enumerated here so the boundary is explicit rather than implied.
1. The local hook protocol
When a governed host tool fires a hook, a shim forwards the call to the daemon over a local socket. Three types cross that boundary:
HookRequest— what the host chose to run:canonical_source(the host name),payload(the tool input as JSON, forwarded unchanged so the policy can be evaluated),cwd,git_remote(optional),shim_version,shim_signature, andshim_asserted_ancestry.Provenance— what the daemon observed about the connecting peer:peer_pid,peer_uid,peer_sid,peer_exe_path, a content hash of the peer executable,ancestry_observed,shim_signature_valid, and averdictstring (host | not_host | unknown). A field the kernel cannot supply isNone, and the peer identity fields are observed by the daemon, never taken from the request.HookDecision— the finalised result returned to the shim:verdict(allow | deny | warn | hitl),origin(the deciding layer), the observedprovenance, and an optionaldlpoutcome.
The findings in a decision's DLP outcome never carry matched bytes.
masked_input redacts only spans matched by a mask rule, so text matched only
by a detect rule stays in it unchanged:
DlpOutcome—action(detect | mask | block),findings,uncompilable_rule_ids, andmasked_input. Each finding names the rule, its category and its action, not the matched text.masked_inputis present only when a mask fired and the call proceeds: it is the tool input with the spans matched by mask rules redacted. Everything else, including text matched only by a detect rule, appears unchanged.DlpFinding— arule_id, the org'scategory, and theaction; nothing else.masked_inputis present only when a mask fired, changed the input, and the verdict lets the call proceed — the caller runs the tool on the redacted input instead of the original.
2. Discovery rows
The discovery detectors return AiDiscovery rows. The row shape is:
id,discovery_type(ai_traffic | ai_process | ai_api_key | ai_tool),summary,details(a metadata-only string map),severity,first_seen,last_seen, andhostname.
The details map is where the privacy boundary is enforced. The module defines
a hard list of forbidden detail keys — argv, args, argument, command,
cmdline, cmd, env, environ, api_key, token, secret, password,
value, credential — and the forbidden-key check flags any detail key that
contains one of these strings, case-insensitively, naming the offending key.
The process and traffic detectors' tests fail any row they emit that trips it.
The key and host detectors have no such test.
What each detector actually reports, rather than what it saw:
- Key scanning reports only
file_path,line_number,key_type, andkey_prefix(the first 8 characters of the matched text plus...). The key value is never returned or transmitted. - Process scanning reports only
process_name,pid, the owner's user id, the matched tool label (tool) and, when more than one instance matched,instance_count. Command-line arguments — which may contain API keys or secrets passed as flags — are deliberately not transmitted. - Traffic scanning reports only the endpoint (a known AI API hostname, or
localhost:<port>for a known local inference port), a label, aportfield that is always the literal443, and a connection count. These are read from the active TCP connection table on Linux and macOS. On Windows it reports nothing. No payload data is captured. - Host detection reports the host name, the config
path that proved presence, and an optional semantic-version string read from
the host binary's
--versionoutput. Config file contents are never read into a row.
3. Inventory wire
Inventory upload is not yet active. Nothing posts a batch to
POST /api/device/inventory in production today. The cz-agent scan command
reports discovery counts locally on the machine; it does not upload them.
The wire type that would carry an upload is defined as follows. Discovery rows
are mapped into an InventoryBatch (scan_cycle_id, observed_at, rows) of
InventoryRow objects, each with a kind from the closed vocabulary process | listener | egress | app | mcp_config | drift | weights | autostart | container | credential_reach | skill | plugin, an attribution (process | unknown), a
metadata-only details map, and a severity. Egress rows carry dst_host and
dst_port; credential-reach rows carry the same key metadata described above
(path, line, type, prefix), never the credential itself.
4. Heartbeat
The heartbeat is a fallback liveness signal carrying operational metadata, not
discovery content. Its fields are agent_version, install_mode, posture,
inventory_digest, spool_depth, spool_drops_total, per-writer last_ok_at
timestamps, and daemon_absent. Every field except agent_version carries a
hardcoded value in production today: install_mode is always user, posture
is always an empty object, inventory_digest is always empty, spool_depth and
spool_drops_total are always 0, the last_ok_at fields (stream,
inventory, upload) are always absent, and daemon_absent is always
false. The heartbeat therefore reports "never run"
regardless of what the agent has done, and its freshness timestamps cannot
distinguish "not run" from "ran and found nothing".
5. Policy fetch
The daemon fetches its policy bundle with a machine-signed GET /api/policy.
The machine-signed requests this agent sends (the map stream
GET /api/device/map, the policy fetch and the heartbeat) all carry
X-CZ-Machine-ID, X-CZ-Timestamp, and X-CZ-Signature headers, which is how
the server identifies the enrolled machine.
6. Enrolment
cz-agent up sends POST /api/device/login/start with hostname, os,
install_mode (always user today) and shim_pubkey in the body, and the
machine public key in the X-CZ-Pending-Key header. It then polls
POST /api/device/login/poll with the device_code it was issued.
7. Map stream
The daemon long-polls a machine-signed GET /api/device/map (every 30 seconds
by default), sending its current policy_version and its enrolment session id
as query parameters.