Skip to content

Redaction scope and server tags: lookup failures never weaken masking or rules and show a notice #843

Open

Description

@smart-ssh-planner

Goal

When the app cannot fully determine which secret values to mask or which server tags apply, it never masks less or decides more leniently than before, and the user sees a notice in the session instead of a silent gap.

Context

Redaction scope (extension secrets for server, groups and tags, Spec 0038 §2a):

  • scope_for_id (crates/app-logic/src/session_extension.rs:84-100): get_server error → None; known_secrets(ext, None) (:124-132) then returns no extension secrets at all, without a log line. Caller: crates/app-shell/src/commands/connect.rs:1633-1641 (extension_scope).
  • server_scope (:60-79): group_chain error → warn, scope without groups → group secrets are not masked for the session.
  • rebuild_redactors_for_sessions (:190-219): get_server error → group_chain = Vec::new(), then swap_redactor_with_literals replaces the literal set, so group secrets that were masked before drop out of the redactor.
  • redactor_with_secrets (:106-121): Regex::new(&regex::escape(s)).ok() silently drops a literal that fails to compile (in practice only the regex size limit for very long values).
  • read_sudo_password_for_redaction (crates/app-logic/src/server_redaction.rs:65-83): backend error → warn only, the session does not mask the stored sudo password.
    Server tags for rule evaluation:
  • tags_for_session (crates/app-logic/src/orchestration/action_exec.rs:891-897) and the re-evaluation of an edited command in handle_user_decision (:1623-1627): any get_server error → the tags frozen at connect time are used. Rule evaluation then uses possibly outdated tags without any signal.
  • The local pseudo-server has no stored row (connect.rs:1630-1632), so ServerNotFound is a normal case there; security invariant 17 forbids special-casing it in security logic, but distinguishing "not stored" (ProfileError::ServerNotFound) from a store failure is not a special case.
  • Pattern for a one-time session notice: announce_second_opinion_setup_failure (action_exec.rs:2436-2470, pending flag on Session, emit_chat_error with a code).

Requirements

  • Scope lookups distinguish "server not stored" from "store failed". On a store failure (get_server or group_chain):
    • scope_for_id returns a scope with at least server_id (empty groups/tags) instead of None, so server-level extension secrets stay masked;
    • the session is marked "redaction incomplete".
  • rebuild_redactors_for_sessions: if the scope cannot be fully determined, it extends the existing literal set of each affected session with the newly known secrets instead of replacing it, logs a warning (server id, error kind, no content) and marks the sessions "redaction incomplete".
  • redactor_with_secrets: a literal that cannot be compiled is logged (count and byte length only, never the value) and marks the session "redaction incomplete". Recommended: retry with a larger RegexBuilder::size_limit before giving up.
  • read_sudo_password_for_redaction backend error: also marks the session "redaction incomplete" (log line unchanged).
  • "Redaction incomplete" shows one chat notice per session (pending flag, like announce_second_opinion_setup_failure), code REDACTION_INCOMPLETE, text in the backend's English style, e.g. "Some secret values for this server could not be loaded; they may not be masked in what is sent to the AI." Add the code to apps/smart-ssh-community/frontend/src/errorCodes.ts and all locales/*/common.json (notice codes such as AI_SECOND_OPINION_SETUP_FAILED are mapped there).
  • Server tags: on a store failure other than ServerNotFound, handle_action_proposed still evaluates with the frozen tags, but an AutoExec result escalates to Confirm with code FILTER_SERVER_TAGS_UNAVAILABLE (a new fact/step in escalate(), Escalation chain: one pure escalate() function shared by the decision path and the rule-suggestion verdict #826). In handle_user_decision a store failure logs a warning; the re-evaluation keeps the frozen tags (only Deny matters there). ServerNotFound keeps today's behaviour. Add the code to errorCodes.ts and all locales.
  • Comments in new code in English.

Acceptance Criteria

  • Test: get_server fails with a backend error at session start → a server-level extension secret is still masked in the session's redactor, and the session emits exactly one REDACTION_INCOMPLETE notice (on the first chat turn or action).
  • Test: group_chain fails → REDACTION_INCOMPLETE notice; server-level secrets masked.
  • Test: rebuild with get_server failing → a group secret that was masked before the rebuild is still masked after it (counter-check: with a healthy store the rebuild still replaces the set, existing test test_rebuild_replaces_literals_but_reconnect_swap_keeps_them passes).
  • Test: a secret literal longer than the default regex size limit is either masked (after the retry) or produces the notice; no value appears in the log output (captured with a tracing test subscriber).
  • Test: profile store backend error during tags_for_session with a matching allow rule → Confirm with FILTER_SERVER_TAGS_UNAVAILABLE, never AutoExec; with ServerNotFound → unchanged decision (counter-check).
  • Locale parity and error-code checks pass.
  • Relevant automated tests pass.

Technical Notes

  • Security-critical area (redaction, filter inputs, escalation chain): read .claude/skills/smart-ssh-coder/references/security-invariants.md (invariants 2, 11, 17) and THREAT-MODEL.md §2. Review priority elevated.
  • The SessionExtension trait signature (known_secrets(&self, scope) -> Vec<SecretString>) stays unchanged; extensions are not affected.
  • Builds on the pure escalate() function (Escalation chain: one pure escalate() function shared by the decision path and the rule-suggestion verdict #826) for the new tag step; add the step after the existing ones so codes of earlier steps win.
  • Logging follows ADR 0086: ids and error kinds, never contents.

Spec impact: update 0038 (§2a: behaviour when the scope cannot be determined)

Related Work

Activity

  1. added this to the 0.8.0 milestone on Oct 26, 2026
  2. 18 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Fields

    Priority

    P1 - High

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions