# Gateway request provenance Every request sent to the LLM gateway carries a description of what caused it: the kind of run, the machine it came from, and the session it belongs to. The gateway authenticates upstream with one shared credential per provider and reaches it from one address, so a request arriving there says nothing about which workload produced it. A test, an unattended dispatch, and a person at a terminal are indistinguishable. Provenance is what separates them, in the records the gateway already keeps. The fields, and how each one is obtained, are in [`packages/cf-harness/README.md`](../../packages/cf-harness/README.md#request-provenance). This document holds the invariants behind them. ## One vocabulary, two senders `cf-harness` sends provenance on every gateway request it makes. Toolshed sends it on the gateway requests its `/api/ai/llm` routes and its web search make; [`packages/toolshed/README.md`](../../packages/toolshed/README.md#gateway-request-provenance) says what it reports. Both build their headers from `packages/cf-harness/src/provenance.ts`, which toolshed imports as `@commonfabric/cf-harness/provenance`. One implementation means one sanitizer, and it means a field cannot exist for one sender and not the other. The `User-Agent` opens with the name of the sending program, so the two are told apart by user agent alone. The gateway names each header it handles. A Lua filter copies the names it knows into dynamic metadata, and a patch policy removes those names from the request; both lists are written out in the infra repository, under `k8s/manifests/envoy-gateway/`. Neither is a prefix match. A field added to `provenanceEntries` and not to those lists travels to the model vendor and appears in no log, so a new field is two changes across two repositories. The gateway requires the invoker, principal and session headers on every request to its LLM hostname. It returns HTTP 400 before forwarding upstream when any of the three is absent or empty. Browser CORS preflights do not require provenance. ## Two channels, one set of values The values travel twice: as `x-cf-harness-*` headers, and condensed into the `User-Agent`. The `User-Agent` is the channel that works against the gateway as deployed. Its Envoy access log records the user agent of every request, and nothing else that identifies a caller, and that log reaches Cloud Logging. The headers carry the same values for a reader that names them in an access-log field. ## What a value may contain These headers reach the upstream provider along with the request. Two invariants follow. **A value carries no request content.** Nothing derived from a prompt, a message, a tool argument, or a command line appears in one. Three mechanisms hold this: the `command` field is mapped through a closed set, so an unrecognized harness argument reports `prompt` and a toolshed request reports the name of the operation it is performing; every value passes through a sanitizer, at the one point where values leave, that bounds it to a short run of characters that cannot break a header or carry structure into one; and a Loom run manifest contributes only its dispatch class. The sanitizer bounds a value rather than inspecting it. What keeps request content out is the closed set `command` is drawn from: no other field is fed from a prompt, a message, or a command line. **A value identifies no person.** Where a signal is a unique identifier of a person or of their machine, only its presence is read: a coding agent's marker variable says which agent is running, and its contents stay on the machine. One value does name a single execution. Under continuous integration the `ci` field carries the workflow and run identifier, which resolves to one run in that system's records. That is deliberate, and it is the field's purpose: it is what makes a run's traffic findable afterwards. It names an automated execution, and the identifier is already recorded against that run by the system that produced it. ## The principal is not derivable from the machine A principal names the machine a request came from. It is generated by `crypto.getRandomValues` on first use and kept in the harness home, so nothing about the machine determines it. This is a requirement rather than an artifact of the implementation. The identifying values a machine offers are its account name and its hostname. Across a team those form a space of a few hundred candidates, and a commodity laptop searches about a hundred thousand candidates a second, which puts any value computed from them within milliseconds of recovery. The size of that input space is what governs, so a wider output does not move it. This invariant is also why no real principal appears in any document in this repository. A principal published next to the name of whoever committed it is tied to a person for good. A principal that cannot be stored is reported as `unstable-