A guest login is a full account with a clock on it

Mitchell, on the read-only version built earlier today: scratch it. He hands
these to people he trusts and revokes them otherwise, so a console with half
its tabs missing solved a problem he doesn't have.

So the allow-list, the middleware check and every "not for a guest" branch in
the UI are gone. What is left is the part that was the point: a throwaway
account that expires by itself, is checked on every request, swept once a
minute, and can be ended in one tap. A guest now gets the whole console on
every machine, terminal included.

Said once, since it is his call and he has made it: that means a guest has a
root-capable shell on every host, and can make an account or a token of its own
that outlives the guest login.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-27 17:53:55 -07:00
co-authored by Claude Opus 5
parent 8a7167bf03
commit 179f342a2c
11 changed files with 65 additions and 346 deletions
+7 -7
View File
@@ -20,9 +20,9 @@ export default function Users({ me }: { me: Me }) {
const GUEST_MINUTES = [15, 30, 60, 120, 480] as const
// A login to hand somebody so they can look at the console: it expires on its own, it can be
// ended here at any moment, and it can only read. Making one is the only way to see its
// password, so it is shown until dismissed rather than in a toast.
// A login to hand somebody you are showing the console to: an ordinary account with a clock
// on it, which expires on its own and can be ended here at any moment. Making one is the only
// time its password is readable, so it is shown until dismissed rather than in a toast.
function Guests() {
const [list, setList] = useState<Guest[] | null>(null)
const [minutes, setMinutes] = useState<number>(30)
@@ -39,16 +39,16 @@ function Guests() {
finally { setBusy(false) }
}
const revoke = async (g: Guest) => {
if (!await askYes(`End the guest login ${g.name}? Whoever has it is signed out at once.`, 'Revoke')) return
if (!await askYes(`End the guest login ${g.name}? Whoever has it is signed out at once, everywhere.`, 'Revoke')) return
try { await api.revokeGuest(g.name); toast(`${g.name} revoked`); if (fresh?.name === g.name) setFresh(null); load() }
catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') }
}
return (
<>
<div className="dash-bar"><h2 className="section">Guest logins</h2><span className="spacer" /><span className="muted small">temporary, and can only look</span></div>
<div className="dash-bar"><h2 className="section">Guest logins</h2><span className="spacer" /><span className="muted small">temporary, and can be revoked</span></div>
<section className="card">
{list === null && <Loading label="Loading…" />}
{list?.length === 0 && <div className="empty">None. One lets somebody look around without an account.</div>}
{list?.length === 0 && <div className="empty">None. One lets somebody in for a while without giving them an account.</div>}
{list?.map((g) => {
const left = new Date(g.expires).getTime() - Date.now()
return (
@@ -67,7 +67,7 @@ function Guests() {
{GUEST_MINUTES.map((m) => <option key={m} value={m}>{m < 60 ? `${m} minutes` : `${m / 60} hour${m === 60 ? '' : 's'}`}</option>)}
</select>
</label>
<div className="svc-actions"><Btn className="btn on" busy={busy} onClick={create}>Create guest login</Btn><span className="muted small">A guest sees the dashboard, containers, disks and hardware, read-only. No terminal, no files, no settings, nothing it can change. It stops working when the time is up, or when you revoke it.</span></div>
<div className="svc-actions"><Btn className="btn on" busy={busy} onClick={create}>Create guest login</Btn><span className="muted small">A guest can do everything you can, on every machine — including the terminal — until the time is up or you revoke it. Give one out accordingly.</span></div>
</div>
</>
)