Repository navigation
feat(api-proxy): attribute requests to the main agent or a sub-agent via x-awf-caller - #10222
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
Changes to public purpose semantics and security-sensitive ingress paths warrant maintainer sign-off on downstream compatibility.
1 open issue
What changed in this PR
Adds optional caller attribution to API-proxy records, helping consumers distinguish main-agent and sub-agent requests without fragile usage matching.
Changes:
- Parses and strips
x-awf-callerwhile keeping attribution separate from trusted request controls. - Propagates caller fields through HTTP, WebSocket, token, and routing records.
- Updates schemas, documentation, and regression tests.
| File | Description |
|---|---|
| schemas/token-usage.schema.json | Defines caller attribution fields. |
| docs/awf-config-spec.md | Specifies recording and trust rules. |
| docs/api-proxy-sidecar.md | Documents header usage and limitations. |
| containers/api-proxy/websocket-tunnel.js | Passes caller metadata to tracking. |
| containers/api-proxy/upstream-token.test.js | Tests HTTP caller propagation. |
| containers/api-proxy/upstream-token.js | Forwards parsed caller metadata. |
| containers/api-proxy/token-tracker.schema.test.js | Tests attribution and compatibility. |
| containers/api-proxy/token-tracker-ws.js | Records WebSocket caller metadata. |
| containers/api-proxy/token-tracker-http.js | Carries attribution through finalization. |
| containers/api-proxy/token-persistence.js | Applies shared attribution fields. |
| containers/api-proxy/server-factory.test.js | Tests ingress parsing and safe logging. |
| containers/api-proxy/server-factory.js | Parses HTTP and upgrade headers. |
| containers/api-proxy/routing-provider-executor.test.js | Tests trusted classifier attribution. |
| containers/api-proxy/routing-persistence.test.js | Tests caller-field persistence. |
| containers/api-proxy/routing-persistence.js | Allows caller fields in records. |
| containers/api-proxy/routing-observation.test.js | Checks token/routing attribution agreement. |
| containers/api-proxy/routing-observation.js | Adds attribution to request telemetry. |
| containers/api-proxy/request-headers.test.js | Tests caller-header stripping. |
| containers/api-proxy/proxy-utils.js | Marks caller header for removal. |
| containers/api-proxy/Dockerfile | Includes the attribution module. |
| containers/api-proxy/caller-header.test.js | Tests parsing and trusted-purpose precedence. |
| containers/api-proxy/caller-header.js | Centralizes validation and attribution mapping. |
Review effort: Max
| startTime, | ||
| metrics, | ||
| requestModel: requestModel || extractRequestModelFromUrl(req.url), | ||
| caller: req.awfCaller || null, |
|
@copilot address review feedback |
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Addressed in 5e081d6. Added a successful CONNECT tunnel regression verifying the tracker receives req.awfCaller and the serialized upgrade strips x-awf-caller using the real header filter. All 168 targeted tests pass; secret scanning is clean. Read-only review found no issues after automated review was unavailable. CI logs confirm main’s unrelated duplicate EventEmitter declaration remains. |
|
✅ Copilot review passed with no inline comments. @copilot Add the |
Documentation PreviewDocumentation has been built for this PR. To view locally:
Built from commit 658cfad |
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (2 files)
Coverage comparison generated by |
|
@copilot fix the failing ci check https://github.com/github/gh-aw-firewall/actions/runs/39168061206/job/117795554691?pr=10222 |
Removed the duplicate |
|
🔑 Smoke Copilot BYOK AOAI (api-key) is testing Azure OpenAI BYOK (api-key) mode on this pull request... |
|
🛡️ Smoke Copilot Network Isolation confirmed the egress allowlist is enforced. ✅
|
|
✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟
|
|
✅ Build Test Suite completed successfully!
|
|
🪪 Smoke Copilot BYOK AOAI (Entra) is testing Azure OpenAI BYOK (Entra / GitHub OIDC) mode on this pull request... |
|
Chroot tests passed! Smoke Chroot - All security and functionality tests succeeded.
|
|
✅ Smoke Claude passed
|
|
Cloud Hypervisor + Copilot smoke test
|
|
EGRESS_RESULT allow=pass deny=pass
|
Smoke Test: Claude Engine Validation
Overall result: PASS
|
|
Services smoke test: ✅ Redis PONG, ✅ pg_isready, ✅ SELECT 1. Overall: PASS
|
|
Smoke test (Copilot):
Overall: PASS
|
|
Smoke Test: Copilot BYOK (Direct) Mode ✅ PASS
Running in direct BYOK mode (COPILOT_PROVIDER_API_KEY) via api-proxy sidecar.
|
|
OTEL smoke test
|
Smoke Test
|
Chroot Version Comparison
Result: Not all tests passed (Node.js mismatch), so the
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — PASS Notes: the Bun and Deno install scripts were blocked by the sandbox, so I used the npm
|

