You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Posted at andy5995's direction. I am Claude (Opus 5), an LLM made by Anthropic. andy5995 chose this direction; I wrote it up. Edited to fold in the decisions from the comments below.
Serve the game over SSH, so a player connects with an ordinary SSH client instead of a browser. SSH is intended to be how people play remotely.
It suits a terminal game: SSH already provides the encrypted connection, a terminal with a known size, and a per-client identity, all from a protocol reviewed far more thoroughly than anything here. The engine already talks to a byte stream, so this is a new stream implementation, not new game code.
Transport
golang.org/x/crypto/ssh — same module family as the existing x/term and x/sys. The server needs a host key, generated on first run and stored in the data directory with the other per-install files.
Terminal size arrives in the SSH pty-req and changes arrive as window-change, which is better information than the door front-end gets. SSH says nothing about encoding, so the CP437/UTF-8 choice is unaffected.
Identity: record the key on first login
No sysop pre-approval, no password. Boards have always been open to whoever connects. The first time a public key connects, the server records it and that player onboards a realm as any new caller does; the same key returning is the same player, a different key is a different player.
Index by the SHA256 fingerprint rather than the marshalled key — short, stable, printable, suits a map key and a world-file field, and the player can check it against their own ssh-keygen -l.
What this does and does not achieve:
Impersonation: solved. The private key never leaves the client, unlike the browser front-end's cookie, which is a bearer token — whoever holds it, is you.
Loss of access: solved. A key carries between machines; a browser cookie does not.
Discarding keys depends on #83. A key is discarded when its empire is removed, in that same step, driven by that one decision. Not a separate timer: a key expiring while its empire lives locks that player out of a living realm with no way back in. Coupled, the worst case is harmless — connect, not recognised, start a new realm. Transport and first login can be built before #83; only discarding waits on it.
One key means one realm, so a laptop and a desktop with different keys are two realms with no way to merge. A later addition could let a logged-in player authorise a second key from the System menu. Not needed for a first version.
A security review is still required
SSH narrows the question; it does not remove it. Crypto, transport, and authentication come from x/crypto/ssh rather than from this repo — but session lifetime and cleanup, limits on what one caller can cause, what a connected stranger can do to the shared world file, and the game code now being reachable by anyone who can open a socket all remain this project's own code and risk.
The review-status notice the user documentation carries for the web front-end needs an equivalent here. It should not be dropped on the grounds that SSH is involved. See #82.
Prior art: ssh-chat
ssh-chat by Andrey Petrov (MIT) is a chat server built as an SSH front-end to a terminal application, and has solved most of this already. Anything taken from it — technique, constant, or parsing routine — should say so in a comment at that spot, naming the project and its license, even where nothing is copied verbatim.
Worth adopting:
Fingerprint as the identity key (sshd/auth.go), as above.
Per-connection read rate limiting (sshd/ratelimit.go): a small read counts 1, a large read counts its bytes, against a per-minute budget after a startup grace — written to tell a human typing from a bot spamming. Short enough to write against the standard library rather than take their dependency.
Bans by address, key fingerprint, and client version. A fingerprint is the right handle for a sysop ban: it survives an address change, and unlike a cookie the caller cannot discard it and return as someone new for free.
PTY parsing (sshd/pty.go): x/crypto/ssh hands over pty-req and window-change undecoded. Follow theirs rather than re-derive the wire format.
Speculative: ssh-chat reads the session environment, so a forwarded LANG would hint at charset and language — something no drop file provides. I have not checked whether common clients send it (OpenSSH sends only what SendEnv is configured to), so it may be worthless.
Private boards: low priority
An open, public server is the goal, and the design should be built for that. Anyone wanting a private game is welcome to one, but it should not shape the identity model.
When built, the shape: the host sets a password and gives it to their players. Two layers, cleanly divided — the password controls entry, the key controls who you are. The host distributes one secret and never collects keys, which is what made an allowlist unattractive.
One constraint worth settling early, because it decides where the check lives: the password should be an SSH authentication method, not an in-game prompt. An in-game prompt lets every stranger reach game code before proving anything, enlarging exactly the surface a review must examine. That is not SSH's default, where any single successful method suffices; my understanding is x/crypto/ssh can require a second method by returning a partial-success error from the public key callback, but I have not verified that against the version used here. If unavailable, the fallback is to accept the password alone and record whichever key was offered.
The browser front-end
It stays for now, but only as a working reference for the part of this that is not SSH: it is the one front-end holding a single process, a single lock, an in-memory world, and several concurrent sessions against it. An SSH listener needs that same shape, and the door's file-per-action store is the wrong model for it. Once this server is running and has absorbed that pattern, the browser front-end is removed — handlers, page, session type, docs.
Its user documentation comes out earlier than its code, as soon as work starts here. No reason to keep inviting people to set up something being retired.
That makes #77, #78, #79 and #80 work that will not be done; they should be closed as will-not-fix once this is under way rather than sitting on a milestone as if planned. #81 is the exception, restated above.
Serve the game over SSH, so a player connects with an ordinary SSH client instead of a browser. SSH is intended to be how people play remotely.
It suits a terminal game: SSH already provides the encrypted connection, a terminal with a known size, and a per-client identity, all from a protocol reviewed far more thoroughly than anything here. The engine already talks to a byte stream, so this is a new stream implementation, not new game code.
Transport
golang.org/x/crypto/ssh— same module family as the existingx/termandx/sys. The server needs a host key, generated on first run and stored in the data directory with the other per-install files.Terminal size arrives in the SSH
pty-reqand changes arrive aswindow-change, which is better information than the door front-end gets. SSH says nothing about encoding, so the CP437/UTF-8 choice is unaffected.Identity: record the key on first login
No sysop pre-approval, no password. Boards have always been open to whoever connects. The first time a public key connects, the server records it and that player onboards a realm as any new caller does; the same key returning is the same player, a different key is a different player.
Index by the SHA256 fingerprint rather than the marshalled key — short, stable, printable, suits a map key and a world-file field, and the player can check it against their own
ssh-keygen -l.What this does and does not achieve:
ssh-keygencosts a second and requires nothing of the person running it, so realms can be created as fast as with cookies. Unrestricted identity creation lets a script flood the world with empires #81 describes this for the browser front-end and applies here unchanged.Discarding keys depends on #83. A key is discarded when its empire is removed, in that same step, driven by that one decision. Not a separate timer: a key expiring while its empire lives locks that player out of a living realm with no way back in. Coupled, the worst case is harmless — connect, not recognised, start a new realm. Transport and first login can be built before #83; only discarding waits on it.
One key means one realm, so a laptop and a desktop with different keys are two realms with no way to merge. A later addition could let a logged-in player authorise a second key from the System menu. Not needed for a first version.
A security review is still required
SSH narrows the question; it does not remove it. Crypto, transport, and authentication come from
x/crypto/sshrather than from this repo — but session lifetime and cleanup, limits on what one caller can cause, what a connected stranger can do to the shared world file, and the game code now being reachable by anyone who can open a socket all remain this project's own code and risk.The review-status notice the user documentation carries for the web front-end needs an equivalent here. It should not be dropped on the grounds that SSH is involved. See #82.
Prior art: ssh-chat
ssh-chat by Andrey Petrov (MIT) is a chat server built as an SSH front-end to a terminal application, and has solved most of this already. Anything taken from it — technique, constant, or parsing routine — should say so in a comment at that spot, naming the project and its license, even where nothing is copied verbatim.
Worth adopting:
sshd/auth.go), as above.sshd/ratelimit.go): a small read counts 1, a large read counts its bytes, against a per-minute budget after a startup grace — written to tell a human typing from a bot spamming. Short enough to write against the standard library rather than take their dependency.sshd/pty.go):x/crypto/sshhands overpty-reqandwindow-changeundecoded. Follow theirs rather than re-derive the wire format.Speculative: ssh-chat reads the session environment, so a forwarded
LANGwould hint at charset and language — something no drop file provides. I have not checked whether common clients send it (OpenSSH sends only whatSendEnvis configured to), so it may be worthless.Private boards: low priority
An open, public server is the goal, and the design should be built for that. Anyone wanting a private game is welcome to one, but it should not shape the identity model.
When built, the shape: the host sets a password and gives it to their players. Two layers, cleanly divided — the password controls entry, the key controls who you are. The host distributes one secret and never collects keys, which is what made an allowlist unattractive.
One constraint worth settling early, because it decides where the check lives: the password should be an SSH authentication method, not an in-game prompt. An in-game prompt lets every stranger reach game code before proving anything, enlarging exactly the surface a review must examine. That is not SSH's default, where any single successful method suffices; my understanding is
x/crypto/sshcan require a second method by returning a partial-success error from the public key callback, but I have not verified that against the version used here. If unavailable, the fallback is to accept the password alone and record whichever key was offered.The browser front-end
It stays for now, but only as a working reference for the part of this that is not SSH: it is the one front-end holding a single process, a single lock, an in-memory world, and several concurrent sessions against it. An SSH listener needs that same shape, and the door's file-per-action store is the wrong model for it. Once this server is running and has absorbed that pattern, the browser front-end is removed — handlers, page, session type, docs.
Its user documentation comes out earlier than its code, as soon as work starts here. No reason to keep inviting people to set up something being retired.
That makes #77, #78, #79 and #80 work that will not be done; they should be closed as will-not-fix once this is under way rather than sitting on a milestone as if planned. #81 is the exception, restated above.