When it refuses
The four login states and what each one means, terminal refusals, and the missing-string case.
When it refuses — the four states, and what each one means
Run saphan console admin status first. It distinguishes the states that look identical
from the browser.
The browser shows 503 and "no master-admin credential has been set"
status says NOT SET. Nothing is wrong; the console has simply never been
provisioned in this workspace. Go to §1.
The page itself carries the way out — the verb to run, the fact that it asks rather than
taking a flag, and the restart — numbered, so you do not have to come here to find it. It
names the workspace by its short name only; the absolute paths are deliberately not on
that page, because anyone who can reach the port can read it. saphan console admin status,
run in a shell on the host, prints them.
If you set the credential and the page does not change, you have met the one-per-process resolution: restart the server (§1.3). The page says so as step 2. This catches people on their first console, not only on a rotation.
Note that <state> is per workspace. A credential set in one workspace does not open
the console of another.
saphan server refuses to start, naming the console
Something like:
This is deliberate and it is not negotiable by configuration. A credential the engine
cannot trust leaves exactly two alternatives — serve /console open, or serve it shut
without saying why — and both are worse than refusing to start. Fix the custody:
Then start the server again. If the file is corrupt rather than mis-permissioned
(malformed), re-set the credential per §1.1; that overwrites it.
An absent credential never stops the server. That is §4.1, a different state: the server starts, everything else serves, and the console says it is closed.
The login form appears, the password is right, and it still refuses
In order of likelihood:
- Wrong username. The refusal is identical for both by design, so
statusis what tells you the username on file. - A tripped attempt limit — but that answers
429with a countdown, not the ordinary refusal. See §2.4. - The credential was rotated in another terminal and the server was never restarted
(§3.2). This is the common one, and it looks backwards: the server is still verifying
against the password it read at start-up, so the NEW password is refused and the OLD
one works.
statusshows the new credential — it reads the file, which is not what the running server is using. Restart the server.
Login "succeeds" and the browser lands straight back on the login form
The browser is discarding the session cookie. This is almost always §5: the console is
being served over plain HTTP to a host that is not loopback, so the cookie is issued
Secure and the browser drops it on an insecure connection.
The login page states its own cookie mode, at the foot of the form — read it. If it says the connection is plain HTTP and not loopback, serve the console over HTTPS or reach it over loopback (an SSH tunnel is the usual answer):
Then open http://127.0.0.1:7654/console on your own machine.
A console screen is missing a string — where that is said, and where to ask
The engine validates the embedded UI bundle — tokens, both string catalogs, and the screen
inventory — once at the start of every saphan process, before any verb runs. Nothing
about the paragraphs below changes that: a bundle that fails parity stops every command
with exit 2 and a class="ui-assets-parity" line, whichever verb you typed.
What is narrower is the notice about a screen key the owner has quarantined — a key a
screen asks for that resolves to no string in either catalog, held open because which way
it gets resolved is a content decision, not an engineering one. That notice used to be
printed by every process this binary started, including the pre-commit hook Git runs on
every commit and saphan gate … --gate merge, where it is not the answer to the question
being asked.
It is now said to the verbs that render or serve those screens:
Ask for it directly, from anywhere, at any time:
That verb is where the finding lives for everyone else. It loads the same embedded bundle,
prints the screen-key counts, names every unresolved key with the reason it was quarantined,
and exits 2 while any key is unresolved — so it is safe in a gate or a CI step, and a
quiet console verb can never be mistaken for a sound bundle. A sound bundle is not silence
either: it prints its counts and the word ok.
And when the owner has resolved the last key — the string written into both catalogs, or the screen stopped asking for it — the same verb answers:
The audience count on the second line is derived from the assembled command tree, never a list typed beside it, and a census test compares it against a checked-in table — so adding a verb to this binary turns that test red until somebody says which side of the line it is on.
The count is of every command in the tree, hidden ones included. Hidden decides what
--help lists; it decides nothing about who speaks, so a hidden verb enrolled in the audience
would print the notice like any other, and the census counts it and marks it hidden. The one
command outside the count is help itself, which Cobra manufactures and nobody enrols.
When it refuses
Every refusal below happens before the connection becomes a websocket, so what you see is an ordinary HTTP status, not a socket that opens and dies.
| what you see | what it means | what to do |
|---|---|---|
303 to /console/login | no valid session | log in at /console/login; if you were logged in, the server was restarted — the session table is in memory by design |
| 403 "that Origin is not this console" | the handshake came from a page served somewhere else | open the console at its own address; this is the cross-site protection, and it is working |
| 403 "…names its Origin" | no Origin header at all — so, not a browser | if you are driving this by hand, send Origin: carrying the exact console address you dialled |
| 400 "that is not a machine id" | ?machine= is not a registry name in shape — it has a space, a slash, a semicolon | use the id saphan machine list prints |
400 "<id> is not an admitted machine" | the id is well formed and the registry does not carry it | saphan machine list prints the ids that exist; saphan machine admit is what adds one. This is a security refusal, not a typo check — see §6a |
400 "<id> was RETIRED and is off the roster" | the machine is in the registry, marked retired | re-admit it (saphan machine admit) if it is back in service; retirement keeps the row for the record and takes no work |
400 "<id> is admitted with transport local" | that machine is not one this engine dials over ssh | use ?machine=local for a terminal on this host |
400 "<id> … carries no ssh host" / "names no ssh key" | an admitted row the fleet's ssh config projects no Host stanza for | saphan machine renew --id <id> --ssh-key <path> |
| 400 "no machine has been admitted in this workspace yet" | there is no machine registry at all | saphan machine admit first. ?machine=local still works — the near path needs no registry |
400 "<id> carries the master seat <seat>" | a seat on that machine is registered role=master | the fleet reserves it; saphan machine seat --role executor is the owner act that changes the row. ?machine=local meanwhile — see §6b |
400 "<id>'s seat <seat> last ran out of lane at …" | the run ledger reads that seat's model allowance as still spent | wait — a spent lane heals by itself and there is nothing to fix. saphan fleet usage asks the vendor directly; ?machine=local meanwhile — see §6b |
| 400 "the run ledger could not be read" | the gate could not ask whether the lane is spent | a gate that cannot ask its question does not answer it with a pass; check the store (saphan fleet dispatchable reads the same ledger) |
400 "cols must be a whole number…" | the window is out of range | 1–1000 columns, 1–500 rows |
| 405 | you used something other than GET | the terminal is a GET upgrade |
| 500 "…needs HTTP/1.1" | the connection cannot be upgraded | do not put an HTTP/2-only proxy in front of /console/pty |
| 503 console closed | the credential cannot be read | saphan console admin status, then restart the server |
And two that arrive after the socket is open, as a 0x02 notice, because by
then there is no status left to send:
- "the terminal could not be opened: …" — the host has no pseudo-terminal available, or the shell could not start. On the remote path this is also what an ssh failure looks like; the reason follows the colon.
- "that window (…) was ignored" — a resize outside 1×1..1000×500. It is refused rather than clamped, so the browser and the terminal never disagree about geometry.