[core] ssh_key and an identity's ssh_key are the same thing under two names, and nothing said which one won. The resolution was: the identity's if it names one, otherwise [core]'s. So an identity that named no key pushed with the default account's key, silently - which is exactly the mixing identities exist to prevent.
The paragraph directly beneath that rule in docs/configuration.md argued against it: "Pushing under the wrong key fails loudly; pushing under your default key when you asked for another one does not fail at all, which is the direction worth being careful about." dejima refused a typo'd identity name and permitted a partial one, while its own stated reasoning condemned both.
Identities ship in this release and [core] ssh_key does not - it is already on main. So this is the one moment where the two spellings either converge or calcify.
What changed
[identity.default] is the identity in force when none is named. It is where a host's ordinary account goes, and having a name is what lets it carry a git name and a forge credential beside the key, rather than those coming from three different places.
[core] ssh_key is deprecated, not removed. A config with no [identity.default] reads it exactly as before and says once at launch what replaced it. A config with both is refused rather than ranked - which key a session pushed with should not depend on precedence rules a reader has to know.
Credentials no longer fall through. An identity that names some and leaves the rest to the section beneath it is refused, naming each one. Only the selected forge's keys: refusing over a credential nothing on this launch reads would let an unrelated section block a launch.
One gap, documented rather than left to be found. "Set" means written in the config file. A forge key carrying a schema default - [forge.gitea] token_file - still answers for an identity that names none, and nothing refuses that, because nothing in the file says it. The docs say so.
Both refusals and the pre-existing missing-section one are a single identity_validate now. --print-config had its own copy of the first check and none of the new ones, and would otherwise have printed this machine's credentials under an identity's name - the confusion the refusal exists to prevent, in the one command an operator runs to check what they configured.
A performance regression this nearly shipped with
identity_get runs on every config_get, through bin/dejima's wrapper. My first version resolved the identity before checking whether the key could belong to one - so every read walked every forge manifest to work out that the key was core.harness.
--print-config went from 2.4s to 41s. Every test still passed; the suite only got slower, which is what made me look.
Two things I tried first that did not work, and are worth recording because both looked right:
Caching _identity_key_specs. It is called through process substitution, so the assignment happens in a subshell and is lost. Measured worse, and I nearly believed that number - the earlier timings were taken while a full suite was running in the background and were meaningless. I killed it and re-measured on an idle machine.
Removing the $(...) around the resolution. Real, but a small part of it.
The fix is ordering: the cheap case guard runs first, the manifest lookup last, so the expensive path runs for core.ssh_key and the forge's own keys instead of all forty. 2.4s → 2.6s on --print-config, which reads every key and is the worst case; a launch reads fewer and then spends minutes building images.
A test pins the order by reading identity_get's body rather than by timing anything, since a timing test would be a flaky assertion about the machine it runs on. Moving the resolution back in front fails it.
The migration warning
Every launch of a config that sets [core] ssh_key says so and prints the section to move it into, with the key's path in it. That includes launches running as some other identity, where it is not the key in use - a config in that state is one [identity.default] away from the refusal above, and being told on the launch that stops working is worse than being told now. The warning names which account is actually in force so the reader is not left thinking this launch pushed with the deprecated key.
It prints on every launch rather than once. There is nowhere to record "once" that a session could not reach, and a deprecation nobody sees until removal is not a deprecation. It stops the moment the config is migrated, which is the other half of a warning worth having and has its own test.
The path comes from the file rather than through config_get: in this process config_get is bin/dejima's wrapper, so with a named identity in force it answers with that identity's key - and printing it as the thing to move would name a key the operator is not being asked to touch. That was a real bug in the first version, caught by asserting the path appears.
docs/configuration.md's headless-push walkthrough now writes the key to [identity.default] too. It was walking an operator through creating a deploy key and then showing them the deprecated spelling, which would have warned on every launch from the moment they finished following it.
Tests
Thirteen identity tests, of which nine are new, each mutation-checked: the default identity is selected, both spellings together are refused, [core] ssh_key alone still works and says what replaced it, a named identity still wins, a borrowed ssh_key is refused, a borrowed forge credential is refused, and an unused forge's credential is not.
The old test asserting fall-through is rewritten to assert the refusal, since that behaviour is what changed.
Rebased onto the prose-economy rewrite (#257). The docs here are written to its economy rather than restored from before it - the section it touches was retightened in that pass, and reinstating my longer version would have undone the point of it.
test/runtime.bats needs Docker and was not run locally. This is host-side config resolution; nothing about the container, the mounts or the firewall changes.
**Summary generated by LLM agent**
`[core] ssh_key` and an identity's `ssh_key` are the same thing under two names, and nothing said which one won. The resolution was: the identity's if it names one, otherwise `[core]`'s. So an identity that named no key pushed with the default account's key, silently - which is exactly the mixing identities exist to prevent.
The paragraph directly beneath that rule in `docs/configuration.md` argued against it: *"Pushing under the wrong key fails loudly; pushing under your default key when you asked for another one does not fail at all, which is the direction worth being careful about."* dejima refused a **typo'd** identity name and permitted a **partial** one, while its own stated reasoning condemned both.
Identities ship in this release and `[core] ssh_key` does not - it is already on `main`. So this is the one moment where the two spellings either converge or calcify.
## What changed
**`[identity.default]` is the identity in force when none is named.** It is where a host's ordinary account goes, and having a name is what lets it carry a git name and a forge credential beside the key, rather than those coming from three different places.
**`[core] ssh_key` is deprecated, not removed.** A config with no `[identity.default]` reads it exactly as before and says once at launch what replaced it. A config with both is refused rather than ranked - which key a session pushed with should not depend on precedence rules a reader has to know.
**Credentials no longer fall through.** An identity that names some and leaves the rest to the section beneath it is refused, naming each one. Only the selected forge's keys: refusing over a credential nothing on this launch reads would let an unrelated section block a launch.
**One gap, documented rather than left to be found.** "Set" means written in the config file. A forge key carrying a *schema default* - `[forge.gitea] token_file` - still answers for an identity that names none, and nothing refuses that, because nothing in the file says it. The docs say so.
Both refusals and the pre-existing missing-section one are a single `identity_validate` now. `--print-config` had its own copy of the first check and none of the new ones, and would otherwise have printed this machine's credentials under an identity's name - the confusion the refusal exists to prevent, in the one command an operator runs to check what they configured.
## A performance regression this nearly shipped with
`identity_get` runs on every `config_get`, through `bin/dejima`'s wrapper. My first version resolved the identity *before* checking whether the key could belong to one - so every read walked every forge manifest to work out that the key was `core.harness`.
`--print-config` went from **2.4s to 41s**. Every test still passed; the suite only got slower, which is what made me look.
Two things I tried first that did not work, and are worth recording because both looked right:
- Caching `_identity_key_specs`. It is called through process substitution, so the assignment happens in a subshell and is lost. Measured *worse*, and I nearly believed that number - the earlier timings were taken while a full suite was running in the background and were meaningless. I killed it and re-measured on an idle machine.
- Removing the `$(...)` around the resolution. Real, but a small part of it.
The fix is ordering: the cheap `case` guard runs first, the manifest lookup last, so the expensive path runs for `core.ssh_key` and the forge's own keys instead of all forty. **2.4s → 2.6s** on `--print-config`, which reads every key and is the worst case; a launch reads fewer and then spends minutes building images.
A test pins the order by reading `identity_get`'s body rather than by timing anything, since a timing test would be a flaky assertion about the machine it runs on. Moving the resolution back in front fails it.
## The migration warning
Every launch of a config that sets `[core] ssh_key` says so and prints the section to move it into, with the key's path in it. That includes launches running as some other identity, where it is not the key in use - a config in that state is one `[identity.default]` away from the refusal above, and being told on the launch that stops working is worse than being told now. The warning names which account is actually in force so the reader is not left thinking this launch pushed with the deprecated key.
It prints on every launch rather than once. There is nowhere to record "once" that a session could not reach, and a deprecation nobody sees until removal is not a deprecation. It stops the moment the config is migrated, which is the other half of a warning worth having and has its own test.
The path comes from the file rather than through `config_get`: in this process `config_get` is `bin/dejima`'s wrapper, so with a named identity in force it answers with *that* identity's key - and printing it as the thing to move would name a key the operator is not being asked to touch. That was a real bug in the first version, caught by asserting the path appears.
`docs/configuration.md`'s headless-push walkthrough now writes the key to `[identity.default]` too. It was walking an operator through creating a deploy key and then showing them the deprecated spelling, which would have warned on every launch from the moment they finished following it.
## Tests
Thirteen identity tests, of which nine are new, each mutation-checked: the default identity is selected, both spellings together are refused, `[core] ssh_key` alone still works and says what replaced it, a named identity still wins, a borrowed ssh_key is refused, a borrowed forge credential is refused, and an unused forge's credential is not.
The old test asserting fall-through is rewritten to assert the refusal, since that behaviour is what changed.
config 186, providers 94, static 400, project 145, remote 111, audit 164, proxy 32, conversation 12. shellcheck clean.
Rebased onto the prose-economy rewrite (#257). The docs here are written to its economy rather than restored from before it - the section it touches was retightened in that pass, and reinstating my longer version would have undone the point of it.
`test/runtime.bats` needs Docker and was not run locally. This is host-side config resolution; nothing about the container, the mounts or the firewall changes.
[core] ssh_key and an identity's ssh_key were the same thing under two names,
and nothing said which won. Resolution was: the identity's if it named one,
else [core]'s - so an identity that named no key silently pushed with the
default account's, which is the mixing identities exist to prevent. The
paragraph directly beneath that rule in the docs argued against it: pushing
under the default key when you asked for another one does not fail at all,
which is the direction worth being careful about.
Identities are new in this release and [core] ssh_key is not, so this is the
moment the two spellings either converge or calcify.
[identity.default] is now the identity in force when none is named. It is
where a host's ordinary account goes, and having a name is what lets it carry
a git name and a forge credential beside the key rather than those coming
from three places.
[core] ssh_key is deprecated, not removed. A config with no [identity.default]
reads it exactly as before and says once at launch what replaced it. A config
with both is refused rather than ranked: which key a session pushed with is
not a thing to work out from precedence rules.
Credentials no longer fall through either. An identity that names some and
leaves the rest to the section beneath is refused, naming each one - the same
failure as a missing section, reached through a partial one. Only the selected
forge's keys, since refusing over a credential nothing on this launch reads
would let an unrelated section block a launch.
One gap is documented rather than left to be found: a forge key with a schema
default still answers for an identity that names none, because nothing in the
file says so.
Both refusals and the missing-section one are now a single identity_validate,
which --print-config calls too. It had its own copy of the first check, and
would otherwise have printed this machine's credentials under an identity's
name - the confusion the refusal exists to prevent.
Performance: identity_get runs on every config_get through bin/dejima's
wrapper. Resolving the identity before checking whether the key could belong
to one turned a --print-config from 2.4s to 41s, walking every forge manifest
per key to discover the key was core.harness. The cheap guard runs first now,
the manifest lookup last, and a test pins that order rather than a timing.
2.4s -> 2.6s on the worst case, which reads every key.
config 184, providers 94, static 400, project 145, remote 111, audit 164,
proxy 32, conversation 12.
The note only fired when nothing else named an account, so a config with
[core] ssh_key and a named identity was told nothing - and that config is one
[identity.default] away from refusing to start. Being told on the launch that
breaks is worse than being told now.
It names the key's path, read from the file rather than through config_get:
in this process config_get is bin/dejima's wrapper, so with a named identity
in force it answers with THAT identity's key, and printing it as the thing to
move would name a key the operator is not being asked to touch.
Every launch rather than once, because there is nowhere to record "once" that
a session could not reach, and a deprecation nobody sees until removal is not
a deprecation. It stops the moment the config is migrated, which is the other
half of a warning worth having.
The section walks an operator through creating a deploy key and then shows
them writing it to the deprecated key, which would warn on every launch from
the moment they finished following it.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
[core] ssh_keyand an identity'sssh_keyare the same thing under two names, and nothing said which one won. The resolution was: the identity's if it names one, otherwise[core]'s. So an identity that named no key pushed with the default account's key, silently - which is exactly the mixing identities exist to prevent.The paragraph directly beneath that rule in
docs/configuration.mdargued against it: "Pushing under the wrong key fails loudly; pushing under your default key when you asked for another one does not fail at all, which is the direction worth being careful about." dejima refused a typo'd identity name and permitted a partial one, while its own stated reasoning condemned both.Identities ship in this release and
[core] ssh_keydoes not - it is already onmain. So this is the one moment where the two spellings either converge or calcify.What changed
[identity.default]is the identity in force when none is named. It is where a host's ordinary account goes, and having a name is what lets it carry a git name and a forge credential beside the key, rather than those coming from three different places.[core] ssh_keyis deprecated, not removed. A config with no[identity.default]reads it exactly as before and says once at launch what replaced it. A config with both is refused rather than ranked - which key a session pushed with should not depend on precedence rules a reader has to know.Credentials no longer fall through. An identity that names some and leaves the rest to the section beneath it is refused, naming each one. Only the selected forge's keys: refusing over a credential nothing on this launch reads would let an unrelated section block a launch.
One gap, documented rather than left to be found. "Set" means written in the config file. A forge key carrying a schema default -
[forge.gitea] token_file- still answers for an identity that names none, and nothing refuses that, because nothing in the file says it. The docs say so.Both refusals and the pre-existing missing-section one are a single
identity_validatenow.--print-confighad its own copy of the first check and none of the new ones, and would otherwise have printed this machine's credentials under an identity's name - the confusion the refusal exists to prevent, in the one command an operator runs to check what they configured.A performance regression this nearly shipped with
identity_getruns on everyconfig_get, throughbin/dejima's wrapper. My first version resolved the identity before checking whether the key could belong to one - so every read walked every forge manifest to work out that the key wascore.harness.--print-configwent from 2.4s to 41s. Every test still passed; the suite only got slower, which is what made me look.Two things I tried first that did not work, and are worth recording because both looked right:
_identity_key_specs. It is called through process substitution, so the assignment happens in a subshell and is lost. Measured worse, and I nearly believed that number - the earlier timings were taken while a full suite was running in the background and were meaningless. I killed it and re-measured on an idle machine.$(...)around the resolution. Real, but a small part of it.The fix is ordering: the cheap
caseguard runs first, the manifest lookup last, so the expensive path runs forcore.ssh_keyand the forge's own keys instead of all forty. 2.4s → 2.6s on--print-config, which reads every key and is the worst case; a launch reads fewer and then spends minutes building images.A test pins the order by reading
identity_get's body rather than by timing anything, since a timing test would be a flaky assertion about the machine it runs on. Moving the resolution back in front fails it.The migration warning
Every launch of a config that sets
[core] ssh_keysays so and prints the section to move it into, with the key's path in it. That includes launches running as some other identity, where it is not the key in use - a config in that state is one[identity.default]away from the refusal above, and being told on the launch that stops working is worse than being told now. The warning names which account is actually in force so the reader is not left thinking this launch pushed with the deprecated key.It prints on every launch rather than once. There is nowhere to record "once" that a session could not reach, and a deprecation nobody sees until removal is not a deprecation. It stops the moment the config is migrated, which is the other half of a warning worth having and has its own test.
The path comes from the file rather than through
config_get: in this processconfig_getisbin/dejima's wrapper, so with a named identity in force it answers with that identity's key - and printing it as the thing to move would name a key the operator is not being asked to touch. That was a real bug in the first version, caught by asserting the path appears.docs/configuration.md's headless-push walkthrough now writes the key to[identity.default]too. It was walking an operator through creating a deploy key and then showing them the deprecated spelling, which would have warned on every launch from the moment they finished following it.Tests
Thirteen identity tests, of which nine are new, each mutation-checked: the default identity is selected, both spellings together are refused,
[core] ssh_keyalone still works and says what replaced it, a named identity still wins, a borrowed ssh_key is refused, a borrowed forge credential is refused, and an unused forge's credential is not.The old test asserting fall-through is rewritten to assert the refusal, since that behaviour is what changed.
config 186, providers 94, static 400, project 145, remote 111, audit 164, proxy 32, conversation 12. shellcheck clean.
Rebased onto the prose-economy rewrite (#257). The docs here are written to its economy rather than restored from before it - the section it touches was retightened in that pass, and reinstating my longer version would have undone the point of it.
test/runtime.batsneeds Docker and was not run locally. This is host-side config resolution; nothing about the container, the mounts or the firewall changes.7dc534e6f1to4163ec12cd