feat: emit X-Wherobots-Client header with command name (WBC-182)#32
feat: emit X-Wherobots-Client header with command name (WBC-182)#32ClayMav wants to merge 2 commits into
Conversation
Set an advisory X-Wherobots-Client header on outgoing API requests so
the backend can attribute traffic to the CLI and the specific command
that generated it. The CLI is an ORIGIN client and emits a single hop:
client=cli;ver=<buildVersion>;cmd=<command>
The command is the operation's CommandPath joined with "." (e.g.
job-runs.list); the ;cmd= field is omitted when CommandPath is empty.
Values are sanitized so commas and semicolons can never break the hop
grammar. The header is advisory only and never affects auth.
The build version reaches the executor package via an exported
executor.Version var (default "dev"), which main.go sets from the
ldflags-injected buildVersion. This avoids an import cycle and keeps
version wiring in one place. A pure buildClientHeader helper renders
the value for testability.
Claude-Session: https://claude.ai/code/session_01QP5saxNQTUTJpMRRxZXhzR
|
@salty-hambot rubric |
There was a problem hiding this comment.
Reviewed by Salty Hambot 🤖🧂 — rubric mode
Verdict:
| Dimension | Verdict | Notes |
|---|---|---|
| correctness | Confirmed via jobs.go/builder.go: curated job-runs commands reuse the api-subtree ops from findOperation, whose CommandPath is set to PathToResourceSegments(op.Path)+verb (builder.go:109) — so cmd= misreports job-runs.* invocations as runs.. | |
| security | ✅ pass | Header is advisory-only, sanitized against grammar-breaking chars, and never touches the x-api-key auth path. |
| privacy | ✅ pass | Only CLI version and command name are emitted; no credentials or PII enter the header. |
| reliability | executor.Version is a package-level var set in run(); any future entrypoint or test calling BuildRequest before run() silently emits 'dev' — acknowledged but unguarded. | |
| scalability | ✅ pass | strings.Builder with a few small writes; NewReplacer allocated once at init — no concerns. |
| observability | ✅ pass | Header propagates command attribution and appears automatically in --dry-run curl output — though the cmd= value is wrong for curated commands (see correctness). |
| clarity/maintainability | ✅ pass | Well-documented pure helper; change confined to request.go plus one assignment in main.go; consistent with surrounding style. |
| test quality | Unit and integration tests are solid, but they hand-set CommandPath to []string{"job-runs","list"} and never exercise the curated job-runs command path end-to-end — masking the real misattribution bug. |
Tidy, well-tested header PR — but the reviewer waved through the one real bug: curated job-runs commands share the API-subtree ops, so cmd= emits runs.<verb> instead of the user-facing job-runs.create. The two 'questions' were self-answering nitpicks; dropped them.
0 finding(s) posted · 2 filtered as false positives · 1 skipped as already raised.
💬 To request a re-review, comment @salty-hambot review
Curated `job-runs` commands reuse the shared api-subtree operations returned by findOperation (e.g. `POST /runs`), whose CommandPath was assigned the api-tree name (`runs.<verb>`) in builder.go. Deriving the header's cmd= field from op.CommandPath therefore misattributed curated traffic as `runs.create` / `runs.list` instead of the user-facing `job-runs.create` / `job-runs.list`. Thread the invoked command name through the request context: the `job-runs` parent's PersistentPreRunE stamps the leaf command's dotted user-facing path (curatedCommandName) onto the context, and BuildRequest prefers that over op.CommandPath (falling back to op.CommandPath for the dynamic api subtree, which is already correct). Context propagation covers every request a curated command issues, including internal helper calls (org lookup, upload-url, run polling) without touching their signatures, and avoids mutating the shared operation pointers. Add end-to-end tests exercising the curated path through BuildRootCommand + httptest asserting `cmd=job-runs.create` / `cmd=job-runs.list` on the wire, a guard that `api runs list` still reports `runs.list`, and executor unit tests for the context override and its fallback. Claude-Session: https://claude.ai/code/session_01QP5saxNQTUTJpMRRxZXhzR
|
@salty-hambot rubric |
There was a problem hiding this comment.
Reviewed by Salty Hambot 🤖🧂 — rubric mode
Verdict:
| Dimension | Verdict | Notes |
|---|---|---|
| correctness | ✅ pass | Prior misattribution bug (runs.list vs job-runs.list) is fully fixed via PersistentPreRunE + context override; tests confirm both curated and dynamic paths. |
| security | ✅ pass | Header is advisory-only, values sanitized against comma/semicolon, and the x-api-key auth path is untouched. |
| privacy | ✅ pass | Only CLI version and user-invoked command name are emitted — no credentials or PII. |
| reliability | executor.Version is a package-level var set in run(); a future entrypoint calling BuildRequest before run() would silently emit 'dev' — acknowledged, unguarded, low impact. | |
| scalability | ✅ pass | strings.Builder with a few small writes; NewReplacer allocated once at init — no concerns. |
| observability | ✅ pass | Header now propagates correct user-facing attribution on every request and in --dry-run curl output. |
| clarity/maintainability | ✅ pass | curatedCommandName is a clean pure helper, context-key pattern is idiomatic, and the change is tightly scoped with clear comments. |
| test quality | ✅ pass | Three integration tests cover curated list, curated create, and dynamic api-subtree end-to-end; unit tests cover buildClientHeader and context-preference/fallback — prior masking gap closed. |
Round two and the one real bug is properly dead — curated job-runs commands now stamp their user-facing name onto the context, BuildRequest honors it, and three end-to-end tests actually exercise the routing this time instead of hand-faking CommandPath. The dev-version caveat lingers but it's acknowledged and low-stakes. Ship it.
Prior findings: ✅ 2 resolved ·
0 finding(s) posted.
💬 To request a re-review, comment @salty-hambot review
Summary
Implements WBC-182. The CLI now sets an advisory
X-Wherobots-Clientheader on every outgoing API request so the backend can attribute traffic to the CLI and to the specific command that generated it.The CLI is an ORIGIN client, so it emits a single hop and never appends to an existing value:
cmdis the operation'sCommandPathjoined with.(e.g.job-runs.list). Populated ininternal/commands/builder.goasappend(PathToResourceSegments(op.Path), verb).;cmd=field is omitted whenCommandPathis empty.Implementation
internal/executor/request.goVersionpackage var (default"dev").buildClientHeader(version, command string) stringhelper (empty version falls back todev; empty command omits;cmd=; sanitizes separators).BuildRequest(...)now sets the header right afterx-api-key.main.gosetsexecutor.Version = buildVersionat the top ofrun().Version wiring approach
buildVersionis a package-level var inmain.go, injected via ldflags at build time (default"dev"). It reaches the executor package through an exportedexecutor.Versionvar thatmain.goassigns duringrun()setup. This keeps all version wiring in one place, requires no ldflags changes to the build, and introduces no import cycle (mainalready depends oninternal/executor;internal/executordoes not depend onmainorinternal/commands).Testing (TDD)
Followed RED -> GREEN:
CommandPath(client=cli;ver=<version>;cmd=job-runs.list) and the empty-CommandPathcase (no;cmd=); confirmed compile failure (undefinedVersion/buildClientHeader).go test ./...passes;go build ./...andgo vet ./...clean.New tests in
internal/executor/request_test.go:TestBuildRequestInjectsWherobotsClientHeaderTestBuildRequestWherobotsClientHeaderOmitsCommandWhenEmptyTestBuildClientHeader(table: version+command, empty command, empty version, separator sanitization)The two
BuildRequesttests mutate the package-levelVersionand are deliberately non-parallel (verified clean undergo test -race).Rubric self-assessment
.;;cmd=omitted when empty. Verified by tests.request.go, its test, and one line + import inmain.go. No other submodules or staging/prod.BuildRequeston the returned*http.Request; full suite +-racegreen.x-api-keypath; cannot affect auth.dev.executor.Versionvar set frommain.go; no import cycle; wiring centralized.go fmtclean, matches existingrequest_test.gostyle and repoCLAUDE.md.Caveats
executor.Version = buildVersioninrun()rather than injecting ldflags directly onto the executor var. If a future entrypoint callsBuildRequestwithout going throughrun(), the header would reportdevuntilexecutor.Versionis set.Linear: WBC-182
https://claude.ai/code/session_01QP5saxNQTUTJpMRRxZXhzR