Skip to content

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

Merged
ehayes2000 merged 1 commit into
mainfrom
eric.hayes/macrod-harness-registration
Sep 1, 2026
Merged

ehayes2000 merged 1 commit into
mainfrom
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
Adds harness token authentication, pairing/claim flows, and agent-to-harness binding—security-sensitive paths that affect who can run code on user machines.

Overview
Introduces registered macrod harnesses so BYOA daemons pair via a printed code instead of embedding bot tokens in macro.toml. New crates harness_id, harness_token, and harnesses back persistence (harnesses, harness_tokens, harness_pairing_requests), token auth in macro_authorization, and HTTP routes for pairing create/lookup/approve/claim plus harness list/delete. Agents gain an optional harness_id on agent_configs (alongside the existing harness slug); create/update/list queries and APIs carry the binding so macrod agents use harness macrod + a specific harness UUID.

Settings adds harness management: list connected/disconnected macrod instances, enter/approve pairing (including ?pair= deep link), remove harness (revokes tokens), and the Agents UI lists registered harnesses in the harness picker with free-text default model for macrod. Client types and Orval schemas pick up harness_id; docs for bring-your-own walk through pairing and creating agents on a harness.

CI workspace dependency closures are updated to include the new crates across affected packages.

Reviewed by Cursor Bugbot for commit 9726559. 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

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 3802542b-091f-41b6-86f1-282a9c7f9fe8

📥 Commits

Reviewing files that changed from the base of the PR and between 539e78f and 9726559.

⛔ Files ignored due to path filters (60)
  • .sqlx/query-0057301723d11cedc003eab4fb1355b7352cd9a137f0e8807c802e88f538c7ee.json is excluded by !**/.sqlx/**
  • .sqlx/query-0d925eefe7a5d058b803ba33c4740afdfbb1e1fba609a1333016cf779771c38a.json is excluded by !**/.sqlx/**
  • .sqlx/query-2193da6edede173e322e1f6f7ca1206ea8523c16b99d723be67afaa56c7075ec.json is excluded by !**/.sqlx/**
  • .sqlx/query-308407c450a391bd26c2b23bd4b03dca0596ba14972654d7dd2178f47cd4e1dc.json is excluded by !**/.sqlx/**
  • .sqlx/query-39d74321afaaed143e3462074610e033ce0c07edbb1744bf012fc6ca9534b209.json is excluded by !**/.sqlx/**
  • .sqlx/query-42ad7423977f6bc3b9c42cba56eb2e81a82d147e79c29c43e0bd316b731293b6.json is excluded by !**/.sqlx/**
  • .sqlx/query-42f75f261b91698eb160964f5d5ba56c19602fb2c113bde6d00ecf62ee87812d.json is excluded by !**/.sqlx/**
  • .sqlx/query-4376ae3d9e02625711ee2c47124c27406214ba66194a516c971b0a0b8c3ac909.json is excluded by !**/.sqlx/**
  • .sqlx/query-462894fae868528fbf10226edf99171a52bdcd0377f40616c3c304ab6a106f25.json is excluded by !**/.sqlx/**
  • .sqlx/query-50e821ab6b81df124e4335310966a5ba2ccbc811da6df94b09e9288aa9e53011.json is excluded by !**/.sqlx/**
  • .sqlx/query-7082641d64d8cb00e41495a47e02f49271c15b88738515059349b07511865876.json is excluded by !**/.sqlx/**
  • .sqlx/query-76c662c3c7b23512dfa8a466c1e1fa4f03306e93e17003876ceab7081a88ef76.json is excluded by !**/.sqlx/**
  • .sqlx/query-7c44900ff1b6056f0476aabe7fb01f3acbfaa80c6ab3bef064f0fe2cb08553f3.json is excluded by !**/.sqlx/**
  • .sqlx/query-82c3b95aadd28046ab7667de3da9684503d8e27e3ac59f931593b37928769c90.json is excluded by !**/.sqlx/**
  • .sqlx/query-9a4cae02a8b2462b906ebbb6fec71cb77d6fe055a789a6abb2cb363b4ee00640.json is excluded by !**/.sqlx/**
  • .sqlx/query-9cf0d65adf0369dd4429aac0c7b3a01044286c7ab8b1631f80cd98e87f2ba6f4.json is excluded by !**/.sqlx/**
  • .sqlx/query-9de80e4162ec9a996091d800b6443786761dac66d571344961abc576eec19563.json is excluded by !**/.sqlx/**
  • .sqlx/query-ade51572ac68619218ad84c1c13af9094229cb0c46ccb980bbb9b463d4260c73.json is excluded by !**/.sqlx/**
  • .sqlx/query-b4d134196b124c4c4a6b0ca5ceb6c8b0c14fbe1d11ad9c260c9108a27f64134d.json is excluded by !**/.sqlx/**
  • .sqlx/query-bb5f40af2c31290cf842e508fc7affce72ecaf1ba8b9b5d040cec43ba5c0c153.json is excluded by !**/.sqlx/**
  • .sqlx/query-c5d9dc079f9b708c5aaafcaca95fe001fd7a6df89e2710c20123b63c31fa9b99.json is excluded by !**/.sqlx/**
  • .sqlx/query-ca706ffa9077b59f2cfb7b159c25286e43761bede515094d756ae517cb6d6d75.json is excluded by !**/.sqlx/**
  • .sqlx/query-ea316ad555009caf25e3a6d8f25ee154f8b0611efbbc2c1e7354c46becbaa476.json is excluded by !**/.sqlx/**
  • .sqlx/query-f1bfc93ff556e8fc22dbc7788dc5474aa042c379d0f255faa0b487c5480bffcb.json is excluded by !**/.sqlx/**
  • .sqlx/query-f24bd1d4434b2306156cd933271e4854c541d13bf5f3d22da5f366bf4b1a8639.json is excluded by !**/.sqlx/**
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/agent.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/agentHarnessId.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/approvePairingRequest.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/approvePairingRequestName.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/approvePairingRequestTeamId.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/claimPairingRequest.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/claimedPairing.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/createAgentRequest.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/createAgentRequestHarnessId.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/createPairingRequest.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/createPairingRequestHost.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/createPairingRequestScope.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/createdPairing.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harness.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harnessAgent.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harnessId.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harnessLastConnectedAt.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harnessOwner.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harnessOwnerOneOf.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harnessOwnerOneOfThree.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harnessOwnerOneOfThreeType.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/harnessOwnerOneOfType.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/index.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/pairingDetails.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/pairingDetailsHost.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/pairingDetailsRequestedScope.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/pendingClaimResponse.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/requestedHarnessScope.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/updateAgentRequest.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/schemas/updateAgentRequestHarnessId.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-storage/generated/zod.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • packages/sdk/generated/storage/index.ts is excluded by !**/generated/**
  • packages/sdk/generated/storage/sdk.gen.ts is excluded by !**/generated/**, !**/*.gen.ts
  • packages/sdk/generated/storage/types.gen.ts is excluded by !**/generated/**, !**/*.gen.ts
