Skip to content

feat(sdk-rust): harness identifier for telemetry attribution#161

Merged
willwashburn merged 2 commits into
mainfrom
feat/sdk-rust-harness
Jun 3, 2026
Merged

feat(sdk-rust): harness identifier for telemetry attribution#161
willwashburn merged 2 commits into
mainfrom
feat/sdk-rust-harness

Conversation

@willwashburn

@willwashburn willwashburn commented Jun 3, 2026

Copy link
Copy Markdown
Member

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-Harness and stamps harness on every telemetry event — but the Rust SDK had no way to send it, so its traffic always recorded harness: "unknown".

What

  • Public harness on RelayCastOptions / ClientOptions and WsClientOptions via with_harness(...). A User-Agent-style identifier ("human", "codex", "claude-code/2.3 (model=opus-4.8)"):
    • stamped as the X-Relaycast-Harness header on every HTTP request,
    • forwarded as the harness WS query param on both the initial connect and reconnects (browser WebSocket can't set custom headers).
  • New harness module: sanitize_harness() + HARNESS_HEADER, mirroring the TS/engine contract — trim, lowercase, UA-safe charset, cap 120, drop on empty/invalid/CRLF.
  • Survives HttpClient::with_api_key(...); AgentClient::connect() propagates it to the underlying WsClient.
  • Header/param omitted entirely when unset — existing consumers are unchanged on the wire.

Wire contract (matches @relaycast/sdk + engine)

value
HTTP header X-Relaycast-Harness: claude-code/2.3 (model=opus-4.8)
WS query param ?harness=claude-code/2.3
Normalisation trimmed, lowercased, [a-z0-9 ._-/():=;,+], ≤120; invalid → dropped

Tests

  • 5 sanitize_harness unit tests (UA token lowercased, trim, empty/None dropped, CRLF/control dropped, 120-char cap).
  • A wiremock parity test asserting a mixed-case UA harness arrives lowercased in the X-Relaycast-Harness header end-to-end.
  • cargo test (34 lib + 30 parity + 5 doctests), cargo clippy --all-targets, and cargo fmt --check (on changed files) all green.

Notes

  • Static RelayCast::create_workspace still uses default origin (pre-harness bootstrap context), mirroring the TS decision in feat(telemetry): end-to-end harness attribution (UA-style) #160.
  • Two pre-existing cargo fmt violations remain in examples/basic.rs and the lib.rs types re-export block — untouched here to avoid unrelated churn.
  • Follow-up filed for the Python SDK, which has the same gap.

🤖 Generated with Claude Code


CodeAnt-AI Description

Add a harness identifier for request and telemetry attribution

What Changed

  • Requests can now include a user-facing harness name such as codex, human, or claude-code/2.3 (model=opus-4.8) for telemetry attribution
  • The harness is sent on HTTP requests as X-Relaycast-Harness and on WebSocket connections as the harness query param, including reconnects
  • Empty, invalid, or control-character values are dropped, and the harness is omitted entirely when not set
  • The harness value is preserved when reusing a client with a new API key

Impact

✅ 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:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

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:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

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.

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@codeant-ai

codeant-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Harness identifier telemetry for Rust SDK

Layer / File(s) Summary
Harness utilities and public API
packages/sdk-rust/src/harness.rs, packages/sdk-rust/src/lib.rs
HARNESS_HEADER (X-Relaycast-Harness) and sanitize_harness(...) normalize, validate, truncate, and lowercase harness strings; both are exported at crate root and a small re-export order change was applied.
HTTP client harness storage and header injection
packages/sdk-rust/src/client.rs
ClientOptions and HttpClient now store an optional harness, provide with_harness(...), sanitize on construction, expose harness() accessor, preserve harness during with_api_key(...), and conditionally attach X-Relaycast-Harness to outgoing requests.
RelayCast options harness configuration
packages/sdk-rust/src/relay.rs
RelayCastOptions adds an optional harness with with_harness(...); RelayCast::new forwards the harness into ClientOptions when present.
WebSocket client harness and query parameter injection
packages/sdk-rust/src/ws.rs
WsClientOptions/WsClient accept a harness option, sanitize it on creation, and append it as a harness query parameter on initial and reconnect WebSocket URLs when present.
AgentClient harness propagation
packages/sdk-rust/src/agent.rs
AgentClient::connect forwards the HTTP client's harness into WsClientOptions before creating the WsClient.
Testing and documentation
packages/sdk-rust/tests/parity.rs, packages/sdk-rust/README.md, packages/sdk-rust/CHANGELOG.md, .agentworkforce/..., .trajectories/...
Adds Tokio/Wiremock tests asserting lowercased harness header and WS query param behavior, README usage example, CHANGELOG entry, and trajectory metadata files documenting the review run.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • AgentWorkforce/relaycast#160: Implements the same harness telemetry contract (sanitization and header/WS param forwarding) in related components.

Suggested labels

size:XL

Poem

🐰 I found a thread, so neat and small,
I trimmed and lowered it, lest it fall.
From headers to websockets, a quiet song,
A harness carried swift and strong.
Hops of tests confirm the clue—hooray for you!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main change: adding harness identifier support for telemetry attribution in the Rust SDK.
Description check ✅ Passed The description comprehensively relates to the changeset, detailing the harness identifier feature, its wire contract, implementation across HTTP/WebSocket, sanitization logic, tests performed, and notes on decisions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sdk-rust-harness

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Jun 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +19 to +23
c.is_ascii_alphanumeric()
|| matches!(
c,
' ' | '.' | '_' | '-' | '/' | '(' | ')' | ':' | '=' | ';' | ',' | '+'
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +228 to +230
if let Some(ref harness) = self.harness {
query.append_pair("harness", harness);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
👍 | 👎

Comment on lines +18 to +24
fn is_allowed(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
' ' | '.' | '_' | '-' | '/' | '(' | ')' | ':' | '=' | ';' | ',' | '+'
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
👍 | 👎

Comment on lines +13 to +46
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(),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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

codeant-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/sdk-rust/src/harness.rs
agent-relay-code Bot added a commit that referenced this pull request Jun 3, 2026
@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer applied fixes — committed and pushed 05b3b6d to this PR. The notes below describe what changed.

Reviewed and fixed PR #161.

Changed:

  • Aligned Rust harness sanitization with the engine contract: max 40 chars, [A-Za-z0-9._-], invalid values omitted.
  • Updated Rust docs/changelog/tests to stop advertising server-rejected User-Agent-style values.
  • Fixed engine harness extraction to accept harness query params on WebSocket requests when no valid header is present.
  • Added focused engine tests for header sanitization, WS query fallback, precedence, and invalid values.

Verified:

  • npm test --workspace @relaycast/engine -- origin.test.ts
  • npm run typecheck --workspace @relaycast/engine
  • npm run lint --workspace @relaycast/engine

Could not run Rust formatting/tests because cargo is not installed in this environment.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
"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>
@willwashburn willwashburn force-pushed the feat/sdk-rust-harness branch from 05b3b6d to d69212d Compare June 3, 2026 08:59
@codeant-ai

codeant-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown

CodeAnt AI is running Incremental review

@willwashburn

Copy link
Copy Markdown
Member Author

Conflicts + review comments resolved

TL;DR: the automated reviewers reviewed commit 6493cd3 while #160 was still open, so they saw a contract mismatch that no longer exists. #160 has since merged, and the merge conflict was introduced by an auto-fix commit that has now been reverted.

What the bots flagged (all now stale)

Several bots (codex, codeant, cubic) flagged that harness.rs accepts UA-style values (≤120 chars, charset [a-z0-9 ._-/():=;,+]) while the engine only accepted [a-zA-Z0-9._-] ≤40 — so values would be recorded as unknown. That was true against the old engine contract, but #160 (now merged into main) loosened extractHarness to exactly the UA contract this SDK targets:

  • HARNESS_MAX_LENGTH = 120
  • HARNESS_ALLOWED = /^[a-z0-9 ._\-/():=;,+]+$/i
  • reads the harness query param as well as the header (fixes the WS-query-param concern)

This SDK's sanitize_harness matches that 1:1, so harness values now round-trip end-to-end. The UA-style format is a deliberate design decision, not an oversight.

What caused the merge conflict

agent-relay-code[bot]'s commit 05b3b6d ("apply pr-reviewer fixes") acted on those stale comments and (a) narrowed the Rust sanitizer back to the old 40-char contract — now incorrect — and (b) re-applied #160's engine changes onto this branch. Since #160 already merged those to main, the branch diverged → conflict. That commit also committed package-lock.json churn and trajectory JSON.

I've reverted 05b3b6d and rebased the clean, UA-style implementation onto latest main. The branch is now a single commit touching only packages/sdk-rust/*, conflict-free, with cargo test (34 lib + 30 parity + 5 doctests), clippy, and fmt all green.

cc the auto-reviewers: please re-review against main — the engine contract now matches.

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Jun 3, 2026
@codeant-ai

codeant-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown

CodeAnt AI Incremental review completed.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer applied fixes — committed and pushed 8c63013 to this PR. The notes below describe what changed.

Reviewed PR #161 and fixed the missing regression coverage.

Changes made:

  • Added Rust parity test for HttpClient::with_api_key() preserving sanitized harness headers.
  • Added Rust parity test for WsClientOptions::with_harness() forwarding sanitized harness as a WS query param.
  • Formatted the PR-touched Rust files.

Verification run:

  • cargo build --manifest-path packages/sdk-rust/Cargo.toml
  • cargo test --manifest-path packages/sdk-rust/Cargo.toml
  • cargo clippy --manifest-path packages/sdk-rust/Cargo.toml -- -D warnings
  • rustfmt --edition 2021 --check packages/sdk-rust/tests/parity.rs packages/sdk-rust/src/lib.rs
  • npx turbo lint --filter=relaycast
  • npx turbo test --filter=relaycast

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/sdk-rust/tests/parity.rs (1)

522-625: ⚡ Quick win

Consider 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_harness also drops empty/invalid/CRLF inputs). The added tests only assert the present/lowercased case. A small test asserting the x-relaycast-harness header 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

📥 Commits

Reviewing files that changed from the base of the PR and between d69212d and 8c63013.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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.json
  • packages/sdk-rust/src/lib.rs
  • packages/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

@willwashburn willwashburn merged commit 6d46017 into main Jun 3, 2026
4 of 5 checks passed
@willwashburn willwashburn deleted the feat/sdk-rust-harness branch June 3, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant