Wytness Collector
The Collector is a container you run yourself. It polls the Azure logging stores you already have — Application Insights / Log Analytics rows from Microsoft Foundry server-side tracing and Copilot Studio telemetry — turns each row into a Wytness event, signs it with your Ed25519 key and PII-tokenizes it inside your own boundary, then POSTs signed envelopes to the ingest endpoint. It's how you capture agents that never call an SDK directly. The same container can also run Radar discovery sources — identity, proxy and vendor usage logs read for the fingerprints of AI services.
How it works
- Polls each configured source on a cadence and reads the new rows since its last cursor.
- Converts each row into a Wytness event, deterministically keyed so re-reads never duplicate.
- Signs and PII-tokenizes the event inside your container, reusing the same
wytness-aicrypto the SDK uses — your keys and secrets never leave your boundary. - POSTs the signed envelope to your ingest endpoint and advances the cursor.
The collected tier
Collector events carry the collected assurance tier: proof the log content is untouched since pickup. This is deliberately below — and never conflated with — the SDK's sdk-signed tier, which attests an action at the moment it happened. The dashboard labels each tier distinctly so an auditor always knows which guarantee applies.
Setting it up
Seven steps: five things to gather, then the container, then a check. Everything except the container is a one-off, and every credential stays on your side — Wytness never receives your Azure keys, your signing private key or your PII secret.
- Let the collector read your logs — export switch + a read-only service principal
- Create a dedicated API key —
WYTNESS_API_KEY - Generate a dedicated signing keypair —
WYTNESS_SIGNING_KEY - Copy your PII tokenization keys —
WYTNESS_PII_PUBKEY+WYTNESS_PII_SECRET - Describe your sources —
COLLECTOR_SOURCES - Run the container — Docker, Compose, Container Apps, ACI or Kubernetes
- Check it's working — heartbeat on the Collectors page, events on Events
Before step 1, clear the network path with your platform team — everything the collector does is outbound.
Network requirements
The collector needs four outbound HTTPS destinations and zero inbound rules(two more only if you add vendor usage discovery sources). Hand this table to your network team before anything else — it is the only firewall change the whole setup needs.
| Direction | Destination | Port | Purpose |
|---|---|---|---|
| Outbound | login.microsoftonline.com | 443 | Entra token for the service principal (step 1) |
| Outbound | api.loganalytics.io | 443 | Reading the Log Analytics workspace |
| Outbound | api.wytness.ai (or your WYTNESS_ENDPOINT) | 443 | Shipping signed events and discovery observations; syncing the signature library |
| Outbound | api.openai.com / api.anthropic.com | 443 | Only with an openai_usage / anthropic_usage discovery source — the vendor's usage API |
| Outbound | ghcr.io + pkg-containers.githubusercontent.com | 443 | Image pull — or mirror the image into your own registry |
| Inbound | none | — | /healthz serves only your own probes |
1. Let the collector read your logs
Turn on the log export for each product, then create one identity that can read the workspace. Telemetry is OFF by default at the source for both products — if a source later reads silent, this switch is the first thing to re-check.
- Microsoft Foundry. Connect an Application Insights resource to the project — portal: project → Tracing → Connect, or scriptable via an ARM connection PUT. Captures the full model context: system prompt, messages, tool I/O, tokens.
- Copilot Studio. Power Platform admin center → the environment → Data export → Application Insights. One environment-level switch covers both authoring experiences and autonomous runs. Prerequisites: Managed Environment, Dataverse capacity and Copilot Studio licensing. Captures conversation and tool I/O at the agent boundary — no system prompt or model internals.
- Discovery sources (optional). Entra sign-ins: a tenant diagnostic setting that exports the sign-in log categories to the same workspace. Proxy traffic: the custom table your Zscaler, Netskope or other egress export already lands in that workspace. Vendor usage: an org-admin key from the OpenAI or Anthropic console — no Azure-side change at all.
- The identity. Create a service principal and grant it
Log Analytics Readeron the workspace's resource group. The same principal covers both sources when they share a workspace.
AppGenAIContent table becomes Protected. The service principal will additionally need Privileged Monitoring Data Reader, or Foundry prompt and response content stops arriving.You now have the four values step 5 embeds: the Log Analytics workspace GUID, and the principal's tenant id, client id and client secret.
2. Create a dedicated API key
In the dashboard, go to Crypto keys and create an API key used only by the collector. A dedicated key means its traffic is attributable, and revoking it never interrupts your SDK-instrumented agents.
You now have WYTNESS_API_KEY.
3. Generate a dedicated signing keypair
Also on Crypto keys, generate a new Ed25519 signing keypair, label it for the collector, and register the public key. The private half is shown once and never leaves your browser — paste it straight into your secret store. It is what makes each collected event non-repudiable.
A key covers events from its creation onwards — that is what stops anyone signing back-dated evidence. The collector honours it: its first run normally back-fills 24 hours, but if the key is younger than that it starts at the key instead and says so in the log. Nothing is lost that could have been accepted, and if you want that first day captured, create the key before the period you intend to record.
You now have WYTNESS_SIGNING_KEY.
4. Copy your PII tokenization keys
From Privacy, copy your organisation's PII public key and HMAC secret. The collector uses them to replace personal data with tokens before anything leaves your network, which is why Wytness can never read it back. See PII Protection for what is detected automatically and what you should declare.
You now have WYTNESS_PII_PUBKEY and WYTNESS_PII_SECRET.
5. Describe your sources
Write one entry per source into COLLECTOR_SOURCES — either inline JSON or a path to a mounted file. Use the four values from step 1.
[ { "kind": "foundry_traces", "workspace_id": "<Log Analytics workspace GUID>", "display_name": "Foundry — prod", "resource_id": "<App Insights ARM id — required when sources share a workspace>", "tenant_id": "...", "client_id": "...", "client_secret": "..." }]Capture kinds are foundry_traces and copilot_studio_env — they emit collected-tier events. Discovery kinds — entra_signin, proxy_law, openai_usage and anthropic_usage — emit Radar observations, never events; their shapes are under Discovery sources. Source credentials live only in your container or vault — Wytness never stores them.
Checkpoint: what you should now have
Five environment variables, all from your side of the boundary. If any row is missing, the step that produces it is on the left.
| From | Env var | Value |
|---|---|---|
| Step 2 | WYTNESS_API_KEY | The collector's dedicated API key |
| Step 3 | WYTNESS_SIGNING_KEY | The collector's Ed25519 private key |
| Step 4 | WYTNESS_PII_PUBKEY + WYTNESS_PII_SECRET | Your org's PII tokenization keys |
| Step 5 | COLLECTOR_SOURCES | Source list (embeds the step 1 workspace + principal values) |
| Optional | WYTNESS_ENDPOINT | Defaults to https://api.wytness.ai |
6. Run the container
The collector is platform-agnostic — anywhere that runs a Linux container works. Whatever you run it on, the contract is the same:
- One replica. The poll cursor has a single writer — scale sources by adding entries to
COLLECTOR_SOURCES, not replicas. - The env vars from the checkpoint above. Keep them in your platform's secret store, not plain config.
- A persistent volume at
/var/lib/wytness-collector. It holds the poll cursor (no re-ship or skip across restarts) and the dead-letter file (no event loss across delivery outages). On ephemeral storage a restart re-reads dedup-safe, but pending dead-letter is lost. - Outbound 443 only — the four destinations under Network requirements. No ingress, no public IP.
- 512 MB / 1 CPU is comfortable; sources with very large model contexts may need more memory. Optional HTTP health probe on
/healthz(portCOLLECTOR_HEALTH_PORT, default 8080).
docker run -d --name wytness-collector \ --restart unless-stopped \ --memory=512m --cpus=1 \ -v wytness-collector-state:/var/lib/wytness-collector \ -e WYTNESS_API_KEY=... -e WYTNESS_ENDPOINT=... -e WYTNESS_SIGNING_KEY=... \ -e WYTNESS_PII_PUBKEY=... -e WYTNESS_PII_SECRET=... \ -e COLLECTOR_SOURCES='[...]' \ ghcr.io/wytness-ai/wytness-collector:0.2.0COLLECTOR_SOURCES='[...]'is the JSON you built in step 5, inline — or mount the file and pass its path (the Compose tab shows that form).- The named volume +
--restart unless-stoppedsatisfy the persistence and one-replica halves of the contract.
ghcr.io/wytness-ai/wytness-collector@sha256:…. docker pull prints the digest, and every publish records it. Tags are mutable; digests are not.7. Check it's working
Three things should be true within a few minutes. If any is not, the collector tells you which.
- It has checked in. The Collectors page in the dashboard lists your instance with a recent heartbeat and one row per configured source — this works the same on every platform.
- Events are arriving. On Events, filter to the
collectedtier. Each event names the source it came from. - The container is healthy by your platform's own probe. The image ships a Docker
HEALTHCHECKagainst/healthz(sodocker psshows healthy); on Container Apps or Kubernetes, point a liveness probe at the same path.
COLLECTOR_SILENCE_HOURS (default 6). That is the signal to check step 1 — the export switch or the principal's role assignment is the usual cause.Configuration
Wytness credentials are shared with the SDK. Use a dedicated API key and a dedicated Ed25519 signing keypair for the collector so its activity is attributable and revocable on its own.
| Env var | Purpose |
|---|---|
| WYTNESS_API_KEY | Org API key — use a dedicated key for the collector. |
| WYTNESS_ENDPOINT | Ingest endpoint (defaults to https://api.wytness.ai). |
| WYTNESS_SIGNING_KEY | Ed25519 key — use a dedicated signing keypair (label collector). |
| WYTNESS_PII_PUBKEY / WYTNESS_PII_SECRET | Customer-held PII tokenization keys. |
Collector-specific settings:
| Env var | Purpose |
|---|---|
| COLLECTOR_SOURCES | JSON list of source configs, or a path to a mounted JSON file. |
| COLLECTOR_POLL_SECONDS | Poll cadence (default 120, min 30). |
| COLLECTOR_STATE_PATH | Cursor persistence path (default /var/lib/wytness-collector/state.json). |
| COLLECTOR_SILENCE_HOURS | Silence threshold carried on every heartbeat (default 6). |
| COLLECTOR_ID / COLLECTOR_LABEL | Instance identity shown on the Collectors page. |
| COLLECTOR_HEALTH_PORT | /healthz port (default 8080). |
| COLLECTOR_FLYWHEEL | Discovery pattern contribution (default off). Ships only the pattern classes Radar could not match — model ids, hostnames, user-agent classes — with daily counts, and only when the organisation owner has also switched it on in the dashboard; the server refuses it otherwise. |
Sources
The source list is built in step 5. Two notes that matter once you have more than one.
resource_id is required when two sources share a single Log Analytics workspace, so each stream is attributed to the right App Insights resource. Source credentials live only in your container or vault — Wytness never stores them.Discovery sources (Radar)
Discovery sources read logs for the fingerprints of AI services and emit observations — which actor reached which service, day by day — never events. The reduction runs inside the log store, so raw rows never leave it; only matched hits and day counts ship. Findings land in Registry as unreviewed rows with a Radar badge. A finding proves that something ran, not what it did: only capture produces evidence.
[ { "kind": "entra_signin", "workspace_id": "<Log Analytics workspace GUID with the sign-in export>", "display_name": "Entra — corp tenant", "tenant_id": "...", "client_id": "...", "client_secret": "..." }, { "kind": "proxy_law", "workspace_id": "<workspace GUID>", "table": "ZscalerWeb_CL", "preset": "zscaler", "columns": { "user": "login_s" }, "display_name": "Zscaler — egress", "tenant_id": "...", "client_id": "...", "client_secret": "..." }, { "kind": "openai_usage", "api_key": "<OpenAI org-admin key>", "account_label": "openai-prod", "display_name": "OpenAI — production org" }]entra_signin— the same workspace shape as a capture source, read withLog Analytics Readerscoped to one workspace, never a tenant-wide directory role. Noresource_id.proxy_law— the workspace shape plus the custom table name and a column mapping:presetiszscaler,netskopeorcustom, andcolumnsoverrides any ofurl,host,user,client_ip,user_agent— your feed's real column names win. IP-only traffic is recorded as usage, never registered as an agent.openai_usage/anthropic_usage— the vendor's org-admin key and a stableaccount_labelthat survives key rotation; no workspace fields (the collector refuses them on a vendor source). Anthropic's report carries no request count, so its observations report an honest floor of one call per key, model and day.
COLLECTOR_FLYWHEEL=on. Then, and only then, the pattern classes Radar could not match are sent for review. Terms: DPA §13A.Resilience
- Log Analytics throttling or outage. 429s honour Retry-After (capped); 408/5xx and network errors retry with capped jittered backoff, and a revoked AAD token re-auths once. A source that exhausts its retries records the error on its heartbeat and retries next poll — one broken source never stalls the others.
- Ingest outage or rejected delivery. Undeliverable batches append to a dead-letter file beside the cursor on the volume and replay through the live transport every cycle. Deterministic event IDs mean duplicates collapse server-side, so replay is safe by construction.
- Bad or unregistered signing key. Validated at startup with a loud error and the fix path; capture continues — events dead-letter and replay automatically once the key is registered. Ongoing delivery failure also surfaces per-source on the Collectors page.
- Corrupt state. A corrupt cursor restarts from empty (overlap re-reads are dedup-safe); unparseable dead-letter lines are set aside, never looped and never silently dropped.
Versioning & updates
The Collector has its own semver, independent of the SDKs (it pins a minimum wytness-ai). Images are published through CI only — collector tests, then image build, then an image-layer secret scan, then push to GHCR with the digest recorded in the run. To update, pull the new digest and recreate the container; the volume carries your cursor and any pending dead-letter forward.
Related
- PII Protection — the tokenization the collector applies in your boundary
- Security model — how the customer-held keys compose with AGT identity
- Event schema — the shape of what gets shipped
- Wytness Radar — where discovery findings land, and what they do and do not prove
Bring your Foundry and Copilot Studio activity into your audit trail.
Start recording