feat(harness): register macrod harnesses with device-code pairing - #6030
feat(harness): register macrod harnesses with device-code pairing#6030ehayes2000 wants to merge 1 commit into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
a058fa9 to
c30665b
Compare
15c6d5a to
219f42e
Compare
c30665b to
b7004f9
Compare
219f42e to
03cd1ef
Compare
b7004f9 to
6423ae3
Compare
03cd1ef to
0039f32
Compare
63c61c9 to
56236ae
Compare
|
Re: cursor[bot]'s "Team delete blocked by harness FKs" — confirmed and fixed in 56236ae. Verified empirically on a scratch DB before fixing: an Fix, in the squashed migration:
Revalidated: migration up/down/up on a fresh DB, and the team-delete repro with all three blockers present now succeeds, leaving the personal agent unbound. |
| /// | ||
| /// Kept minimal on purpose: the bots domain only needs enough to decide | ||
| /// whether a caller may bind an agent to the harness. | ||
| #[derive(Debug, Clone, PartialEq, Eq)] |
There was a problem hiding this comment.
this is really stinky. why are we calling this "HarnessFacts" why isn't this an enum with variants TeamOwned and TeamOwned?
0039f32 to
f33e121
Compare
56236ae to
cdc0282
Compare
| match self { | ||
| Self::User(user) => Some(user), | ||
| Self::Bot(bot) => bot.acting_user.as_ref(), | ||
| Self::Harness(harness) => Some(&harness.acting_user), |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: HIGH
Harness authentication always supplies a verified acting_user, and MacroAuthorization::acting_user() now returns that user for every harness request. document_storage_service and agent-session accept harness bearer tokens on the shared auth stack. Extractors that special-case Bot but fall through to acting_user() (document, chat, channel, history, reminder, foreign-entity, agent-session) therefore authorize the daemon as that user. ActingUser-gated routes do the same. Team harnesses accept x-macro-harness-for-macro-user-id for any current teammate; managed session create skips the harness-binding check.
Impact: A paired or stolen harness token can read and mutate the owner’s (or any current teammate’s) documents, chats, and agent sessions, open managed cloud sessions, and send prompts — far beyond agents bound to that harness.
Reviewed by Cursor Security Reviewer for commit cdc0282. Configure here.
| { | ||
| return MacroUserIdStr::try_from(claimed) | ||
| .map_err(|_| CreateSessionApiError::UnparseableOwner); | ||
| } |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: HIGH
For harness callers, CreateAgentSessionRequest.owner is parsed and used with no check against harness ownership or the verified acting user. Tests lock in that a bound-agent session can be owned by an arbitrary STRANGER. Session create then grants that user owner entity access while the runtime remains the caller’s daemon.
Impact: Anyone holding a harness token can plant agent sessions in another user’s account. If that user opens or prompts the session, their messages are served by the attacker’s machine.
Reviewed by Cursor Security Reviewer for commit cdc0282. Configure here.
| .context("failed to register this harness's trigger feed")?; | ||
| if let Some(feed) = &initial { | ||
| *signing_secret.write().expect("signing secret lock") = feed.signing_secret.clone(); | ||
| } |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
After pairing, macrod serves POST /macro-events even when no agents are bound and ensure_feed returns None, leaving the webhook HMAC secret as an empty string. webhook_signature::verify accepts HMAC-SHA256 keyed by that empty secret. Unbinding later clears the feed id but does not clear the in-memory secret; the empty-key window is the initial unbound path (and up to the first reconcile after a bot is bound).
Impact: A network attacker who can reach the daemon port can mint a valid empty-key signature and drive session create/prompt using the paired harness credential.
Reviewed by Cursor Security Reviewer for commit cdc0282. Configure here.
| Json(req): Json<CreatePairingRequest>, | ||
| ) -> Result<(StatusCode, Json<CreatedPairing>), HarnessesHandlerErr> { | ||
| let pairing = state.service.create_pairing(req).await?; | ||
| Ok((StatusCode::CREATED, Json(pairing))) |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
POST /harness-pairings is unauthenticated by design, and the only throttle is a deployment-wide cap of 32 open pairings (and 4 per requested name) with a 15-minute TTL. There is no per-client or per-IP limit on this handler.
Impact: An anonymous caller can fill those slots and refresh them as they expire, blocking legitimate device pairing for the whole deployment.
Reviewed by Cursor Security Reviewer for commit cdc0282. Configure here.
| } | ||
|
|
||
| #[tracing::instrument(skip(self), err)] | ||
| async fn get_pairing(&self, code: &str) -> Result<PairingDetails, HarnessError> { |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
get_pairing (and approve_pairing, which logs code and caller) uses tracing::instrument that does not skip the pairing code. That code is the user-facing device secret. create_pairing correctly uses skip_all.
Impact: Anyone with application traces during the 15-minute window can recover codes and approve the pairing onto their own account, binding the victim’s daemon.
Reviewed by Cursor Security Reviewer for commit cdc0282. Configure here.
cdc0282 to
ddb36a1
Compare
f33e121 to
09e06fe
Compare
| match self { | ||
| Self::User(user) => Some(user), | ||
| Self::Bot(bot) => bot.acting_user.as_ref(), | ||
| Self::Harness(harness) => Some(&harness.acting_user), |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: HIGH
Harness authentication always supplies a verified acting_user, and MacroAuthorization::acting_user() now returns that user for every harness request. Document storage and agent-session accept harness bearer tokens on the shared auth stack. Extractors that fall through to acting_user() (document, chat, channel, history, reminder, webhook, sandbox-size, and agent-session) therefore authorize the daemon as that user. Team harnesses accept x-macro-harness-for-macro-user-id for any current teammate. Managed session create skips harness-binding checks, and session control uses the same acting-user grants.
Impact: A private harness token exercises the owner’s grants on those routes. A team daemon can impersonate any teammate. A paired credential can open and prompt managed/cloud sessions and other sessions that user already owns, not only agents bound to that harness.
Reviewed by Cursor Security Reviewer for commit ddb36a1. Configure here.
| && let Some(claimed) = claimed | ||
| { | ||
| return MacroUserIdStr::try_from(claimed) | ||
| .map_err(|_| CreateSessionApiError::UnparseableOwner); |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: HIGH
For harness callers, CreateAgentSessionRequest.owner is parsed and used with no check against harness ownership or the verified acting user. Tests lock in that a bound-agent session can be owned by an arbitrary STRANGER. Session create then grants that user owner entity access while the runtime connection remains the caller’s daemon.
Impact: Anyone holding a harness token can plant agent sessions in another user’s account. If that user opens or prompts the session, their messages are served by the attacker’s machine.
Reviewed by Cursor Security Reviewer for commit ddb36a1. Configure here.
| .context("failed to register this harness's trigger feed")?; | ||
| if let Some(feed) = &initial { | ||
| *signing_secret.write().expect("signing secret lock") = feed.signing_secret.clone(); | ||
| } |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
After pairing, macrod serves POST /macro-events on 0.0.0.0 even when no agents are bound and ensure_feed returns None, leaving the webhook HMAC secret as an empty string. webhook_signature::verify accepts HMAC-SHA256 keyed by that empty secret.
Impact: A network attacker who can reach the daemon port can mint a valid empty-key signature and drive session create/prompt using the paired harness credential.
Reviewed by Cursor Security Reviewer for commit ddb36a1. Configure here.
| Json(req): Json<CreatePairingRequest>, | ||
| ) -> Result<(StatusCode, Json<CreatedPairing>), HarnessesHandlerErr> { | ||
| let pairing = state.service.create_pairing(req).await?; | ||
| Ok((StatusCode::CREATED, Json(pairing))) |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
POST /harness-pairings is unauthenticated by design, and the only throttle is a deployment-wide cap of 32 open pairings (and 4 per requested name) with a 15-minute TTL. There is no per-client or per-IP limit on this handler.
Impact: An anonymous caller can fill those slots and refresh them as they expire, blocking legitimate device pairing for the whole deployment.
Reviewed by Cursor Security Reviewer for commit ddb36a1. Configure here.
| } | ||
|
|
||
| #[tracing::instrument(skip(self), err)] | ||
| async fn get_pairing(&self, code: &str) -> Result<PairingDetails, HarnessError> { |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
get_pairing (and approve_pairing, which logs code and caller) uses tracing::instrument that does not skip the pairing code. That code is the user-facing device secret. create_pairing correctly uses skip_all.
Impact: Anyone with application traces during the 15-minute window can recover codes and approve the pairing onto their own account, binding the victim’s daemon.
Reviewed by Cursor Security Reviewer for commit ddb36a1. Configure here.
09e06fe to
8075f84
Compare
ddb36a1 to
35d54f1
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 35d54f1. Configure here.
Makes user-run macrod daemons first-class harnesses: a harnesses entity (private or team-owned), device-code pairing that mints a harness credential, agents bound to a registered harness with ownership checks, the runtime gateway rekeyed from bot to harness so one daemon serves every bound agent, and the Settings UI to approve pairings and manage harnesses. The macrod TOML is credential-free; identity comes from pairing, with scope (private/team) requestable from config.
35d54f1 to
7fe2598
Compare
8075f84 to
8381f6a
Compare


Makes user-run macrod daemons first-class private/team harnesses: device-code pairing mints the daemon's credential, agents bind to a registered harness, the runtime gateway is keyed by harness so one daemon serves every bound agent, and Settings gains the approval and management UI. Stacked on #6005.
Note
High Risk
Introduces harness bearer auth, unauthenticated pairing/claim endpoints, and team-shared execution on user machines—security- and authorization-sensitive paths that affect how agents run locally.
Overview
macrod self-hosted agents move from per-bot tokens in config to registered harnesses: the daemon pairs via a printed code, gets a harness bearer credential, and agents are created against a specific harness UUID while still using the
macrodharness slug.The backend adds
harnesses,harness_tokens, andharness_pairing_requests(create → user approve → daemon claim), wiresagent_configs.harness_id, and extends storage/auth so daemons authenticate with hashed harness tokens and connection presence updateslast_connected_at. Runtime routing in agent harness resolves a bot’s bound harness and attaches WebSocket runtimes by harness id so one daemon can serve every agent on that machine.Settings → Harness lists macrod harnesses (connected/disconnected, private/team), supports Enter pairing code /
?pair=deep links, and removal (revokes tokens). Settings → Agents adds registered harnesses to the harness picker, free-text default model for macrod, and persistsharnessIdon create/update. Docs for bring-your-own-agent are rewritten around pairing andmacrod logininstead of embeddingbot_tokeninmacro.toml.Reviewed by Cursor Bugbot for commit 7fe2598. Bugbot is set up for automated code reviews on this repo. Configure here.