Saphan StudioDocs
The console

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:

server: console: consoleauth: credential file has loose permissions: /…/console/admin.cred is 0644 (want 0600) — refusing to load a group/other-reachable credential

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:

chmod 600 /path/to/workspace/.saphan/state/console/admin.cred

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:

  1. Wrong username. The refusal is identical for both by design, so status is what tells you the username on file.
  2. A tripped attempt limit — but that answers 429 with a countdown, not the ordinary refusal. See §2.4.
  3. 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. status shows 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):

ssh -N -L 7654:127.0.0.1:7654 user@the-fleet-machine

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:

saphan console …      saphan serve      saphan server …      saphan serverd …

Ask for it directly, from anywhere, at any time:

saphan checks ui-assets

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.

$ saphan checks ui-assets
checks ui-assets: 346 screen-key requests; 320 distinct; 319 resolved; 1 unresolved; 38 orphaned catalog strings
checks ui-assets: the startup notice is heard by 23 of 160 commands
screen-key-unresolved: key="set.sys.pending"; screen="settings-system"; reason="owner decision pending: …"
$ echo $?
2

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:

$ saphan checks ui-assets
checks ui-assets: 345 screen-key requests; 319 distinct; 319 resolved; 0 unresolved; 38 orphaned catalog strings
checks ui-assets: the startup notice is heard by 23 of 160 commands
checks ui-assets: ok
$ echo $?
0

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 seewhat it meanswhat to do
303 to /console/loginno valid sessionlog 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 elseopen 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 browserif 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 semicolonuse the id saphan machine list prints
400 "<id> is not an admitted machine"the id is well formed and the registry does not carry itsaphan 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 retiredre-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 sshuse ?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 forsaphan machine renew --id <id> --ssh-key <path>
400 "no machine has been admitted in this workspace yet"there is no machine registry at allsaphan 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=masterthe 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 spentwait — 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 spenta 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 range1–1000 columns, 1–500 rows
405you used something other than GETthe terminal is a GET upgrade
500 "…needs HTTP/1.1"the connection cannot be upgradeddo not put an HTTP/2-only proxy in front of /console/pty
503 console closedthe credential cannot be readsaphan 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.