Skip to content

Session save failures now carry an errno and a photographable read-out - #186

Merged
GLinnik21 merged 2 commits into
mainfrom
fix/storage-diagnosis
Sep 21, 2026
Merged

GLinnik21 merged 2 commits into
mainfrom
fix/storage-diagnosis

Conversation

@GLinnik21

Copy link
Copy Markdown
Owner

A save failure told a reporter with no shell exactly nothing: write_atomic returned a bare bool
and discarded every errno, so a total fallback failure logged only
could not persist to ANY candidate path — indistinguishable from a server-side authorization
problem, and useless for deciding whether the jail refused the write or the path was never there.

write_atomic and create_private_temp now return a typed WriteFailure carrying the OS errno
where there is one, and save_legacy_fallback_locked logs one line per failed candidate: the path,
the parent directory's uid/gid/mode, and the errno or refusal class. It iterates whatever
paths::session_candidates() returns rather than assuming a fixed count, so it keeps telling the
truth as that list changes. This is the difference between EACCES and EROFS being visible at all —
on the reporting set they mean two entirely different things, one a permission bit and one a
read-only mount.

The failure read-out's existing Details card also names the persistence class, key-manager stage
and service error code. Those three were already defined on telemetry::incident::IncidentContext
and already sent to Sentry; they were simply never shown to the person looking at the screen. Both
projections now read the same typed evidence through one storage_evidence_line helper, and an
absent field renders as a fixed unknown rather than vanishing, so the line's shape can never
signal more than it knows. No new visual primitive, no devtrigger, no debug-flavour gate — it sits
behind the affordance that already ships.

PRIVACY.md and the in-app privacy policy named every sign-in-report field event_body could emit
except those three, which have been shipping silently. Their prose is corrected to match what the
serializer has always sent. That is a notice correction, not a widening: no consent scope changes
and POLICY_VERSION does not move.

Known gap, stated rather than papered over: details_offered() returns false while a
persistence_warning is up, which is precisely the reporter's case — so the on-screen half does
not reach the failure that prompted this. The logged per-candidate evidence does. Lifting that
suppression is a separate change.

🤖 Generated with Claude Code

write_atomic returned a bare bool and threw away every errno; a total fallback
failure logged only "could not persist to ANY candidate path", indistinguishable
from a server-side auth problem for a reporter with no shell. write_atomic and
create_private_temp now return a typed WriteFailure carrying the OS errno where
there is one, and save_legacy_fallback_locked logs per candidate: path, parent
directory uid/gid/mode, and the errno or refusal class — iterating whatever
paths::session_candidates() returns rather than assuming a fixed count.

The failure read-out's existing Details card (screens::login::support_line) now
also names the persistence class, key-manager stage and service error code
already defined on telemetry::incident::IncidentContext but previously only
sent to Sentry, never shown. Both projections read the same typed evidence
through one new storage_evidence_line helper; an absent field renders as a
fixed "unknown" class rather than disappearing, so the line's shape can never
signal more than it knows. Reachable on every build, behind the affordance
that already exists today, with no devtriggers or debug-flavour gate.

PRIVACY.md and the in-app Privacy Policy named every sign-in-report field
event_body could emit except these three, already shipping silently; their
prose is corrected to match what the serializer has sent all along. No
consent-scope change, no POLICY_VERSION bump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 21, 2026 01:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-21T02:22:09.024717Z 00aad70 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…the earlier refusals too

save_legacy_fallback_locked accumulated a CandidateDiagnostic per refused candidate but only
logged it on the total-failure paths; both success arms returned early and threw the evidence
away. Since 4f1c787 added in_runtime_dir("auth.json") as a strictly-last candidate, the common
case on a jail like /media/developer is exactly this shape — durable candidates refuse, the
runtime-dir one succeeds — so the errno/path/parent-mode evidence that explains WHY the durable
ones refused was being discarded on the one path that shape actually takes.

Both success arms now log the accumulated failures, with wording that says the save succeeded
via a later candidate rather than reusing the total-failure "refused" phrasing. Silent when
failures is empty, exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@GLinnik21

Copy link
Copy Markdown
Owner Author

Codex review, one round, on the combined result (this branch sits on 4f1c787). One blocking finding, now fixed in 00aad70.

Both success paths discarded the accumulated diagnostics. save_legacy_fallback_locked only logged failures on the total-failure paths; the sealed and plaintext loops each returned early on the first candidate that accepted the write. With 4f1c787 on main that is no longer a rare shape — it is the common one: the durable candidates are refused and the runtime-directory candidate then succeeds, so the evidence explaining why /media/developer refused would have been thrown away in exactly the case this branch exists to explain. The diagnostics would only ever have surfaced when nothing worked at all.

Both paths now log the refusals before returning, through the same log_candidate_diagnostics formatter, with wording that says the save succeeded on a later candidate so the line cannot be misread as a failure. Silent when nothing was refused.

No finding on the other four: nothing secret reaches the log or the screen (paths, numeric ownership, errno — no token, server identity or username); the three screen fields were already serialized by event_body, so the PRIVACY.md wording is a correction and not a widening; every write_atomic call site preserves its previous boolean decision; and storage_evidence_line keeps three labelled slots for all eight presence combinations, including no context at all.

One factual note for the record, not a change here: resolve_runtime_dir returns /tmp itself for the stable app id, so a stable install's fallback is /tmp/auth.json in the shared root, while flavoured installs get /tmp/<app id>/auth.json. That is safe — 0600, sticky directory, owner check and O_NOFOLLOW — but the name is guessable in a shared namespace, so another app holding auth.json first would cost a stable install this fallback. Worth a follow-up alongside lifting the Details suppression.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 00aad70e30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

LinkClass::Unknown => offer.key.kind.code().to_string(),
link => format!("{}.{}", offer.key.kind.code(), link.code()),
};
let storage = crate::telemetry::incident::storage_evidence_line(offer.context.as_ref());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate storage evidence for real save failures

For an actual persistence failure, this call always receives a context whose storage fields are unset: repository-wide, with_persistence is only called by tests, remains #[allow(dead_code)], and SessionMachine::apply_persistence_completion creates only a PersistenceWarning while discarding the failed outcome. Consequently production support lines always append persistence:unknown keymgr:unknown svc:unknown, and the newly documented report fields are never emitted. Wire the completion outcome into a retained SaveFailed incident before projecting it here.

AGENTS.md reference: AGENTS.md:L5-L8

Useful? React with 👍 / 👎.

LinkClass::Unknown => offer.key.kind.code().to_string(),
link => format!("{}.{}", offer.key.kind.code(), link.code()),
};
let storage = crate::telemetry::incident::storage_evidence_line(offer.context.as_ref());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose the storage line during persistence warnings

When a fresh session save actually fails, the owner sets persistence_warning, but details_offered() unconditionally returns false while that warning exists. Since this added storage evidence is rendered only inside the Details card, the locked-out user in the motivating no-shell scenario sees only the Continue-unsaved control and cannot photograph any of these values; make the warning surface provide access to the diagnostic read-out.

AGENTS.md reference: AGENTS.md:L5-L8

Useful? React with 👍 / 👎.

@GLinnik21
GLinnik21 merged commit 7b3256c into main Sep 21, 2026
5 checks passed
@GLinnik21
GLinnik21 deleted the fix/storage-diagnosis branch September 21, 2026 02:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants