Skip to content

feat(harness): register macrod harnesses with device-code pairing - #6030

Open
ehayes2000 wants to merge 1 commit into
eric.hayes/personas-harness-mentions-cursorfrom
eric.hayes/macrod-harness-registration
Open

feat(harness): register macrod harnesses with device-code pairing#6030
ehayes2000 wants to merge 1 commit into
eric.hayes/personas-harness-mentions-cursorfrom
eric.hayes/macrod-harness-registration

Conversation

@ehayes2000

@ehayes2000 ehayes2000 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

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 macrod harness slug.

The backend adds harnesses, harness_tokens, and harness_pairing_requests (create → user approve → daemon claim), wires agent_configs.harness_id, and extends storage/auth so daemons authenticate with hashed harness tokens and connection presence updates last_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 persists harnessId on create/update. Docs for bring-your-own-agent are rewritten around pairing and macrod login instead of embedding bot_token in macro.toml.

Reviewed by Cursor Bugbot for commit 7fe2598. Bugbot is set up for automated code reviews on this repo. Configure here.

@ehayes2000
ehayes2000 requested a review from a team as a code owner August 28, 2026 18:44
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 0590dfcc-a66c-41be-bc8f-a90ed6528e46

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Comment thread crates/macro_authorization/src/domain/harness_authorizer.rs
Comment thread crates/harnesses/src/outbound/pg_harness_repo.rs
Comment thread crates/macro_authorization/src/domain/models.rs
@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from a058fa9 to c30665b Compare August 31, 2026 18:56
@ehayes2000
ehayes2000 force-pushed the eric.hayes/personas-harness-mentions-cursor branch from 15c6d5a to 219f42e Compare August 31, 2026 18:56
Comment thread crates/agent_harness/src/outbound/runtime_registry.rs
Comment thread crates/macro_authorization/src/domain/models.rs
@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from c30665b to b7004f9 Compare August 31, 2026 19:13
@ehayes2000
ehayes2000 force-pushed the eric.hayes/personas-harness-mentions-cursor branch from 219f42e to 03cd1ef Compare August 31, 2026 19:13
Comment thread crates/macro_authorization/src/domain/models.rs
Comment thread crates/harnesses/src/outbound/pg_harness_repo.rs
Comment thread crates/harnesses/src/outbound/pg_harness_repo.rs
Comment thread apps/web/src/features/settings/Agents.tsx
Comment thread crates/macro_db_client/migrations/20260831201832_create_harnesses.up.sql Outdated
Comment thread crates/macro_authorization/src/domain/models.rs
Comment thread crates/agent_session/src/inbound/axum_router.rs
Comment thread crates/coding_agent_worker/src/main.rs
@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from b7004f9 to 6423ae3 Compare August 31, 2026 20:00
@ehayes2000
ehayes2000 force-pushed the eric.hayes/personas-harness-mentions-cursor branch from 03cd1ef to 0039f32 Compare August 31, 2026 20:01
Comment thread crates/coding_agent_worker/src/outbound/pairing.rs
Comment thread crates/harnesses/src/inbound/axum_router.rs
Comment thread crates/harnesses/src/domain/service.rs
@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch 2 times, most recently from 63c61c9 to 56236ae Compare August 31, 2026 20:35
@ehayes2000

Copy link
Copy Markdown
Contributor Author

Re: cursor[bot]'s "Team delete blocked by harness FKs" — confirmed and fixed in 56236ae.

Verified empirically on a scratch DB before fixing: an approved pairing row (never GC'd, by design) or a user-owned agent bound to the team harness did block DELETE FROM team; a team-owned agent did not (its config cascades away via bots.team_id within the same statement, and NO ACTION is checked at statement end).

Fix, in the squashed migration:

  • harness_pairing_requests.harness_idON DELETE CASCADE (ephemeral bookkeeping).
  • agent_configs.harness_idON DELETE SET NULL, with the slug check relaxed to one direction (harness_id IS NULL OR harness = 'macrod'): a member's personal agent survives its team's deletion unbound and rebindable. The unbound state is behaviorally identical to the existing soft-deleted-harness state (bind already returns None), and the API layer still requires slug+id together on create/update, so only a cascade can produce it.

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.

Comment thread crates/macro_authorization/src/outbound/pg_harness_authorization.rs
///
/// 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)]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is really stinky. why are we calling this "HarnessFacts" why isn't this an enum with variants TeamOwned and TeamOwned?

@ehayes2000
ehayes2000 force-pushed the eric.hayes/personas-harness-mentions-cursor branch from 0039f32 to f33e121 Compare August 31, 2026 21:20
@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from 56236ae to cdc0282 Compare August 31, 2026 21:20
Comment thread crates/macro_authorization/src/domain/models.rs
Comment thread crates/agent_session/src/inbound/axum_router.rs
Comment thread crates/coding_agent_worker/src/main.rs
Comment thread crates/harnesses/src/inbound/axum_router.rs
Comment thread crates/harnesses/src/domain/service.rs
match self {
Self::User(user) => Some(user),
Self::Bot(bot) => bot.acting_user.as_ref(),
Self::Harness(harness) => Some(&harness.acting_user),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit cdc0282. Configure here.

{
return MacroUserIdStr::try_from(claimed)
.map_err(|_| CreateSessionApiError::UnparseableOwner);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

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)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

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> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit cdc0282. Configure here.

@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from cdc0282 to ddb36a1 Compare August 31, 2026 22:45
@ehayes2000
ehayes2000 force-pushed the eric.hayes/personas-harness-mentions-cursor branch from f33e121 to 09e06fe Compare August 31, 2026 22:45
Comment thread apps/web/src/features/settings/Harness.tsx
Comment thread apps/web/src/features/settings/Agents.tsx
match self {
Self::User(user) => Some(user),
Self::Bot(bot) => bot.acting_user.as_ref(),
Self::Harness(harness) => Some(&harness.acting_user),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit ddb36a1. Configure here.

&& let Some(claimed) = claimed
{
return MacroUserIdStr::try_from(claimed)
.map_err(|_| CreateSessionApiError::UnparseableOwner);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

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)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

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> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit ddb36a1. Configure here.

@ehayes2000
ehayes2000 force-pushed the eric.hayes/personas-harness-mentions-cursor branch from 09e06fe to 8075f84 Compare August 31, 2026 23:29
@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from ddb36a1 to 35d54f1 Compare August 31, 2026 23:29

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

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

Comment thread crates/bots/src/domain/service.rs
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.
@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from 35d54f1 to 7fe2598 Compare August 31, 2026 23:40
@ehayes2000
ehayes2000 force-pushed the eric.hayes/personas-harness-mentions-cursor branch from 8075f84 to 8381f6a Compare August 31, 2026 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant