Skip to content

feat(server): device workers can claim runs in orgs they're pinned to#1149

Merged
buremba merged 5 commits into
mainfrom
feat/device-cross-org-pins
May 29, 2026
Merged

feat(server): device workers can claim runs in orgs they're pinned to#1149
buremba merged 5 commits into
mainfrom
feat/device-cross-org-pins

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 29, 2026

Gap

A user-scoped device worker (e.g. Owletto Mac) could only claim runs in [token's bound org, personal org] (index.ts:766). So a watcher/connection pinned to the device in another org the owner belongs to was never claimable — the run sat pending forever (the server deliberately skips device-pinned runs). This blocked running an org's watcher on a personal device via local Claude.

Fix

resolveDeviceClaimableOrgs widens the poll's claimable orgs to include any org where the device has an active pin AND its owner is still a member. The pin is already the consent (evaluateDeviceWorkerAccess only lets a device's owner attach it); the membership join revokes scope automatically if the owner leaves. The device stays anchored to its home+personal org; within-org claiming still follows the existing pinned/capability rules, so it only runs the resource it was pinned to. Revoke = unpin.

Tests

New integration test (2 cases): pinned+member org included, pinned+non-member excluded, base scope preserved, archived-watcher pin grants nothing. Passes locally under Node 22 + embedded PG.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

New Features

  • Device workers can now claim and execute runs across multiple organizations when pinned to those organizations, provided the device owner maintains membership in those organizations.

Tests

  • Added comprehensive test coverage for cross-organization device pin scoping behavior.
  • Added unit tests for worker scope authorization logic.

Review Change Stack