AWF records don't say which agent made a request:
purposeexists only for router classifier calls. Downstream consumers therefore match AWF token records to engine-reported sub-agent requests by model, token counts and order, which breaks easily. This PR adds an optionalx-awf-callerrequest header. AWF parses it at ingress, always strips it before forwarding upstream, and records it as attribution metadata ontoken-usage.jsonlandmodel-routing.jsonlrequest records.Header contract
mainorsubagent/<agentName>, at most 128 characters, and the whole value must match^(?:main|subagent/[A-Za-z0-9_.:/-]+)$. AWF treats any other value as absent.caller_header_rejectedwith a fixedreason(too_long/invalid_format/not_a_string).STRIPPED_HEADERS, so it is never sent upstream, valid or not. That coversbuildRequestHeaders, cross-provider fallback and the WebSocket tunnel.Ingress
caller-header.js(added to the Dockerfile COPY list) withparseCallerHeader,callerHeaderRejectionandcallerRecordFields.server-factory.jsparses the header once intoreq.awfCaller: in the HTTP handler beforerouting?.observeRequest, and in the upgrade handler.req.awfCalleris kept separate from the trustedreq.awfRequestContext. Steering, guards, budget accounting, the classifier abort signal and response handling still read onlyawfRequestContext.purpose. The header cannot change behavior.Recording
callerRecordFields(awfPurpose, caller)is the single source for both record types, so the two always agree for the samerequest_id:purposeagent_namecaller_sourcex-awf-caller: mainagentcaller_headerx-awf-caller: subagent/<name>subagent<name>caller_headerrouting_classificationawfupstream-token.js→trackTokenUsage→buildAndWriteTokenRecord, and throughwebsocket-tunnel.js→token-tracker-ws.js. With no valid caller and no AWF purpose, the record is unchanged frommain.caller_source: "awf".requestTelemetryadds the same fields, andagent_nameandcaller_sourceare added toFIELDSinrouting-persistence.js.Schema and docs
schemas/token-usage.schema.json:purposeenum is nowrouting_classification | agent | subagent.agent_name(pattern^[A-Za-z0-9_.:/-]+$, maxLength 119).caller_source(caller_header | awf).docs/api-proxy-sidecar.mdhas a new "Caller attribution" section, anddocs/awf-config-spec.md§13a.1 gains the request-record rules. Both document the trust model: the header is an unauthenticated declaration from inside the sandbox. Consumers should take credits, token counts and served models from AWF's own record and use the caller fields only for attribution.Acceptance cases → tests
token-tracker.schema.test.js›buildTokenUsageRecord records a sub-agent callerupstream-token.test.js›passes the parsed x-awf-caller from the request to trackTokenUsagerequest-headers.test.js›buildRequestHeaders strips x-awf-caller before forwarding upstreamtoken-tracker.schema.test.js›buildTokenUsageRecord records the main-agent caller without agent_nametoken-tracker.schema.test.js›buildTokenUsageRecord is unchanged when no caller is present. It uses a fixed clock and a strict deep-equal againstmain's output.caller-header.test.js›parseCallerHeader rejects invalid values(table-driven)request-headers.test.js›buildRequestHeaders strips an invalid x-awf-callertoken-tracker.schema.test.js›buildTokenUsageRecord prefers the AWF purpose over a caller headerrouting-provider-executor.test.js›classifier requests carry no x-awf-caller and record caller_source awfrouting-observation.test.js›request records carry the same caller fields as the token recordrouting-persistence.test.js›persists agent_name and caller_source on request recordsAdditional tests:
server-factory.test.js: header parsing for HTTP and upgrade requests, including a check that a rejected value never reaches the log.token-tracker.schema.test.js: records written through the HTTP and WebSocket tracking paths.Known failure on
maincontainers/api-proxy/upstream-http.test.jsfails to parse onmain(Identifier 'EventEmitter' has already been declared). It is unrelated to this change and not fixed here. The latest Build Verification runs onmainare also failing; their logs haven't been checked, so this test hasn't been confirmed as the cause.