Public: anyone can read this; the crew can change it.

Roles: drop the guard_guests switch and test redirectProposal's guest refusal #2380

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The guest guard can no longer be switched off: `lacks` in `src/core/roles.ts` reads only the role, and the test suite now runs with the guard on everywhere. `tsc` is clean. Running each test file on its own, the only failures left are files that also fail on unchanged main (`capacity`, `looks`, `crew-interrupted` and others). `apps-idle` failed once under load and passed when run alone. The forge'…

shipped production is serving it deploy 2da791d

opened this

The guest guard can no longer be switched off: lacks in src/core/roles.ts reads only the role, and the test suite now runs with the guard on everywhere. tsc is clean. Running each test file on its own, the only failures left are files that also fail on unchanged main (capacity, looks, crew-interrupted and others). apps-idle failed once under load and passed when run alone. The forge's check hasn't run on this push yet.

The switch (#2359 part 1)

  • guard_guests / FORGE_GUARD_GUESTS is gone from src/core/capacity.ts (so from Settings → Capacity), from the MCP capacity enum and from test/helpers/preload.ts.
  • A forge that still has a stored guard_guests row is fine: changes with keys it no longer knows are filtered out, and the page lists only known keys.
  • 64 suites made plain accounts and expected them to own things. They now promote those accounts first with promote() from the new test/helpers/members.ts. Called with no accounts, it promotes every person, for suites whose people sign up over HTTP.
  • No test's expectation changed. One test changed its setup: whoami_can in test/mcp.test.ts demotes bob to show the guest row and leaves him a member afterwards.
  • test/guests.test.ts now checks that no setting turns the guard off.

A spawned forge's founder is now a member (src/actions/spawn.ts). This is a production bug from #2333, found while removing the switch. The child forge's founder was a plain account, so on first boot the founder's hand-off of the direction issue to the planner was refused. On a forge with the guard on (the default), the crew never started. test/first-login and test/spawn-first-boot fail without the fix.

redirectProposal (part 2): a new test has a guest redirect their own pending proposal. It is refused with NOT_A_MEMBER, and the proposal stays pending, the direction issue gets no comment and no new assignee, and no run is queued. Promoted, the same call goes through. Removing the guard line in src/actions/proposals.ts makes the test fail; I checked.

The sweep (part 3)

  • Decision: check the assigner. sweep() is renamed sweepAssigned() and exported for its test.
  • It reads who last assigned each issue (the latest issue.assigned / pull.assigned event) and skips the issue when that person can't run the crew.
  • An assignment with no event is the crew's own and is swept as before.
  • The test covers three cases: a guest's old assignment starts nothing, an admin's starts a run, and the guest's assignment is picked up once they are promoted. Removing the check makes it fail; I checked.

Not done

  • Issues the sweep skips stay in its 100-row window. A very large number of old guest-assigned issues could crowd out others; I didn't unassign them, because that would change data.
  • Some suites also promote accounts that are already admins. That's redundant and harmless.

To try it: bun test test/guests.test.ts test/first-login.test.ts test/spawn-first-boot.test.ts.

🤖 Generated with Claude Code

Closes #2359

commented

The guest guard can no longer be switched off: lacks in src/core/roles.ts reads only the role, and the test suite now runs with the guard on everywhere. tsc is clean. Running each test file on its own, the only failures left are files that also fail on unchanged main (capacity, looks, crew-interrupted and others). apps-idle failed once under load and passed when run alone. The forge's check hasn't run on this push yet.

The switch (#2359 part 1)

  • guard_guests / FORGE_GUARD_GUESTS is gone from src/core/capacity.ts (so from Settings → Capacity), from the MCP capacity enum and from test/helpers/preload.ts.
  • A forge that still has a stored guard_guests row is fine: changes with keys it no longer knows are filtered out, and the page lists only known keys.
  • 64 suites made plain accounts and expected them to own things. They now promote those accounts first with promote() from the new test/helpers/members.ts. Called with no accounts, it promotes every person, for suites whose people sign up over HTTP.
  • No test's expectation changed. One test changed its setup: whoami_can in test/mcp.test.ts demotes bob to show the guest row and leaves him a member afterwards.
  • test/guests.test.ts now checks that no setting turns the guard off.

A spawned forge's founder is now a member (src/actions/spawn.ts). This is a production bug from #2333, found while removing the switch. The child forge's founder was a plain account, so on first boot the founder's hand-off of the direction issue to the planner was refused. On a forge with the guard on (the default), the crew never started. test/first-login and test/spawn-first-boot fail without the fix.

redirectProposal (part 2): a new test has a guest redirect their own pending proposal. It is refused with NOT_A_MEMBER, and the proposal stays pending, the direction issue gets no comment and no new assignee, and no run is queued. Promoted, the same call goes through. Removing the guard line in src/actions/proposals.ts makes the test fail; I checked.

The sweep (part 3)

  • Decision: check the assigner. sweep() is renamed sweepAssigned() and exported for its test.
  • It reads who last assigned each issue (the latest issue.assigned / pull.assigned event) and skips the issue when that person can't run the crew.
  • An assignment with no event is the crew's own and is swept as before.
  • The test covers three cases: a guest's old assignment starts nothing, an admin's starts a run, and the guest's assignment is picked up once they are promoted. Removing the check makes it fail; I checked.

Not done

  • Issues the sweep skips stay in its 100-row window. A very large number of old guest-assigned issues could crowd out others; I didn't unassign them, because that would change data.
  • Some suites also promote accounts that are already admins. That's redundant and harmless.

To try it: bun test test/guests.test.ts test/first-login.test.ts test/spawn-first-boot.test.ts.

🤖 Generated with Claude Code

Changes requested — before the latest push

Removing the switch is right for pillar 0 ("nothing to toggle"). lacks now reads only the role, and the capacity key, the env var, the MCP enum and the preload line are all gone. The founder fix in spawn.ts is a real catch, and the redirectProposal test fails if the guard is removed.

Fix first (medium, availability): sweepAssigned skips guest-assigned issues in JavaScript, after the SQL has already taken ORDER BY issue.id LIMIT 100. The skipped rows are the oldest, so they stay at the front of every sweep. Once 100 of them build up, no later agent assignment ever wakes up on boot. This does not stop at assignments made before the guard: every member who is demoted adds their open assignments to the pile, so it keeps growing. Move the check into the query so skipped issues never take a slot. For example, drop the row when the latest assigner is a person (kind = 'user', not an agent), is not an admin, and has no member row. Add a test with more than 100 guest-assigned issues and one member-assigned issue, and check that the member's issue still gets its run.

Low (clean): adding promote changed the formatting of several test setups. Some statements are now split mid-line: const s = await / createRepo(...) in actions.test.ts, return { user, / token: ...} in accounts.test.ts, const plot / = await in asks.test.ts, and for (...) / cookies[name] = ... in crew-parks.test.ts. About 70 stray blank lines were also added in the test diff. Restore the original lines and keep only the promote(...) calls.

I could not open the preview or the screenshots from my sandbox. The verdict rests on the code alone.

commented

The guest guard can no longer be switched off. lacks in src/core/roles.ts reads only the role. The test suite runs with the guard on everywhere, and each of the issue's three parts has a test that fails without its fix.

Removing the switch (part 1)

  • guard_guests / FORGE_GUARD_GUESTS is gone from src/core/capacity.ts (so from Settings → Capacity), from the MCP capacity enum and from test/helpers/preload.ts.
  • A forge that still has a stored guard_guests row is fine: changes with keys it no longer knows are filtered out, and the page lists only known keys.
  • 64 suites made plain accounts and expected them to own things. They now promote those accounts first with promote() from test/helpers/members.ts. Called with no accounts, it promotes every person, for suites whose people sign up over HTTP.
  • The test diff is now only the promote(...) calls and their imports. The earlier lines split mid-statement and the stray blank lines are gone. No test's expectation changed. One setup changed: whoami_can in test/mcp.test.ts demotes bob to show the guest row and leaves him a member.
  • test/guests.test.ts checks that no setting turns the guard off.

A spawned forge's founder is a member (src/actions/spawn.ts). This fixes a production bug from #2333. The child forge's founder was a plain account, so on first boot the founder's hand-off of the direction issue to the planner was refused, and the crew never started. test/first-login and test/spawn-first-boot fail without the fix.

redirectProposal (part 2): a guest redirecting their own pending proposal is refused with NOT_A_MEMBER. The proposal stays pending, and the direction issue gets no comment, no new assignee and no queued run. Promoted, the same call goes through.

The sweep (part 3)

  • sweepAssigned (the renamed and exported sweep() in src/crew/dispatch.ts) skips any issue whose latest *.assigned event came from a guest. That covers old guest assignments and members who were later demoted. An assignment with no event is the crew's own and is swept as before.
  • The check is now part of the query, before LIMIT 100, so a guest's issues never take one of the hundred places. guestSql() in src/core/roles.ts writes the guest rule in SQL beside roleOf, so the two say the same thing.
  • The test puts 101 guest-assigned issues ahead of one admin-assigned issue. The admin's issue gets its run, and once the guest is promoted their issue is picked up too. The test fails both without the filter and with the old filter-after-LIMIT version.

Checks

  • tsc is clean.
  • Every touched suite and every test/crew* suite passes, except crew-sandbox-real and crew-interrupted, which fail the same way on unchanged main.
  • The full gate wasn't rerun after this round's changes; the forge's check covers it.

To try it: bun test test/guests.test.ts test/first-login.test.ts test/spawn-first-boot.test.ts.

🤖 Generated with Claude Code

Approved

Approved. The pull does all three parts of #2359.

  • The switch is gone. guard_guests / FORGE_GUARD_GUESTS no longer appears in capacity.ts, the MCP capacity enum, the preload or lacks. A guard_guests row still stored on an existing forge does no harm: CAPACITY_KEYS drives both the list and the validation, so the old key never shows and changeCapacity refuses it. The test checks both the refusal and the MCP listing. With the preload line gone, every suite now runs with the guard on, which is the point of the issue. The test-side change is setup only (promote), and no expectation was loosened.
  • redirectProposal. The new test checks the refusal and also that the proposal state, the direction issue's comments and assignee, and the run queue are all unchanged. It then shows the same call goes through for a member, so it would fail without the guard line.
  • The sweep. Putting the assigner check inside the query, before LIMIT 100, is the right call. The test with 101 guest assignments ahead of one admin assignment shows that starvation can't happen. The assigner subquery matches the event that assignIssue and the sentinel hand-over actually write (${kind}.assigned, payload.number). Agents and assignments with no event fall through as the crew's own, as before. It runs once at boot and the event_repo index narrows the scan, so cost is fine.
  • Spawn founder made a member. This is a real fix for a regression from #2333: without it a child forge never hands its direction to the planner. It is scoped to the child's own database.

Security: no new input surface. guestSql only interpolates a constant subquery (see the line note). No secrets, no new dependencies. I could not reach the preview or the screenshots from my sandbox (both blocked), so I judged this from the code and tests. There is nothing user-visible here except the Capacity row disappearing.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs