Skip to content

QM-36: Let every org admin administer the org-scoped loop - #1118

Merged
pcapriolo-yc merged 2 commits into
qm-29-port-factory-loopfrom
qm-36-s18720
Sep 11, 2026
Merged

QM-36: Let every org admin administer the org-scoped loop#1118
pcapriolo-yc merged 2 commits into
qm-29-port-factory-loopfrom
qm-36-s18720

Conversation

@pcapriolo-yc

@pcapriolo-yc pcapriolo-yc commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Closes QM-36.

Changes

Why this matters: The org-scoped "Software factory" loop is created by whichever org admin first applies the factory config, and until now only that one person could administer it. Every other org admin got a 403 on the loop's detail, fire, pause, decide, grant and autopilot endpoints, and the loop never appeared in their loops list at all. Administration of an org-wide resource was pinned to a single human, so if that admin was away or lost access nobody else could run or unblock the factory.

What changes:

  • Any current org admin can now read, list and administer a loop whose owner scope is the org scope, including GET /v1/loops, GET/PATCH/DELETE /v1/loops/:id, fire, output decisions, grants, autopilot and the loop's ledger items.
  • Ownership is unchanged: the loop keeps its original owner, and actions are attributed to the admin who performed them.
  • Nothing else widens — personal, group, channel and capability-scoped loops behave exactly as before, and a member without an org admin grant (or an admin whose grant or account has been removed) is still refused.
  • The live-human requirement on sensitive actions is untouched: relayed requests without a live human still get human_required on decide, grant creation and enabling autopilot.

Acceptance stories:

  • A second org admin opens the Loops page and now sees and opens the "Software factory" loop, where before the list was empty and the detail page bounced back.
  • A second org admin fires the loop, pauses and resumes it, clears quarantine, toggles autopilot and edits the playbook, and each action is recorded under their own name while the loop's owner stays the original admin.
  • An org member with no admin grant still gets an empty loops list and a "you may not administer this loop" refusal, as does an admin whose grant was just revoked.
  • An org admin still cannot reach another person's personal loop.
  • An API caller on a deployment with no admin service wired sees exactly today's behavior, since the new check simply answers no.

Test Plan

  • NODE_ENV=test ALLOW_UNSIGNED_TEST_IDENTITY=1 node --experimental-test-module-mocks --test test/loop-routes.test.ts
  • NODE_ENV=test ALLOW_UNSIGNED_TEST_IDENTITY=1 node --experimental-test-module-mocks --test test/loop-item-routes.test.ts
  • node --experimental-test-module-mocks --test test/loop-routes.test.ts test/loop-item-routes.test.ts test/loop-factory-loop.test.ts test/admin-grants.test.ts test/admin-resources.test.ts
  • npx tsc --noEmit
  • npx eslint src/api/routes/loops.ts src/api/routes/shared.ts test/loop-routes.test.ts test/loop-item-routes.test.ts
  • Manual: on a local dev stack, applied the factory config as the first admin, then signed in as a second org admin and drove the Loops page (open, fire, pause, resume, clear quarantine, autopilot off/on, back link); reproduced the empty list and 403 first with the fix reverted, and confirmed a revoked grant immediately restores the refusal.

Proof it works

On the same live stack, the second org admin went from an empty Loops list and a 403 detail page to seeing, opening and driving the factory loop, while a non-admin stayed refused.

story0-before-loops-list-empty.png
story0-before-loop-detail-denied.png
story1-loops-list-second-admin.png
story1-loop-detail-second-admin.png
story2a-clear-quarantine.png
story2b-paused.png
story2c-resumed.png
story2d-autopilot-off.png
story2e-autopilot-on.png
story2f-playbook-edit.png
story2g-fire-now.png
story2h-back-to-list.png
story3-review-sections.png
story4-nonadmin-loops-list-empty.png
story4-nonadmin-loop-detail-denied.png

Session recordings, not embeddable here:
page@962d80c2a90f3bf22b520f880cd9f420.webm — .io-agent-qm-36/video/
page@cbac5f4176f618c3135b91a0377817f5.webm — .io-agent-qm-36/video/
page@32ee1b4fa4443c2e3b8d442ca22519a3.webm — .io-agent-qm-36/video/


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Only org-scoped loops reach the check and there is one org loop per
deployment, so a per-request memo on ActingPrincipal bought nothing.
The predicate now lives in canAdministerLoop as a single branch, and
the test that only pinned the cache is gone.
@pcapriolo-yc
pcapriolo-yc merged commit 47d4a87 into qm-29-port-factory-loop Sep 11, 2026
17 checks passed
@pcapriolo-yc
pcapriolo-yc deleted the qm-36-s18720 branch September 11, 2026 21:34
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