The console's signing-identity custody: keep the default, refuse the one harmful run
Status: decided on the maintainer's ruling; the refusal and the identity-location option are both built. The behaviour is specified in SERVER_JOB_API.md, the option in Where the identity is: the console's option, with the operator-facing account in CONSOLE.md; this note records the posture and why it was taken rather than restating those rows. The CLI's own posture is signing-identity-custody.md, the decision this one was left out of. See docs/notes/README.md.
The console creates and reads this party's signing identity in the one working directory the operator mounts, unless they point it somewhere else. On a single-mount console that directory is also the folder a shared-directory exchange rendezvouses out of, because JOB_RENDEZVOUS_DIR falls back to JOB_DATA_ROOT -- so the long-lived private key sits in the folder the partner writes into, and the operator's sync tool hands it over. Advisory copy on the receipts card is the console's whole answer to that layout without this decision; the decision adds a refusal for the one run where copy is not enough, and holds the copy to what the refusal does and does not see.
Why the default location stays
The CLI resolves no location for the identity and refuses to invent one, for reasons recorded in its own note. The console is not in that position: it already has a mounted working directory the operator chose, it composes an explicit signing.identity_file on every job, and it never relied on a default the CLI removed. Creating the key there on demand costs an SFTP or WebRTC exchange nothing at all -- nobody syncs that folder on those channels -- and it is what makes a first signed exchange possible with one mount and one variable.
So the default is not the defect. Exactly one layout is, and it is a layout the console can recognize.
Why this one is a refusal and not a warning
The console's standing posture is warn-and-guide: the operator is the machine's own user, and a hard block on their own choice is the wrong shape (see CLAUDE.md, Applications). A credential file in the single mount warns, and the run proceeds.
Key disclosure does not fit that shape. What the operator would be choosing is not a risk they carry themselves and can weigh -- it is handing a partner the key that signs for them with every other partner, and no later choice of theirs takes it back. The warning also arrives where it cannot be acted on cheaply: the remedy is a mount change, which means restarting the console, so an operator reading the warning mid-authoring is being asked to abandon the run either way. Refusing states the same thing at the moment it is true and leaves the authored draft alone.
The refusal stays narrow so that the posture does not spread. It fires only for a shared-directory exchange, only where the identity file is in the synced folder, and only where the comparison positively found the collision. Every other layout runs, including layouts where the check is merely unsure -- a refusal is owed a positive finding, and an operator hard-blocked by a check that could not resolve a path has no remedy the console can name.
"Would have a key" is the identity file's presence, and only that -- presence, never readability. A file the console's own uid cannot open is a file the partner's sync copies all the same, so a readability test would admit the run on exactly the layouts a mode-000 key, a uid-mapped mount, or a key minted under another account puts the operator in. The console passes the identity path to the CLI child explicitly and the child LOADS this party's identity from it -- a certificate run with nothing there is refused by the CLI, with its own guidance, rather than given a key it just made. So no run creates the key it would go on to publish, and the presence probe is the whole condition.
Creating the identity is refused on no layout. The rendezvous falls back to the data root, so on the default one-mount console every mint would land in a directory a leg holds: refusing there would take signed exchanges away from that console entirely, including the SFTP and WebRTC exchanges where nothing is synced at all. The key is created on demand in the working directory, which is the default the ruling keeps, and the one harmful run is the one refused.
One moment is refused rather than one layout: a mint while a shared-directory exchange holds the single exchange slot and a leg holds the directory the key would land in. The run's own check ran before that key existed, so without this the console would write a private key into a folder its own exchange is syncing as it runs -- the one case where the console, not the operator, chooses the moment. It refuses the mint alone: reading a fingerprint from an identity already there changes nothing about where that key is.
What the refusal is not
It is not containment. A configured rendezvous is kept in step with the partner's copy by the operator's own sync tool, continuously; a key written into that folder is the partner's from the moment it lands, whether or not an exchange is ever started. By the time the console refuses a run, the disclosure the refusal names has already happened.
What the refusal stops is the console taking that step itself -- starting an exchange whose own protocol step copies the folder to the partner -- and what it gives the operator is the layout, named at the moment the console can name it, with the mount change that ends it. Choosing to keep a key in a folder they sync stays the operator's choice, met the way the console meets the operator's choices: the receipts card warns and points at the remedy. That is why the refusal is no guarantee about the key, and why the card keeps its word about the layout where the refusal is in force.
What the check can and cannot see
The comparison is the one the rendezvous report already had for sharesDataRoot, read per run rather than at boot since the identity is created between runs: each directory as configured, as its real path, and by filesystem identity along the data root's ancestor chain, which is what catches one host folder bind-mounted at two container paths.
One host folder mounted twice, outside that ancestor chain, stays invisible to it -- no path or identity the console can reach expresses the aliasing.
The other half of the refusal is a presence probe: one file name at each location the console knows -- the fixed name under the mounted working directory, and the file the operator picked where they picked one. Those names, and whether something is at them, are the whole of what the console knows about the key. The receipts card says so on the layouts where a partner syncs the key's folder or the console could not rule that out -- what the refusal covers where the shared layout was established, and what no refusal covers where it was not. Where the report positively says the rendezvous has a mount of its own the card raises neither: nothing about the key is in a folder the partner reads, and a warning shown on the recommended layout as well as the hazardous one tells the operator nothing about which they are in.
Limits accepted, not closed
These are accepted under the deployment the console is for: the partner is an authenticated party under a signed agreement, and one writing into the synced folder to attack this party is not the case being designed against. They are recorded here, and in the spec's refusal section, so the refusal is not read as a control against a hostile partner. The last two are not about the partner at all: they are windows the console's own checks leave open, recorded rather than gated.
- The folder syncs at all times, not only during a run. The disclosure a key in that folder is has already happened by the time any run is started, so the refusal signals the layout rather than preventing it. The advisory is what names that, and the mount change is what ends it.
- The probe reads a name, not a key. It reads that name in the folder the partner writes into. A file a partner plants there refuses a run that would have published nothing -- an exchange by a party with no identity of its own anywhere -- and an identity the operator renamed, without pointing the location option at the new name, is not seen at all, so a run that would publish it is admitted.
- A document at that path is taken as this party's identity. The console points the CLI at that path for every fingerprint request and for runs on every channel, and the CLI loads whatever is there. A document bound to a different identity label is warned about rather than refused on that load (
warnOnIdentityDivergence), and the console discards the child's stderr; one bound to the same label -- the label the agreed terms hold, which the partner knows -- diverges from nothing, soalcove exchangeaccepts it too. A partner able to write into the synced folder could therefore put their own signing identity at that path. - The two refusals exclude each other in one direction only. The mint reads the exchange slot; the run reads whether the identity file is there, and nothing makes it read a mint already under way. A shared-directory run created after the mint's child started and before that child wrote the file is admitted, over a window of one child process start, and the key then lands in the folder that run syncs. A gate over both would buy that one window at the cost of a second refusal on a surface whose whole point is how narrow the first one is.
- A picked location inside a synced leg takes the write the read leaves open. The syncing refusal applies at the console's own default location only: it fires where nothing is at the path, and a picked location is read only where something is, so applying it there would refuse nothing. Where the operator has mounted the secrets directory inside a rendezvous leg, the file removed between the presence check and the child's load is created into a folder a shared-directory run is syncing as it goes -- the disclosure that refusal exists to prevent at the default location. Mounting the secrets directory read-only is what closes it; the refusal cannot.
The remedy for all of them is the one the refusal already names: keep the identity outside every folder a partner syncs. The identity-location option makes that a pick in the console rather than a mount change.
Why the location is an option rather than a second default
The maintainer's ruling also has the key's location become a console option: the CLI's own signing.identity_file, prefilled with the default and changeable by browsing a mount, mount-relative and resolved server-side, exactly as the connection's credential file is browsed today.
Keeping it an option rather than moving the default is the same judgement the default's own section records. A console with one mount and one variable still makes a first signed exchange, and graduation copies the configuration unchanged either way, because the option composes the key the CLI already reads. What the option adds is a second remedy for the one layout the refusal is about -- move the key, rather than move the rendezvous -- available without restarting the console, which is what made the warning hard to act on.
Two properties follow from the console creating the identity at its default location ONLY:
- A picked location is read. With nothing at it, the console reports that rather than minting a key in a directory the operator keeps their own way. That is what lets the directory be mounted read-only, which is the posture the deployment guide recommends for key material -- and read-only is also what closes the gap between the console's presence check and the child's load, where a file removed in between would otherwise be created.
- The console writes nowhere but the data root, save the window above. The certificate export lands there, and the fingerprint child's working directory is pinned there whatever the identity's location, so an
alcove.yamlin the browsed mount never becomes the child's config.
The refusal reads both the identity this run loads and the default in the data root. Moving the option does not move the file, and a key left behind in a folder the partner syncs is a key the partner reads whether or not this run is the one that loads it. The card therefore raises that leftover key at the weight the layout gives it: where the rendezvous report says the folder is shared or cannot rule it out, the caveat is a warning of its own -- the weight the identical disclosure takes with no location picked -- rather than a line inside the picked-location notice. Where the rendezvous has a mount of its own it stays inside that notice, because nothing about the key is then in a folder the partner reads.
Why a name the terms do not state is refused too, and guided rather than re-bound
The identity is bound to a party name when it is created, and the console's create-or-reuse never rebinds it: the label a later fingerprint request sends is ignored on a reuse. So one long-lived key meets an exchange that names this party something else, and that run cannot finish -- the partner authorizes the certificate against the agreed terms and rejects a receipt signed under any other name. The CLI refuses it at identity load, before any connection, credential, or row of data leaves the machine.
This is a refusal for the reason above it is: warn-and-guide governs a choice the operator may legitimately make, and this is not one. The run does not complete either way, and a console that let the launch through would spend the press on a failure whose explanation it already had. The statement therefore stands where the operator is at launch -- the control that starts the exchange, with a line on the receipts card pointing there, since the card is where the identity is authored and neither remedy is a control on it.
The remedies are stated and not offered. Both are the CLI's own: name this party in the terms as the identity is bound, or re-key under the name the terms state. A console button for the second would mint a key with a new fingerprint and silently invalidate every pin a partner holds, which is the same reason re-keying is not offered anywhere else on the card. A button for the first would edit the agreed terms from a signing control, at the point the operator is reviewing what they agreed on. Guidance costs an operator one edit they can see; an affordance for either is deferred until that edit is shown to be the friction.
The console compares a value it reads rather than deciding the question: the fingerprint endpoint reports the name the certificate is bound to, and a file it cannot read a name from reports none, which the console treats as nothing to compare rather than as agreement. The run's own refusal, which reads the file itself, stays the authority.
Both remedies are stated only where both are open. A name field is not a way to reach every string: what is typed there reaches the agreed terms trimmed and NFC-normalized, so an identity bound to a name with space around it, or to another Unicode form of one, is a name no entry restates. Sending that operator to the field would hand them back the same refusal over two names that look alike on the screen, with nothing to see for the attempt -- so that class states the re-key alone, and names which of the two differences it is instead of quoting a value whose difference is invisible. It is the same shape as a bound name no terms document may state, one step earlier: there the terms close the edit, here the field does.
See also
- SERVER_JOB_API.md - the refusal's conditions, its comparison, and the
400body that names it - CONSOLE.md - what the operator meets, and the mount that resolves it
- SERVER_JOB_API.md - the location option: the locator, its resolution, and what a picked location is read rather than created
- DEPLOYMENT.md - the CLI's own mounts for the identity, read-only after creation
- signing-identity-custody.md - the CLI's posture: no invented location, and what each command does without one
- SERVER_JOB_API.md - the bound party name the fingerprint response holds, how it is read, and when it is absent