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:
cliffhall 2026-08-22 14:15:35 -04:00
commit decc673a5d
2 changed files with 458 additions and 26 deletions

View file

@ -112,4 +112,26 @@ describe("SecretStorageFooter", () => {
renderWithMantine(<SecretStorageFooter info={undefined} />);
expect(screen.queryByTestId("secret-storage-footer")).toBeNull();
});
it("says the permissions could not be checked rather than claiming owner-only", async () => {
// The third permission state. Falling into the verified-0600 branch here
// would have the band assert owner-only having read nothing — the same
// bug as claiming 0600 in the caveat, one layer up.
renderWithMantine(
<SecretStorageFooter
info={{
kind: "file",
reason: "fallback",
durable: true,
plaintext: true,
path: "/home/node/.mcp-inspector/secrets.json",
permissionsUnknown: "EACCES",
}}
/>,
);
const band = screen.getByTestId("secret-storage-footer");
expect(band).toHaveTextContent("Permissions could not be checked.");
expect(band).not.toHaveTextContent("Owner-only");
expect(band).toHaveAttribute("data-tone", "warn");
});
});

View file

@ -127,6 +127,28 @@ const formatMode = (mode: number): string => mode.toString(8).padStart(4, "0");
* and the underlying facts still come from the shared helpers, which is
* what keeps them from disagreeing about anything that matters.
*/
/**
* How the band describes the file's permissions.
*
* Three states, not two, and the third is the point: "Owner-only
* permissions." is a claim of *fact*, so it may only be made when the mode
* was actually read and found to be 0600. `looseMode` says we looked and it
* was wrong; `permissionsUnknown` says we could not look at all. Treating
* the second like the first — which a plain `looseMode === undefined` test
* does — makes the footer assert owner-only having verified nothing, which
* is the failure this band exists to prevent, arrived at by omission rather
* than by error.
*/
function filePermissionsSentence(info: SecretStorageInfo): string {
if (info.looseMode !== undefined) {
return `Mode ${formatMode(info.looseMode)} — not owner-only.`;
}
if (info.permissionsUnknown !== undefined) {
return "Permissions could not be checked.";
}
return "Owner-only permissions.";
}
function footerMessage(info: SecretStorageInfo): string {
if (info.kind === "keyring") return secretStorageLabel(info);
if (info.kind === "memory") {
@ -143,10 +165,7 @@ function footerMessage(info: SecretStorageInfo): string {
// is something else and could not be tightened, and saying "owner-only"
// there would be the footer asserting the opposite of what is true — the
// one failure this whole band exists to prevent.
const permissions =
info.looseMode === undefined
? "Owner-only permissions."
: `Mode ${formatMode(info.looseMode)} — not owner-only.`;
const permissions = filePermissionsSentence(info);
return `${encryption} file. ${permissions}`;
}
@ -173,6 +192,10 @@ function footerTooltip(info: SecretStorageInfo): string | undefined {
parts.push(
`Mode ${formatMode(info.looseMode)} could not be tightened to 0600 — anyone who can read the file can read the secrets in it.`,
);
} else if (info.permissionsUnknown !== undefined) {
parts.push(
`The file's permissions could not be read (${info.permissionsUnknown}), so it is not known whether others can read it.`,
);
}
parts.push("Click to copy file path");
return parts.join(" ");