A user-scoped device worker's claim scope was [token's bound org, personal
org], so a watcher/connection pinned to the device in another org the owner
belongs to could never be claimed (the run sat pending — the server skips
device-pinned runs). The pin is already the owner's consent
(evaluateDeviceWorkerAccess only lets a device's owner attach it).

resolveDeviceClaimableOrgs widens the poll's claimable orgs to include orgs
where the device has an active pin AND the owner is still a member. Keeps the
device anchored to its home+personal org; revoke = unpin or leave the org.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Warning

Review limit reached

@buremba, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 4 minutes and 11 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 45fd2c5e-29c1-48fb-8b26-08b7e8b4d3df

📥 Commits

Reviewing files that changed from the base of the PR and between e387029 and 7fa572d.

📒 Files selected for processing (1)
  • packages/server/src/worker-api.ts
📝 Walkthrough

Walkthrough

This PR introduces utilities to determine organizational scope for user-scoped device workers based on active device pins, integrates them into worker API authorization and polling flows to enable cross-organization run claiming, and adds unit and integration test coverage for the new scope logic.

Changes

Cross-org device pin scoping

Layer / File(s) Summary
Device-claimable organization scope utilities
packages/server/src/utils/device-claimable-orgs.ts
resolveDeviceClaimableOrgs queries the database for org IDs where a device worker has active pinned watchers or non-deleted connections, intersects with owner membership, and returns the union with base org IDs. runInWorkerScope returns true when the run's org is in the worker's scope or when the worker owns the run via device_owner or watcher_device_owner.
Worker API authorization and polling with device-claimable scope
packages/server/src/worker-api.ts
authorizeRunForWorker expands its query to include watcher/device ownership fields and uses runInWorkerScope to determine access instead of inline matching. pollWorkerJob computes claimableOrgIds via resolveDeviceClaimableOrgs for user-scoped workers and uses it for org-scoping filters and the no-scope early return, with non-fatal fallback on lookup failure.
Test coverage for device-claimable scope
packages/server/src/__tests__/unit/run-in-worker-scope.test.ts, packages/server/src/__tests__/integration/auth/device-cross-org-pins.test.ts
Unit tests validate runInWorkerScope for in-scope and out-of-scope decisions based on org membership and device/watcher ownership. Integration tests validate resolveDeviceClaimableOrgs including base org scope, pinned org inclusion (when owner is member), archived pin exclusion, and database state reset.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • lobu-ai/lobu#1017: Both PRs modify packages/server/src/worker-api.ts's pollWorkerJob org/claim scoping logic, with this PR computing claimableOrgIds via resolveDeviceClaimableOrgs and the related PR using effectiveWorkerOrgIds.
  • lobu-ai/lobu#814: Both PRs touch the device-pinned watcher run authorization/claiming flow in packages/server/src/worker-api.ts by changing how workers determine which pinned watcher runs they're allowed to act on.

Poem

🐰 Across the orgs a worker hops,
With pinned device, the scope won't stop!
Memberships align, watchers gleam bright,
Authorization checked—now claims take flight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately describes the primary feature: enabling device workers to claim runs in additional pinned organizations.
Description check ✅ Passed Description provides gap context, solution explanation, and test coverage but is missing test plan checkboxes from template.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/device-cross-org-pins

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.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 29, 2026

bug_free 78, simplicity 82, slop 6, bugs 0, 0 blockers

Suite logs passed: typecheck/unit/integration all exit 0. Static-inspected worker auth, poll, and trigger paths; did not boot server. [env] Targeted rerun of the new integration test failed before execution because this shell lacks DATABASE_URL.

Suggested fixes

File Line Change
packages/server/src/__tests__/integration/auth/device-cross-org-pins.test.ts 52 Add a route-level poll test that seeds a cross-org device-pinned watcher or connection plus an unrelated unpinned capability run in that org, calls /api/workers/poll with the worker-bound token, and asserts only the pinned run is claimable.
packages/server/src/__tests__/integration/watchers/manual-trigger.test.ts 320 Add cross-org manual-trigger coverage: a worker-bound token from org A should get 200 for an org B watcher pinned to that device when the owner is still a member, and 403 after membership is removed.
Full verdict JSON
{
  "bug_free_confidence": 78,
  "bugs": 0,
  "slop": 6,
  "simplicity": 82,
  "blockers": [],
  "change_type": "feat",
  "behavior_change_risk": "high",
  "tests_adequate": false,
  "suggested_fixes": [
    {
      "file": "packages/server/src/__tests__/integration/auth/device-cross-org-pins.test.ts",
      "line": 52,
      "change": "Add a route-level poll test that seeds a cross-org device-pinned watcher or connection plus an unrelated unpinned capability run in that org, calls /api/workers/poll with the worker-bound token, and asserts only the pinned run is claimable."
    },
    {
      "file": "packages/server/src/__tests__/integration/watchers/manual-trigger.test.ts",
      "line": 320,
      "change": "Add cross-org manual-trigger coverage: a worker-bound token from org A should get 200 for an org B watcher pinned to that device when the owner is still a member, and 403 after membership is removed."
    }
  ],
  "notes": "Suite logs passed: typecheck/unit/integration all exit 0. Static-inspected worker auth, poll, and trigger paths; did not boot server. [env] Targeted rerun of the new integration test failed before execution because this shell lacks DATABASE_URL.",
  "categories": {
    "src": 140,
    "tests": 151,
    "docs": 0,
    "config": 0,
    "deps": 0,
    "migrations": 0,
    "ci": 0,
    "generated": 0
  }
}

Local review gate — branch protection can require the pi-review commit status. See docs/REVIEW_SCHEMA.md.

…vices

pi caught that widening the poll's claim scope wasn't enough: authorizeRunForWorker
(complete-watcher / heartbeat) only trusted the token's base orgs or the
connection-pinned device owner, so a device could CLAIM a cross-org watcher run
but get 403 trying to FINISH it. Join the watcher's pinned device too and accept
the owner via a shared runInWorkerScope() decision (unit-tested).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/server/src/utils/device-claimable-orgs.ts (1)

21-24: ⚡ Quick win

Extract these new object shapes into interfaces.

The new exported signatures introduce inline object-shape types. Please move them to named interfaces so this file stays aligned with the repo's TypeScript convention.

As per coding guidelines, "Use interface for defining object shapes in TypeScript files".

Also applies to: 55-60

🤖 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/server/src/utils/device-claimable-orgs.ts` around lines 21 - 24,
Extract the inline object shapes into named exported interfaces and replace the
inline types in the function signatures: create an interface (e.g.,
ResolveDeviceClaimableOrgsParams) representing the params object used by
resolveDeviceClaimableOrgs, export it, and change the function signature to use
that interface; do the same for the other exported signatures mentioned around
lines 55-60 (create appropriate interface names for their parameter/return
object shapes and use those interfaces in the signatures). Ensure names are
descriptive, exported, and referenced where the inline object types currently
appear (e.g., replace params: { deviceWorkerId: string; ownerUserId: string;
baseOrgIds: string[] } with params: ResolveDeviceClaimableOrgsParams).
packages/server/src/__tests__/integration/auth/device-cross-org-pins.test.ts (1)

57-106: ⚡ Quick win

Add one integration case for pinned connections too.

This suite locks in the watcher branch well, but resolveDeviceClaimableOrgs() also widens scope from connections.device_worker_id. A small active/non-deleted connection-pin case would protect the other half of the SQL UNION from drifting unnoticed.

🤖 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/server/src/__tests__/integration/auth/device-cross-org-pins.test.ts`
around lines 57 - 106, Add an integration test that covers the
connections.device_worker_id branch of resolveDeviceClaimableOrgs: create a
deviceWorkerId, a base org (orgA) and another org (orgD) where the user is a
member, then create an active/non-deleted connection-pin record that sets
connections.device_worker_id to the deviceWorkerId for orgD (use your existing
helper like createTestConnection or pinConnection), call
resolveDeviceClaimableOrgs with { deviceWorkerId, ownerUserId: user.id,
baseOrgIds: [orgA.id] } and assert the result contains orgD (and still contains
orgA); also add a small case where the connection is archived/non-active and
assert it does not grant scope—this mirrors the watcher-pin tests but targets
the connections.device_worker_id union branch.
🤖 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.

Inline comments:
In `@packages/server/src/utils/device-claimable-orgs.ts`:
- Around line 54-66: The runInWorkerScope helper currently authorizes based
solely on device_owner/watcher_device_owner, letting authorizeRunForWorker
accept cross-org runs even after membership is revoked; update runInWorkerScope
to consult the worker's current claimable orgs via resolveDeviceClaimableOrgs()
(or accept a ctx.claimableOrgIds array) and only allow the ownership fallback
when run.organization_id is still contained in that resolved set—i.e., first
check ctx.orgIds/includes(run.organization_id), then if ctx.workerUserId is
present resolve the worker's claimable org IDs and permit the
device_owner/watcher_device_owner match only when run.organization_id is in that
resolved claimable set.

---

Nitpick comments:
In
`@packages/server/src/__tests__/integration/auth/device-cross-org-pins.test.ts`:
- Around line 57-106: Add an integration test that covers the
connections.device_worker_id branch of resolveDeviceClaimableOrgs: create a
deviceWorkerId, a base org (orgA) and another org (orgD) where the user is a
member, then create an active/non-deleted connection-pin record that sets
connections.device_worker_id to the deviceWorkerId for orgD (use your existing
helper like createTestConnection or pinConnection), call
resolveDeviceClaimableOrgs with { deviceWorkerId, ownerUserId: user.id,
baseOrgIds: [orgA.id] } and assert the result contains orgD (and still contains
orgA); also add a small case where the connection is archived/non-active and
assert it does not grant scope—this mirrors the watcher-pin tests but targets
the connections.device_worker_id union branch.

In `@packages/server/src/utils/device-claimable-orgs.ts`:
- Around line 21-24: Extract the inline object shapes into named exported
interfaces and replace the inline types in the function signatures: create an
interface (e.g., ResolveDeviceClaimableOrgsParams) representing the params
object used by resolveDeviceClaimableOrgs, export it, and change the function
signature to use that interface; do the same for the other exported signatures
mentioned around lines 55-60 (create appropriate interface names for their
parameter/return object shapes and use those interfaces in the signatures).
Ensure names are descriptive, exported, and referenced where the inline object
types currently appear (e.g., replace params: { deviceWorkerId: string;
ownerUserId: string; baseOrgIds: string[] } with params:
ResolveDeviceClaimableOrgsParams).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d919b5c9-0acd-48f5-aa7c-f743b4f96cae

📥 Commits

Reviewing files that changed from the base of the PR and between a8835c4 and e387029.

📒 Files selected for processing (4)
  • packages/server/src/__tests__/integration/auth/device-cross-org-pins.test.ts
  • packages/server/src/__tests__/unit/run-in-worker-scope.test.ts
  • packages/server/src/utils/device-claimable-orgs.ts
  • packages/server/src/worker-api.ts

Comment on lines +54 to +66
export function runInWorkerScope(
run: {
organization_id: string;
device_owner: string | null;
watcher_device_owner: string | null;
},
ctx: { workerUserId: string | null; orgIds: string[] }
): boolean {
if (ctx.orgIds.includes(run.organization_id)) return true;
if (!ctx.workerUserId) return false;
return (
run.device_owner === ctx.workerUserId || run.watcher_device_owner === ctx.workerUserId
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Reapply org-membership checks for owned cross-org runs.

This helper now treats device_owner / watcher_device_owner as sufficient on their own, but resolveDeviceClaimableOrgs() only grants cross-org scope while the owner is still a member. Once membership is revoked, authorizeRunForWorker() will still allow heartbeat/completion for already-running cross-org runs because this path ignores the resolved claimable-org set entirely.

[suggested fix: have completion/heartbeat authorization resolve the worker's current claimable orgs and only allow the ownership fallback when run.organization_id is still in that set.]

🤖 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/server/src/utils/device-claimable-orgs.ts` around lines 54 - 66, The
runInWorkerScope helper currently authorizes based solely on
device_owner/watcher_device_owner, letting authorizeRunForWorker accept
cross-org runs even after membership is revoked; update runInWorkerScope to
consult the worker's current claimable orgs via resolveDeviceClaimableOrgs() (or
accept a ctx.claimableOrgIds array) and only allow the ownership fallback when
run.organization_id is still contained in that resolved set—i.e., first check
ctx.orgIds/includes(run.organization_id), then if ctx.workerUserId is present
resolve the worker's claimable org IDs and permit the
device_owner/watcher_device_owner match only when run.organization_id is in that
resolved claimable set.

buremba added 3 commits May 29, 2026 01:29
pi: the widened claimableOrgIds was shared by all claim branches, so one pin in
org B also let the device claim unrelated unpinned capability-matched device
runs in org B. Split scopes: pinned branches (connection/watcher) keep the
widened, membership-gated scope; the unpinned capability branch uses the base
[bound, personal] scope only.
Third endpoint with the same gap (pi): /api/workers/me/watchers/:id/trigger was
base-org scoped, so 'Run now' on a cross-org pinned watcher 403'd. Accept the
watcher's org when the caller is still a member; the existing pin-to-this-device
check is the consent. poll + complete + trigger are now consistent.
@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 29, 2026

pi review (4 rounds, adversarial): caught + fixed 3 real cross-org consistency bugs — completion/heartbeat auth (403 after claim), over-broad scope (one pin exposed unpinned capability runs), and the manual-trigger gap — plus a redeclare. Final verdict: bug_free 78, simplicity 82, 0 bugs, 0 blockers, typecheck/unit/integration all green. Core logic covered by a unit test (runInWorkerScope) + integration test (resolveDeviceClaimableOrgs). Follow-up (deferred): route-level /api/workers/poll + manual-trigger endpoint tests for the pinned-vs-unpinned scope split.

@buremba buremba merged commit 3f24eec into main May 29, 2026
21 of 22 checks passed
@buremba buremba deleted the feat/device-cross-org-pins branch May 29, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants