feat(felis): add Operator break-glass op behind an operation menu

When a staff account already exists, the break-glass console now opens on a
thin top-level menu (menuModel) where account operations are peers rather than
tails of one wizard: provision/reset the Owner, or add an Operator. A fresh
machine with no Owner skips the menu and goes straight to Owner bootstrap, since
minting an Operator first would create a staff account the login gate rejects.

The Operator path reuses ownerModel via a bgOperation discriminator. It is
insert-only (performAddOperator -> InsertOperator), wraps a duplicate username as
api.ErrConflict and routes back to the provision form for a retry rather than
tearing down, and deliberately never flips the global local_auth toggle the way
the Owner thread does. The post-exit summary and audit trail distinguish the two
outcomes (isOperator); only the Owner provision claims local-password login was
enabled.

Tests cover the operator-model defaults, path selection (insert vs upsert and
the local-auth gate), conflict-retry versus generic teardown, isOperator
propagation, and the root menu routing for both fresh and admin-present
machines.
This commit is contained in:
flyemoji committed 2026-06-30 15:40:24 +09:00
1 parent ac02c69612
commit eb5875a699
6 files changed
+513 -28

No files matched your search

+9 -4
View File
@@ -149,14 +149,19 @@ func TestRootRerunLandsOnStatus(t *testing.T) {
}
func TestRootBreakGlassQuitsAfterOwner(t *testing.T) {
// Break-glass starts at owner and must quit on owner completion without
// entering the connection chooser.
// Break-glass with a staff account present opens on the operation menu; choosing
// "provision/reset the Owner" lands on the owner screen, which must quit on
// completion without entering the connection chooser (that step is setup-only).
m := newTestRoot(true, consoleModeBreakGlass, "")
if m.stage != stageMenu {
t.Fatalf("break-glass initial stage = %v, want stageMenu", m.stage)
}
m = drive(t, m, menuChoiceMsg{op: bgProvisionOwner})
if m.stage != stageOwner {
t.Fatalf("break-glass initial stage = %v, want stageOwner", m.stage)
t.Fatalf("after the menu choice, stage = %v, want stageOwner", m.stage)
}
if _, ok := m.screen.(*ownerModel); !ok {
t.Fatalf("break-glass initial screen = %T, want *ownerModel", m.screen)
t.Fatalf("after the menu choice, screen = %T, want *ownerModel", m.screen)
}
next, cmd := m.Update(ownerResultMsg{username: "owner", displayPassword: "pw", mode: "recovery"})