📒 Files selected for processing (105)
  • .github/workspace-dep-closures.json
  • Cargo.toml
  • apps/docs/AI/bring-your-own.mdx
  • apps/web/src/features/settings/Agents.test.tsx
  • apps/web/src/features/settings/Agents.tsx
  • apps/web/src/features/settings/Harness.test.tsx
  • apps/web/src/features/settings/Harness.tsx
  • apps/web/src/features/settings/HarnessPairingDialog.test.tsx
  • apps/web/src/features/settings/HarnessPairingDialog.tsx
  • apps/web/src/features/settings/primitives.tsx
  • apps/web/src/lib/queries/agents/agents.ts
  • apps/web/src/lib/queries/harnesses/harnesses.ts
  • apps/web/src/lib/queries/harnesses/keys.ts
  • apps/web/src/lib/service-clients/service-storage/client.ts
  • apps/web/src/lib/service-clients/service-storage/openapi.json
  • crates/agent_harness/Cargo.toml
  • crates/agent_harness/src/domain/model.rs
  • crates/agent_harness/src/domain/ports.rs
  • crates/agent_harness/src/domain/service/test.rs
  • crates/agent_harness/src/inbound/runtime_gateway.rs
  • crates/agent_harness/src/inbound/runtime_gateway/test.rs
  • crates/agent_harness/src/outbound/runtime_registry.rs
  • crates/agent_harness/src/outbound/runtime_registry/test.rs
  • crates/agent_session/Cargo.toml
  • crates/agent_session/src/domain/ports.rs
  • crates/agent_session/src/inbound/axum_router.rs
  • crates/agent_session/src/inbound/axum_router/test.rs
  • crates/agent_trigger/src/domain/service/test.rs
  • crates/bots/Cargo.toml
  • crates/bots/src/domain/models.rs
  • crates/bots/src/domain/ports.rs
  • crates/bots/src/domain/service.rs
  • crates/bots/src/inbound/axum_router.rs
  • crates/bots/src/outbound/pg_bots_repo.rs
  • crates/bots/src/outbound/pg_bots_repo/tests.rs
  • crates/coding_agent_worker/Cargo.toml
  • crates/coding_agent_worker/config.example.toml
  • crates/coding_agent_worker/src/config.rs
  • crates/coding_agent_worker/src/config/test.rs
  • crates/coding_agent_worker/src/dispatch.rs
  • crates/coding_agent_worker/src/main.rs
  • crates/coding_agent_worker/src/outbound/agent_session.rs
  • crates/coding_agent_worker/src/outbound/credentials.rs
  • crates/coding_agent_worker/src/outbound/credentials/test.rs
  • crates/coding_agent_worker/src/outbound/link.rs
  • crates/coding_agent_worker/src/outbound/mod.rs
  • crates/coding_agent_worker/src/outbound/pairing.rs
  • crates/coding_agent_worker/src/outbound/registration.rs
  • crates/coding_agent_worker/src/runtime.rs
  • crates/coding_agent_worker/src/webhook.rs
  • crates/coding_agent_worker/src/webhook/test.rs
  • crates/entity_access/src/inbound/axum_extractors/entity_body.rs
  • crates/entity_access/src/inbound/axum_extractors/entity_permission.rs
  • crates/entity_access/src/inbound/axum_extractors/pin.rs
  • crates/entity_access/src/inbound/axum_extractors/project.rs
  • crates/entity_access/src/inbound/axum_extractors/team.rs
  • crates/harness_id/Cargo.toml
  • crates/harness_id/src/lib.rs
  • crates/harness_id/src/test.rs
  • crates/harness_token/Cargo.toml
  • crates/harness_token/src/lib.rs
  • crates/harness_token/src/test.rs
  • crates/harnesses/Cargo.toml
  • crates/harnesses/src/domain/mod.rs
  • crates/harnesses/src/domain/models.rs
  • crates/harnesses/src/domain/ports.rs
  • crates/harnesses/src/domain/service.rs
  • crates/harnesses/src/domain/service/test.rs
  • crates/harnesses/src/domain/tokens.rs
  • crates/harnesses/src/inbound/axum_router.rs
  • crates/harnesses/src/inbound/mod.rs
  • crates/harnesses/src/lib.rs
  • crates/harnesses/src/outbound/mod.rs
  • crates/harnesses/src/outbound/pg_harness_repo.rs
  • crates/harnesses/src/outbound/pg_harness_repo/tests.rs
  • crates/macro_authorization/Cargo.toml
  • crates/macro_authorization/src/domain.rs
  • crates/macro_authorization/src/domain/harness_authorizer.rs
  • crates/macro_authorization/src/domain/harness_authorizer/test.rs
  • crates/macro_authorization/src/domain/models.rs
  • crates/macro_authorization/src/domain/ports.rs
  • crates/macro_authorization/src/domain/service.rs
  • crates/macro_authorization/src/inbound.rs
  • crates/macro_authorization/src/inbound/axum.rs
  • crates/macro_authorization/src/inbound/axum/harness.rs
  • crates/macro_authorization/src/inbound/axum/macro_authorization.rs
  • crates/macro_authorization/src/inbound/axum/policy.rs
  • crates/macro_authorization/src/lib.rs
  • crates/macro_authorization/src/outbound.rs
  • crates/macro_authorization/src/outbound/pg_harness_authorization.rs
  • crates/macro_authorization/src/outbound/pg_harness_authorization/test.rs
  • crates/macro_db_client/migrations/20260831201832_create_harnesses.down.sql
  • crates/macro_db_client/migrations/20260831201832_create_harnesses.up.sql
  • packages/sdk/specs/storage.json
  • packages/sdk/src/coverage/skipped.ts
  • services/agent_harness_service/Cargo.toml
  • services/agent_harness_service/src/api.rs
  • services/agent_harness_service/src/bots_directory.rs
  • services/agent_harness_service/src/harness_bindings.rs
  • services/agent_harness_service/src/main.rs
  • services/document_storage_service/Cargo.toml
  • services/document_storage_service/src/api.rs
  • services/document_storage_service/src/api/context.rs
  • services/document_storage_service/src/api/swagger.rs
  • services/document_storage_service/src/main.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added self-hosted harness pairing through a first-run code and Macro settings.
    • Added harness management, including connection status, ownership details, removal, and re-pairing.
    • Added support for assigning agents to registered harnesses.
    • Added macrod login for pairing and credential recovery.
    • Added private and team harness sharing options.
  • Bug Fixes
    • Improved authorization and access checks for harness-connected agents.
    • Prevented disconnected or unauthorized harnesses from opening agent sessions.
  • Documentation
    • Updated self-hosted harness setup, pairing, permissions, and credential revocation guidance.

