feat(sdk-rust): harness identifier for telemetry attribution#161
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
CodeAnt AI is reviewing your PR. |
📝 WalkthroughWalkthroughThe Rust SDK adds an optional harness identifier: sanitization utilities and crate exports; ClientOptions/HttpClient and RelayCastOptions gains harness builders/storage; WsClient propagates harness as a query parameter; AgentClient forwards harness to WsClient; tests, README, and CHANGELOG updated. ChangesHarness identifier telemetry for Rust SDK
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6493cd3205
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| c.is_ascii_alphanumeric() | ||
| || matches!( | ||
| c, | ||
| ' ' | '.' | '_' | '-' | '/' | '(' | ')' | ':' | '=' | ';' | ',' | '+' | ||
| ) |
There was a problem hiding this comment.
Match the server's harness sanitization contract
The Rust SDK now accepts UA-style values here, but the current engine sanitizer in packages/engine/src/lib/origin.ts::extractHarness only accepts ^[a-zA-Z0-9._-]+$ and rejects values over 40 chars. As a result, documented values such as claude-code/2.3 (model=opus-4.8) are sent by the SDK but recorded by the server as unknown, defeating the telemetry attribution this option is meant to provide. Please either constrain the SDK to the server contract or update the engine contract in the same change.
Useful? React with 👍 / 👎.
| if let Some(ref harness) = self.harness { | ||
| query.append_pair("harness", harness); | ||
| } |
There was a problem hiding this comment.
Send WS harness where the engine reads it
Appending harness as a WebSocket query parameter does not currently affect telemetry: I checked the engine WS paths, and the Hono route uses c.get('harness') populated from request headers, while the Node upgrade path reads only token before calling attachAgentSocket/attachWorkspaceSocket; repo-wide search found no searchParams.get('harness'). In hosted or self-hosted WS sessions, with_harness(...) therefore still records unknown unless the Rust client sends a header or the engine is updated to read and sanitize this query param.
Useful? React with 👍 / 👎.
| pub const HARNESS_HEADER: &str = "X-Relaycast-Harness"; | ||
|
|
||
| /// Upper bound on the harness value — generous enough for a UA-style token. | ||
| const HARNESS_MAX_LENGTH: usize = 120; |
There was a problem hiding this comment.
Suggestion: The client caps harness values at 120 characters, but the server-side extractor currently rejects anything longer than 40, so long harnesses will still be recorded as unknown. Align the SDK cap with the engine's accepted maximum (or update engine and SDK together) to avoid silent telemetry attribution loss. [api mismatch]
Severity Level: Major ⚠️
- ⚠️ Harness telemetry dropped for identifiers longer than 40 characters.
- ⚠️ Engine cannot distinguish long-named harnesses in analytics.
- ⚠️ Rust SDK appears to send harness but backend discards.Steps of Reproduction ✅
1. In the Rust SDK, construct HTTP client options using
`ClientOptions::new(...).with_harness(...)` in `packages/sdk-rust/src/client.rs:36-72`,
passing a harness string longer than 40 characters but shorter than 120 (e.g.
`"my-harness-name-with-extra-version-and-model-params-123456"`).
2. `HttpClient::new` at `packages/sdk-rust/src/client.rs:106-127` calls
`sanitize_harness(options.harness)` from `packages/sdk-rust/src/harness.rs:31-47`, which
trims, validates with `is_allowed`, and caps the value at `HARNESS_MAX_LENGTH` (120)
defined at `packages/sdk-rust/src/harness.rs:12-13`, so the >40-character harness is
accepted unchanged.
3. Any subsequent HTTP call via `HttpClient::request` at
`packages/sdk-rust/src/client.rs:175-233` builds a request with `build_request`, which
unconditionally adds the header `X-Relaycast-Harness` when `self.harness` is `Some`, at
`packages/sdk-rust/src/client.rs:235-247`, sending the long harness to the Relaycast
engine.
4. On the engine side, telemetry code in `packages/engine/src/lib/serverTelemetry.ts:7-20`
calls `extractHarness(c.req.raw.headers)` from `packages/engine/src/lib/origin.ts:28-40`;
`extractHarness` declares `const HARNESS_MAX_LENGTH = 40;` at line 15 and returns
`UNKNOWN_HARNESS` when `trimmed.length > HARNESS_MAX_LENGTH` at line 34, so the
>40-character harness from the Rust SDK is dropped and telemetry events record `harness:
'unknown'` instead of the supplied identifier.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** packages/sdk-rust/src/harness.rs
**Line:** 13:13
**Comment:**
*Api Mismatch: The client caps harness values at 120 characters, but the server-side extractor currently rejects anything longer than 40, so long harnesses will still be recorded as `unknown`. Align the SDK cap with the engine's accepted maximum (or update engine and SDK together) to avoid silent telemetry attribution loss.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| fn is_allowed(c: char) -> bool { | ||
| c.is_ascii_alphanumeric() | ||
| || matches!( | ||
| c, | ||
| ' ' | '.' | '_' | '-' | '/' | '(' | ')' | ':' | '=' | ';' | ',' | '+' | ||
| ) | ||
| } |
There was a problem hiding this comment.
Suggestion: This sanitizer allows spaces, slashes, parentheses, colons, semicolons, commas, and plus signs, but the current engine harness parser only accepts [a-zA-Z0-9._-]+; values accepted here will be downgraded to unknown by the backend. Restrict the allowed character set to match the server parser (or coordinate a simultaneous server change) so telemetry attribution is consistent end-to-end. [api mismatch]
Severity Level: Critical 🚨
- ❌ UA-style harness values from Rust always downgraded to unknown.
- ❌ Backend telemetry cannot segment Rust SDK traffic by harness.
- ⚠️ Cross-SDK harness analytics inconsistent between Rust and TypeScript.Steps of Reproduction ✅
1. In a Rust client, configure `ClientOptions` or `WsClientOptions` with a
User-Agent-style harness such as `"Claude-Code/2.3 (model=Opus-4.8; fast)"` as shown in
the harness docs comment at `packages/sdk-rust/src/harness.rs:3-7`, via
`ClientOptions::with_harness` (`packages/sdk-rust/src/client.rs:68-72`) or
`WsClientOptions::with_harness` (`packages/sdk-rust/src/ws.rs:89-93`).
2. The Rust sanitizer `sanitize_harness` at `packages/sdk-rust/src/harness.rs:31-47` trims
the string, then checks every character with `is_allowed` at
`packages/sdk-rust/src/harness.rs:18-24`, which permits spaces, slashes, parentheses,
colons, equals, semicolons, commas, and plus signs in addition to alphanumerics and `._-`,
so the example string containing `/`, spaces, `(`, `)`, `=`, and `;` is accepted and
lowercased.
3. For HTTP calls, `HttpClient::new` stores this sanitized harness at
`packages/sdk-rust/src/client.rs:106-127`, and `build_request` at
`packages/sdk-rust/src/client.rs:235-247` sends it on every request as the
`X-Relaycast-Harness` header; for WebSockets, `WsClient::new` stores the sanitized harness
at `packages/sdk-rust/src/ws.rs:148-175` and `WsClient::connect` appends it as the
`harness` query param at `packages/sdk-rust/src/ws.rs:220-230` and again on reconnect at
`packages/sdk-rust/src/ws.rs:14-23` (reconnect loop).
4. On the engine, `extractHarness` in `packages/engine/src/lib/origin.ts:28-40` enforces a
stricter charset: it requires `^[a-zA-Z0-9._-]+$` at line 38 after trimming and length
checks, so any harness containing spaces, `/`, parentheses, `=`, `;`, commas, or plus
signs is rejected and mapped to `UNKNOWN_HARNESS` in `emitServerEvent`
(`packages/engine/src/lib/serverTelemetry.ts:7-20`), causing telemetry events for Rust SDK
traffic using the documented UA-style harness format to record `harness: 'unknown'`
despite the SDK accepting and sending the value.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** packages/sdk-rust/src/harness.rs
**Line:** 18:24
**Comment:**
*Api Mismatch: This sanitizer allows spaces, slashes, parentheses, colons, semicolons, commas, and plus signs, but the current engine harness parser only accepts `[a-zA-Z0-9._-]+`; values accepted here will be downgraded to `unknown` by the backend. Restrict the allowed character set to match the server parser (or coordinate a simultaneous server change) so telemetry attribution is consistent end-to-end.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| const HARNESS_MAX_LENGTH: usize = 120; | ||
|
|
||
| /// Characters permitted in a harness identifier. Deliberately broad enough for a | ||
| /// User-Agent-style token (`name/version (model=...; setting)`) while excluding | ||
| /// CR/LF and other control characters. | ||
| fn is_allowed(c: char) -> bool { | ||
| c.is_ascii_alphanumeric() | ||
| || matches!( | ||
| c, | ||
| ' ' | '.' | '_' | '-' | '/' | '(' | ')' | ':' | '=' | ';' | ',' | '+' | ||
| ) | ||
| } | ||
|
|
||
| /// Normalize a caller-supplied harness identifier to the wire contract. | ||
| /// | ||
| /// Returns a trimmed, lowercased, length-capped token, or `None` when the input | ||
| /// is empty or contains disallowed characters — in which case callers omit the | ||
| /// header/param entirely rather than sending garbage the server would reject. | ||
| pub fn sanitize_harness(raw: Option<impl AsRef<str>>) -> Option<String> { | ||
| let raw = raw?; | ||
| let trimmed = raw.as_ref().trim(); | ||
| if trimmed.is_empty() { | ||
| return None; | ||
| } | ||
| if !trimmed.chars().all(is_allowed) { | ||
| return None; | ||
| } | ||
| Some( | ||
| trimmed | ||
| .chars() | ||
| .take(HARNESS_MAX_LENGTH) | ||
| .collect::<String>() | ||
| .to_lowercase(), | ||
| ) |
There was a problem hiding this comment.
🟠 Architect Review — HIGH
The Rust harness sanitizer allows UA-style strings up to 120 characters with separators like spaces, '/', '(', ')', ':', '=', ';', ',', and '+', but the engine's extractHarness only accepts values matching ^[a-zA-Z0-9._-]+$ and length ≤ 40, so documented examples such as claude-code/2.3 (model=opus-4.8) will still be normalized to unknown on the server.
Suggestion: Align the Rust SDK's sanitize_harness() contract (length and allowed characters) with the engine's extractHarness implementation in packages/engine/src/lib/origin.ts, or update the engine and shared contract together so UA-style harness values are actually accepted and recorded instead of falling back to unknown.
Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is an **Architect / Logical Review** comment left during a code review. These reviews are first-class, important findings — not optional suggestions. Do NOT dismiss this as a 'big architectural change' just because the title says architect review; most of these can be resolved with a small, localized fix once the intent is understood.
**Path:** packages/sdk-rust/src/harness.rs
**Line:** 13:46
**Comment:**
*HIGH: The Rust harness sanitizer allows UA-style strings up to 120 characters with separators like spaces, '/', '(', ')', ':', '=', ';', ',', and '+', but the engine's extractHarness only accepts values matching `^[a-zA-Z0-9._-]+$` and length ≤ 40, so documented examples such as `claude-code/2.3 (model=opus-4.8)` will still be normalized to `unknown` on the server.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
If a suggested approach is provided above, use it as the authoritative instruction. If no explicit code suggestion is given, you MUST still draft and apply your own minimal, localized fix — do not punt back with 'no suggestion provided, review manually'. Keep the change as small as possible: add a guard clause, gate on a loading state, reorder an await, wrap in a conditional, etc. Do not refactor surrounding code or expand scope beyond the finding.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
CodeAnt AI finished reviewing your PR. |
There was a problem hiding this comment.
1 issue found across 9 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
✅ pr-reviewer applied fixes — committed and pushed Reviewed and fixed PR #161. Changed:
Verified:
Could not run Rust formatting/tests because |
There was a problem hiding this comment.
1 issue found across 14 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".agentworkforce/trajectories/active/traj_1774522548467_9eeddc03/trajectory.json">
<violation number="1" location=".agentworkforce/trajectories/active/traj_1774522548467_9eeddc03/trajectory.json:325">
P3: The `content` field in the decision event duplicates the same text twice (`"...: ..."`). This is a copy-paste/concatenation error — the value should appear once.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| { | ||
| "ts": 1780468523123, | ||
| "type": "decision", | ||
| "content": "Accepted harness query params through engine extraction: Accepted harness query params through engine extraction", |
There was a problem hiding this comment.
P3: The content field in the decision event duplicates the same text twice ("...: ..."). This is a copy-paste/concatenation error — the value should appear once.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .agentworkforce/trajectories/active/traj_1774522548467_9eeddc03/trajectory.json, line 325:
<comment>The `content` field in the decision event duplicates the same text twice (`"...: ..."`). This is a copy-paste/concatenation error — the value should appear once.</comment>
<file context>
@@ -318,8 +318,41 @@
+ {
+ "ts": 1780468523123,
+ "type": "decision",
+ "content": "Accepted harness query params through engine extraction: Accepted harness query params through engine extraction",
+ "raw": {
+ "question": "Accepted harness query params through engine extraction",
</file context>
| "content": "Accepted harness query params through engine extraction: Accepted harness query params through engine extraction", | |
| "content": "Accepted harness query params through engine extraction", |
…with @relaycast/sdk)
Brings the Rust SDK to parity with the TypeScript SDK's harness support. The
relaycast engine already reads X-Relaycast-Harness and stamps `harness` on every
telemetry event; the Rust SDK previously had no way to send it.
- Public `harness` on RelayCastOptions/ClientOptions and WsClientOptions via
with_harness(). A UA-style identifier ('human', 'codex',
'claude-code/2.3 (model=opus-4.8)') sent as the X-Relaycast-Harness header on
every HTTP request and forwarded as the `harness` WS query param (connect +
reconnect; browsers can't set custom WS headers).
- New harness module: sanitize_harness() + HARNESS_HEADER, mirroring the TS/engine
contract (trim, lowercase, UA-safe charset, cap 120, drop on invalid/CRLF).
- Survives HttpClient::with_api_key(); AgentClient propagates it to its WsClient.
- Header omitted entirely when unset — existing consumers unchanged on the wire.
Tests: 5 sanitize unit tests + a wiremock parity test asserting the header is
stamped and lowercased end-to-end. cargo test (34 lib + 30 parity + 5 doctests),
clippy, and fmt (changed files) all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
05b3b6d to
d69212d
Compare
|
CodeAnt AI is running Incremental review |
Conflicts + review comments resolvedTL;DR: the automated reviewers reviewed commit What the bots flagged (all now stale)Several bots (codex, codeant, cubic) flagged that
This SDK's What caused the merge conflict
I've reverted cc the auto-reviewers: please re-review against |
|
CodeAnt AI Incremental review completed. |
|
✅ pr-reviewer applied fixes — committed and pushed Reviewed PR #161 and fixed the missing regression coverage. Changes made:
Verification run:
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/sdk-rust/tests/parity.rs (1)
522-625: ⚡ Quick winConsider adding negative-path coverage for the "omitted when unset/invalid" contract.
The PR contract states the harness is omitted entirely from the wire when unset (and
sanitize_harnessalso drops empty/invalid/CRLF inputs). The added tests only assert the present/lowercased case. A small test asserting thex-relaycast-harnessheader is absent (e.g.header_is_missing) when no harness is configured — and/or that an invalid harness is dropped — would lock down the omit branch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/sdk-rust/tests/parity.rs` around lines 522 - 625, Add negative-path tests that assert the harness is omitted when unset or invalid: create tests (e.g., client_omits_harness_when_unset and client_drops_invalid_harness) that start MockServer and use RelayCast::new/RelayCastOptions and HttpClient::new/ClientOptions without calling with_harness (or with an invalid value like "" or "\r\n") and assert the mock expects header_is_missing("x-relaycast-harness") when calling list_channels/get; similarly add a ws test (e.g., ws_client_omits_harness_query_param) using WsClient::new/WsClientOptions without with_harness and verify the captured WS URI has no "harness" query pair after connect. Ensure tests cover both unset and sanitized-invalid inputs and use the same helper methods used in existing tests (list_channels, get, connect) to trigger requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/sdk-rust/tests/parity.rs`:
- Around line 522-625: Add negative-path tests that assert the harness is
omitted when unset or invalid: create tests (e.g.,
client_omits_harness_when_unset and client_drops_invalid_harness) that start
MockServer and use RelayCast::new/RelayCastOptions and
HttpClient::new/ClientOptions without calling with_harness (or with an invalid
value like "" or "\r\n") and assert the mock expects
header_is_missing("x-relaycast-harness") when calling list_channels/get;
similarly add a ws test (e.g., ws_client_omits_harness_query_param) using
WsClient::new/WsClientOptions without with_harness and verify the captured WS
URI has no "harness" query pair after connect. Ensure tests cover both unset and
sanitized-invalid inputs and use the same helper methods used in existing tests
(list_channels, get, connect) to trigger requests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 18403eb1-e63b-4294-b36b-d51248f3ef83
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
.agentworkforce/trajectories/completed/2026-06/traj_phcp4f04b31o/summary.md.agentworkforce/trajectories/completed/2026-06/traj_phcp4f04b31o/trajectory.json.trajectories/active/traj_qs5trmxsz90l/trajectory.jsonpackages/sdk-rust/src/lib.rspackages/sdk-rust/tests/parity.rs
✅ Files skipped from review due to trivial changes (2)
- .agentworkforce/trajectories/completed/2026-06/traj_phcp4f04b31o/trajectory.json
- .trajectories/active/traj_qs5trmxsz90l/trajectory.json
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/sdk-rust/src/lib.rs
User description
Brings the Rust SDK to parity with the TypeScript SDK's harness support (companion to #160). The relaycast engine already reads
X-Relaycast-Harnessand stampsharnesson every telemetry event — but the Rust SDK had no way to send it, so its traffic always recordedharness: "unknown".What
harnessonRelayCastOptions/ClientOptionsandWsClientOptionsviawith_harness(...). A User-Agent-style identifier ("human","codex","claude-code/2.3 (model=opus-4.8)"):X-Relaycast-Harnessheader on every HTTP request,harnessWS query param on both the initial connect and reconnects (browserWebSocketcan't set custom headers).harnessmodule:sanitize_harness()+HARNESS_HEADER, mirroring the TS/engine contract — trim, lowercase, UA-safe charset, cap 120, drop on empty/invalid/CRLF.HttpClient::with_api_key(...);AgentClient::connect()propagates it to the underlyingWsClient.Wire contract (matches
@relaycast/sdk+ engine)X-Relaycast-Harness: claude-code/2.3 (model=opus-4.8)?harness=claude-code/2.3[a-z0-9 ._-/():=;,+], ≤120; invalid → droppedTests
sanitize_harnessunit tests (UA token lowercased, trim, empty/None dropped, CRLF/control dropped, 120-char cap).X-Relaycast-Harnessheader end-to-end.cargo test(34 lib + 30 parity + 5 doctests),cargo clippy --all-targets, andcargo fmt --check(on changed files) all green.Notes
RelayCast::create_workspacestill uses default origin (pre-harness bootstrap context), mirroring the TS decision in feat(telemetry): end-to-end harness attribution (UA-style) #160.cargo fmtviolations remain inexamples/basic.rsand thelib.rstypes re-export block — untouched here to avoid unrelated churn.🤖 Generated with Claude Code
CodeAnt-AI Description
Add a harness identifier for request and telemetry attribution
What Changed
codex,human, orclaude-code/2.3 (model=opus-4.8)for telemetry attributionX-Relaycast-Harnessand on WebSocket connections as theharnessquery param, including reconnectsImpact
✅ Clearer telemetry attribution✅ Fewer unknown-client events✅ Consistent harness tracking across HTTP and WebSocket requests💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.