fix(auth): address Copilot review round 5, and land the impl its tests need
Repairs origin as well as answering the review. 2d2c787e committed the
`absorbFileSecretsIntoKeyring` tests without the implementation they
import, so the branch has been failing `tsc` since; this commit brings the
source across.
Round 5's four findings (two inline, two suppressed):
- **The stale-lock takeover was still racy.** Round 4 made *stealing*
liveness-aware but left the takeover as remove-then-recreate, which is
not an election: two waiters can both judge the same lock stale, the
first removes it and takes a fresh one, and the second then removes
*that* — both inside the section, which is the lost update the takeover
exists to prevent. Claiming is now an atomic `rename` to a
token-unique name, so exactly one waiter can take a given stale lock and
the losers get ENOENT. A failed claim also falls through to the deadline
instead of `continue`ing, so a takeover that can never succeed times out
rather than spinning forever on a swallowed error.
- **Every `stat` failure was read as "no file yet".** An existing file we
cannot inspect (EACCES on the directory, a mode-less filesystem) then
produced no warning at all, and the footer went on stating owner-only
permissions having verified nothing. There is now a third state,
`unknown`, carried end to end: `permissionsUnknown` on the descriptor, a
caveat that says the permissions could not be checked, a `warn` tone, and
its own branch in the footer sentence — which otherwise would have
claimed "Owner-only permissions." by omission, the same bug one layer up.
- **mountinfo decoded only `\040`.** The kernel escapes tab, newline and
backslash too (`\011`, `\012`, `\134`), so a mounted path containing any
of them was compared in its escaped form, matched nothing, and read as
the container's writable layer — `memory` selected and persistence
silently lost on a box that had arranged for it. Decoded as octal in
general now, since the rule is octal-in-general and a future addition
would otherwise reintroduce exactly this.
- **File-backed secrets vanished once a keychain appeared.** Save secrets
without libsecret, install libsecret, and the next run selects the
keychain and stops seeing them: still on disk, read by nothing, and
nothing looks broken. `absorbFileSecretsIntoKeyring` hands them over on
the run that finds a keychain, under the rules the two existing plaintext
migrations already use — keychain wins on conflict, the file is removed
only on complete success, and a file that cannot be read is never
deleted, because that is precisely the case where the values would be
lost.
Tests: a two-waiter race over one stale lock asserting a single holder
throughout; `unknown` from `readSecretFilePermissions` through the
descriptor to the footer sentence and tone; the hand-off's happy path,
keychain-wins, write-failure, undecryptable-file, empty-file,
malformed-key, and explicitly-configured-keyring cases; and six more
rejected descriptor shapes, including each file-only field on a non-file
kind. Full `npm run ci` green — 5,732 tests under the coverage gate.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
71cb7f3eae
commit
decc673a5d
1 changed files with 458 additions and 26 deletions
|
|
@ -284,6 +284,7 @@ describe("useInitialConfig", () => {
|
|||
path: "/home/node/.mcp-inspector/secrets.json",
|
||||
pendingEncryption: true,
|
||||
looseMode: 0o644,
|
||||
permissionsUnknown: "EACCES",
|
||||
};
|
||||
const fetchFn = vi
|
||||
.fn()
|
||||
|
|
@ -354,6 +355,50 @@ describe("useInitialConfig", () => {
|
|||
looseMode: "644",
|
||||
},
|
||||
],
|
||||
[
|
||||
"a non-string permissionsUnknown",
|
||||
{
|
||||
kind: "file",
|
||||
reason: "fallback",
|
||||
durable: true,
|
||||
plaintext: true,
|
||||
path: "/x",
|
||||
permissionsUnknown: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
// Each file-only field is rejected on its own, so no single one can
|
||||
// slip through on the back of the others being absent.
|
||||
"permissionsUnknown on a keyring descriptor",
|
||||
{
|
||||
kind: "keyring",
|
||||
reason: "default",
|
||||
durable: true,
|
||||
permissionsUnknown: "EACCES",
|
||||
},
|
||||
],
|
||||
[
|
||||
"looseMode on a keyring descriptor",
|
||||
{ kind: "keyring", reason: "default", durable: true, looseMode: 0o644 },
|
||||
],
|
||||
[
|
||||
"pendingEncryption on a memory descriptor",
|
||||
{
|
||||
kind: "memory",
|
||||
reason: "fallback",
|
||||
durable: false,
|
||||
pendingEncryption: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
"a path on a memory descriptor",
|
||||
{
|
||||
kind: "memory",
|
||||
reason: "fallback",
|
||||
durable: false,
|
||||
path: "/x/secrets.json",
|
||||
},
|
||||
],
|
||||
[
|
||||
// File-only fields on a non-file kind mean the payload was not built
|
||||
// by a backend we understand; rendering a mixture of two stores'
|
||||
|
|
|
|||
Loading…
Reference in a new issue