Walkthrough

Adds private and team harness pairing through device codes, persisted credentials, harness token authorization, and harness-scoped runtime connections. Adds database tables, storage APIs, service routes, agent harness bindings, feed reconciliation, and daemon login support. Updates the web settings pages for pairing, listing, removal, and agent harness selection. Adds unit and integration tests for pairing, authorization, persistence, runtime routing, configuration, and UI behavior.

Warning

Some tools did not complete. Review the errors below.

🔧 Checkov (3.3.11)
.github/workspace-dep-closures.json

Checkov timed out on this file

apps/web/src/lib/service-clients/service-storage/openapi.json

Checkov timed out on this file

packages/sdk/specs/storage.json

Checkov timed out on this file


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
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
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
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_id → ON DELETE CASCADE (ephemeral bookkeeping).
  • agent_configs.harness_id → ON 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/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.

Comment thread crates/agent_session/src/inbound/axum_router.rs
.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
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.

Comment thread crates/agent_session/src/inbound/axum_router.rs
.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/macrod-harness-registration branch from ddb36a1 to 35d54f1 Compare August 31, 2026 23:29
Comment thread crates/bots/src/domain/service.rs
@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/macrod-harness-registration branch from 7fe2598 to 9df03df Compare September 1, 2026 15:05
Comment thread crates/harnesses/src/domain/service.rs
(Some(connected_at), Some(disconnected_at)) => connected_at > disconnected_at,
(Some(_), None) => true,
(None, _) => false,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reconnect races connected status

Medium Severity

connected is derived from whichever of last_connected_at and last_disconnected_at is newer. Attach and detach write those timestamps independently with no generation or fencing, so a reconnect can persist disconnect after connect and show a live daemon as disconnected.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9df03df. Configure here.

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 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, including sandbox-size, do the same. Team harnesses accept x-macro-harness-for-macro-user-id for any current teammate; managed session create and session control skip harness-binding checks.

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 9df03df. Configure here.

Comment thread crates/agent_session/src/inbound/axum_router.rs
credentials.clone(),
config_path,
));
let signing_secret = Arc::new(std::sync::RwLock::new(String::new()));

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 9df03df. Configure here.

(status = 500, body = ErrorResponse),
)
)]
pub async fn create_pairing_handler<S: HarnessService, Auth: MacroAuthorizationService>(

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 9df03df. 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 9df03df. Configure here.

@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from 9df03df to 73613fc Compare September 1, 2026 15:42
@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from 73613fc to f7b371f Compare September 1, 2026 15:46
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() returns that user for every harness request. ActingUser then admits the daemon token as that user, and several entity extractors (document, channel, chat, agent session) only special-case Bot before falling through to acting_user(). A team harness can also set x-macro-harness-for-macro-user-id to any current teammate.

Impact: A valid mhns_… token can create webhooks, control or delete that user’s agent sessions, and read or mutate their documents, chats, and channels. On a team harness, the daemon can impersonate any teammate, not only the pairing approver.

Fix in Cursor Fix in Web

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

Comment thread crates/agent_session/src/inbound/axum_router.rs
.context("failed to register this bot's trigger feed")?;
let needs_validation = (!feed.is_valid).then_some(feed.webhook_id);
(feed.signing_secret, needs_validation)
.context("failed to register this harness's trigger feed")?;

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 still binds the webhook listener when ensure_feed returns None (no agents bound). The signing secret stays an empty string, and POST /macro-events verifies HMAC against that empty key.

Impact: Anyone who can reach the daemon port can forge a valid signature with an empty HMAC key and submit agent-trigger events until a feed is registered.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit f7b371f. 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 a per-name cap). There is no per-IP or per-client limit.

Impact: An unauthenticated caller can fill the global pending-pairing quota and block every user from pairing a daemon until those rows expire.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit f7b371f. 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 is instrumented with skip(self) only, so the pairing code is recorded on the tracing span. approve_pairing similarly logs code and caller fields.

Impact: Pairing codes in logs or traces can be used to look up pending pairings and, together with a leaked device secret, complete a claim.

Fix in Cursor Fix in Web

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

@ehayes2000
ehayes2000 force-pushed the eric.hayes/macrod-harness-registration branch from f7b371f to c3973b3 Compare September 1, 2026 16:16
>(
State(state): State<CreateSessionState<Opener, Bots, Auth>>,
caller: MacroAuthorizationExtractor<Auth, UserOrBot>,
caller: MacroAuthorizationExtractor<Auth, UserBotOrHarness>,

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

create_agent_session now accepts harness tokens via UserBotOrHarness, but the managed path (no workspace) still skips every bot/harness bind check and opens a cloud-managed sandbox as the harness acting_user.

A valid mhns_ token can therefore provision Macro-hosted sessions with caller-supplied prompt/instructions. A team harness can also set x-macro-harness-for-macro-user-id to any current teammate and open those sessions as that user. Binding is only enforced later on the external-workspace branch.

Impact: A user-run daemon credential can spawn and attribute managed sandboxes the harness was not meant to operate, including as another teammate.

Fix in Cursor Fix in Web

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

Base automatically changed from eric.hayes/personas-harness-mentions-cursor to main September 1, 2026 16:49
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 c3973b3 to 9726559 Compare September 1, 2026 16:49

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

There are 2 total unresolved issues (including 1 from previous review).

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 9726559. Configure here.

.context("failed to revoke harness tokens")?;

tx.commit().await.context("failed to commit")?;
Ok(deleted.rows_affected() == 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deleted harness leaves agents bound

Medium Severity

Removing a harness only soft-deletes the row and revokes tokens. Bound agents keep the old harness_id, routing ignores deleted harnesses, and a later macrod login mints a new harness. Agents stay dead until each one is edited, despite copy that they will run again after reconnect.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9726559. Configure here.

Comment on lines +1155 to +1160
if let UserBotOrHarnessAuthorization::Harness(_) = caller
&& 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: MEDIUM

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.

Impact: A holder of the daemon token can attribute and later control sessions as another Macro user, including a teammate when combined with a team harness acting-user claim.

Fix in Cursor Fix in Web

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

@ehayes2000
ehayes2000 merged commit 034c7ff into main Sep 1, 2026
33 of 34 checks passed
@ehayes2000
ehayes2000 deleted the eric.hayes/macrod-harness-registration branch September 1, 2026 17:14
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