Skip to content

Pass CONSOLE_REASON through the query alias, and never send a blank CONSOLE_USER - #3263

Merged
jeremy merged 6 commits into
mainfrom
console-reason
Nov 4, 2026
Merged

jeremy merged 6 commits into
mainfrom
console-reason

Conversation

@jeremy

@jeremy jeremy commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

A console audit asks that sessions reading customer data carry the link that authorized them: the card, issue or Help Scout conversation. rails query can't prompt for a reason, so console1984 records every agent query as rails query (via Claude Code). basecamp/console1984#169 appends CONSOLE_REASON to that reason. This passes it through the query alias:

CONSOLE_REASON=https://app.basecamp.com/…/cards/3 bin/kamal query "'Account.count'" -d production

It also fixes a second finding from the same audit: query sessions recorded under user unknown.

CONSOLE_REASON through five parsers

On its way to docker exec --env, the value crosses five layers:

  1. ERB, into a single-quoted YAML scalar
  2. Shellwords.split, in Kamal::Cli::Alias::Command
  3. Thor's -e hash, which splits each pair at its first colon, so a URL arrives whole
  4. String#dump, in Kamal's escape_shell_value
  5. the remote shell, which expands any ${…}: Kamal escapes $( but leaves ${…} in deliberately

Escaping for each layer in turn kept missing one. A Unicode line separator ends the YAML scalar and swallows the command. A C locale splits multibyte characters. dump adds a backslash before #@ and #{. And ${…} runs on the console host. So the header percent-encodes every byte outside a URL's own characters (A-Za-z0-9, space, ._~:/?#[]!&()*+,;=%-). A link arrives unchanged, and anything else arrives as inert ASCII: $ becomes %24, é becomes %C3%A9, a newline becomes %0A. When the variable is unset or blank, Kamal sends --env CONSOLE_REASON with no value, the same way it already handles an empty CODEX_THREAD_ID, and the container doesn't get the variable.

CONSOLE_USER when USER is blank

In a sandbox, cron, launchd or env -i, USER can be unset. The aliases rendered a bare CONSOLE_USER:. In the unquoted console, bash and ssh aliases, that turns into KEY: "…", a YAML mapping, and the whole file fails to parse (mapping values are not allowed in this context). Every kamal command fails, not just the console ones. The header now takes the first of USER, LOGNAME and the passwd name that is a plain login name (\A[\w.@-]+\z, matched on bytes), else uid-<n>. A name with a space, an apostrophe or invalid bytes is skipped, not interpolated.

So a blank USER can't be what recorded the unknown sessions through this alias. It breaks kamal before anything runs. Those sessions most likely came from a bin/rails query run without -e CONSOLE_USER, for example a hand-built bin/kamal app exec that doesn't go through the alias. console1984#169 proposes refusing a blank username, as an option rather than a default.

Verified, without touching a host

A local harness renders this deploy.yml the way Kamal 2.12 does: ERB with trim_mode: "-", then YAML. It expands query the way Kamal::Cli::Alias::Command does and runs Kamal's own Thor CLI, with App#exec stubbed. It then passes each --env argument Kamal would send through sh -c, standing in for the remote shell, and compares CONSOLE_USER and CONSOLE_REASON with literal expected values. 46 of 46 pass for each of bc3, haystack and Fizzy: 23 cases, each in the default locale and under LC_ALL=C.

Case Arrives as
Basecamp card URL with ?, &, #__recording_…; Help Scout URL unchanged
HS 12345: refund request, -p --hosts evil, : unchanged, as one value, not as options
it's "quoted" \ back it%27s %22quoted%22 %5C back
${X:=a[$(id)]}${!X} ${HOME} $HOME and a backtick every $, brace and backtick encoded; nothing expands
#@ticket #{x} #$y #%40ticket #%7Bx%7D #%24y, with no backslash from dump
newline, tab, U+2028/U+2029, U+FFFF, non-ASCII, invalid UTF-8 percent-encoded bytes
unset or empty --env CONSOLE_REASON with no value, as today
USER unset, empty, whitespace, a b, \xFF, é; LOGNAME="Jane O'Doe" the next plain login name

To check that the harness can fail, I mutated the header three ways: removing .b, letting $ through, and matching names on characters. Each mutation fails 1 to 4 cases.

Landed dependencies

Fizzy doesn't record query sessions yet

Gemfile.saas.lock pins console1984 at 0.2.2 (23419ec). Console1984::QueryAuditor arrived in 0.2.3, so Fizzy's rails query sessions aren't recorded in console1984_sessions at all, whatever the reason or user. The bump this PR waits on brings in the auditor along with the reason. It also brings the other changes since 0.2.2: the Prism parser on Ruby 3.3+, and the insert_all/upsert_all fix to the auditable-tables shield.

…ONSOLE_USER

A console audit asks that sessions reading customer data carry the link that
authorized them. rails query can't prompt for one, so the query alias now
passes CONSOLE_REASON, which console1984 appends to the session's reason
(basecamp/console1984#169; recorded once the gem is bumped, ignored before).

It's free text bound for a single-quoted YAML scalar and then Kamal's
Shellwords.split, so it's flattened to one line and escaped for both. Thor
splits each -e pair at its first colon, so a URL arrives whole.

A blank USER (a sandbox, cron, launchd, env -i) left a bare "CONSOLE_USER:"
in the console and shell aliases, which breaks this file's YAML for every
kamal command. Fall back to LOGNAME, then the login name.
Kamal leaves a ${...} in an env value for the remote shell to expand, so a
reason carrying one would run on the console host. Encode every $ as %24,
which a URL reads the same.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Merging requires maintainer confirmation that the external bin/failed-jobs parser update lands first and coordination of the console1984 upgrade needed to audit query sessions and their reasons.

No open issues 🎉

What changed in this PR

Updates SaaS Kamal aliases to forward query authorization context and provide a nonblank console username when USER is missing. Recording query reasons still depends on the pending console1984 update.

Changes:

  • Normalizes and escapes CONSOLE_REASON for the query alias.
  • Adds username fallbacks through LOGNAME, the passwd entry, and finally uid-<n> across console, query, and SSH aliases.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
saas/​config/​deploy.yml Prepares query reasons for forwarding and adds console username fallbacks.

Review effort: Balanced

LOGNAME and the passwd name are interpolated into every alias, plain YAML
scalars and the single-quoted query alias alike. A name with a space or an
apostrophe would split into arguments or break this file's YAML, so take
the first of USER, LOGNAME and the passwd name made only of word
characters, dots, @ and hyphens.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Audit recording and alias compatibility depend on pending console1984 and external coworker updates, requiring maintainer confirmation of compatibility and landing order.

No open issues 🎉

Review effort: Balanced

Escaping free text for each layer it crosses (YAML, Shellwords, Thor,
String#dump, the remote shell) kept missing one: Unicode line separators
end the YAML scalar, a C locale splits multibyte characters, and dump
backslashes #@ and #{. Percent-encode every byte outside a URL's own
characters instead. A link arrives unchanged, and anything else arrives
as inert ASCII. Match login names on bytes, so an invalid USER falls
through rather than raising.
@jeremy

jeremy commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

Review threads: none.
Open for a decision: landing order and the console1984 bump. Copilot's 48e336a and 450d245 reviews ask for maintainer confirmation of both. The order is coworker#307, then basecamp/console1984#169, then this PR with the bump, which also brings QueryAuditor to Fizzy for the first time.
Not reported on the head: Copilot, at f65dba2. It last reviewed 450d245 and was already re-requested once.
CI: GitHub checks green on f65dba2.

basecamp/audits1984#108: the session header and the sensitive access card
render the reason and justification escaped, with URLs still linked.
@jeremy

jeremy commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

Since the last summary: audits1984 moves from 4271a8e to f99fc64 in Gemfile.saas.lock (basecamp/audits1984#108). Only that revision line changes.
Review threads: none.
Not reported on the head: Copilot, at 0e3eb3e. It last reviewed 450d245 and was already re-requested once.
CI: GitHub checks green on 0e3eb3e.

0.2.5 records CONSOLE_REASON in rails query sessions (basecamp/console1984#169)
and carries everything since this app's previous pin, including Prism parsing
on Ruby 3.3+ (#144).
@jeremy
jeremy marked this pull request as ready for review November 4, 2026 03:05
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 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 ⚠️ Failed 2026-11-04T05:45:53.599484Z
We couldn’t complete this review. Comment @codex review to try again.
4784d4a Manual request
ℹ️ 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.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jeremy

jeremy commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jeremy

jeremy commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

Since the last summary: console1984 is pinned to the GitHub tag v0.2.5 in Gemfile.saas (it was 0.2.2). This brings QueryAuditor, so Fizzy now records rails query sessions. The PR is ready for review.
Review threads: none.
Not reported on the head: Copilot, at 4784d4a, which last reviewed 450d245 and was already re-requested once. Codex, at 4784d4a: usage limit, including after one @codex review.
CI: GitHub checks green on 4784d4a.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jeremy
jeremy merged commit 42584c7 into main Nov 4, 2026
13 checks passed
@jeremy
jeremy deleted the console-reason branch November 4, 2026 07:28
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