Skip to content

fix(brain): redact plaintext credentials from provider tool context - #573

Merged
vladimirrott merged 5 commits into
lacs-project:mainfrom
QinXi-ai:fix/552-redact-tool-output
Oct 12, 2026
Merged

vladimirrott merged 5 commits into
lacs-project:mainfrom
QinXi-ai:fix/552-redact-tool-output

Conversation

@QinXi-ai

@QinXi-ai QinXi-ai commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Read tools could forward journal passwords and bearer tokens to an LLM provider and retain them for later turns. Redact plaintext credential assignments, Bearer values, recognizable provider-key shapes, and private-key blocks before normalization and truncation; screen normalized content and retained provider context too.

An unterminated quoted assignment now stops at the line boundary, preserving subsequent accepted-login and sudo records. Empty assignment, command-line option, and Bearer values also preserve later log lines. Require the SendGrid SG.<22>.<43>, Hugging Face, and npm token formats, retain generic key: file paths and ordinary environment/cache values, and redact spaced credential options such as mysqldump --password hunter2 mydb without losing neighboring arguments.

Related Issue

Closes #552.

Validation

  • Tests added or updated: six new Rust tests relative to main. The three review regressions reproduced later-log deletion, host-fact deletion, and spaced-password leakage against the unchanged redactor before the fix.
  • Documentation updated if behavior changed: SECURITY.md describes the screen and its limits; measured baseline and three prose references regenerated after rebase onto main.
  • Security impact considered: recording-provider regression checks read-tool success/error results and retained context, preserving innocuous facts while preventing recognized plaintext credentials from reaching the provider.
  • Trust boundary preserved (daemon remains the only privileged executor).
  • CI passes on this PR head: Rust, docs/hygiene, frontend, PostgreSQL, scripts lint, security audit, dependency review, secret scanning, CodeQL analyses, and container image. Container-smoke was skipped by the workflow.

Ubuntu validation passed: cargo fmt --all -- --check; UPDATE_TEST_BASELINE=1 bash scripts/test_baseline.sh (1,949 passed, six configured skips); cargo clippy --workspace --all-features --all-targets --locked -- -D warnings; cargo doc --workspace --all-features --no-deps --locked; evidence/public-claim and secret screens; changed-document markdownlint. The generated baseline and all three published counts are synchronized. Three isolated review regression tests passed after the original redactor failed all three.

Full scripts/ci-local.sh --no-postgres completed as FAIL/INCOMPLETE: its sole failing check was action-pin verification because remote gh is missing. Rust (including docs with RUSTDOCFLAGS=-D warnings), frontend (72 tests), every release/E2E regression invoked by the board, configured Markdown lint, and YAML lint passed. The privileged action-steps assertions, markdown-link-check, ShellCheck, cargo-audit, and live PostgreSQL contract were skipped due to the local environment or --no-postgres. GitHub CI on the updated head passed all executed checks, including the PostgreSQL contract and privileged scripts checks.

Notes for Reviewers

No dependency or prompt changes. This is a plaintext pattern screen: unlabelled passwords, encoded credentials, and unknown formats can still reach a provider. It is independent of intent admission in #543. Rebased onto main to resolve the count conflicts; tests cover normalization, quoted values, truncation, private-key blocks, provider shapes, benign facts, and subsequent provider context.

Assisted by OpenAI Codex; validation figures come from actual runs.

@vladimirrott vladimirrott left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed at 9447e552. One blocking item: the redactor removes host facts that are not secrets, in one case every line after the match.

Screening before and after normalisation is the right order, since a zero-width joiner can hide a key from the first pass, and pushing the cap after redaction means truncation can never split a PEM block into an unredacted tail. The integration test that records what the provider receives on turns 2 and 3 is the test #552 asked for.

I ran the sanitize_tool_output you ship over a few host facts, from a small example binary inside a --network=none container. I did not build this branch on my machine. Output, envelope included:

[query_package_repos] deb http://sg.archive.ubuntu.com/ubuntu jammy main
  -> deb http://<redacted> jammy main
[query_environment]   HF_HOME=/root/.cache/huggingface
  -> <redacted>
[query_logs]          Oct 07 sshd[812]: Unable to load host key: /etc/ssh/ssh_host_rsa_key
  -> Oct 07 sshd[812]: Unable to load host key: <redacted>
[query_logs]          Oct 07 app: password="
                      Oct 07 sshd[900]: Accepted publickey for root from 203.0.113.9
                      Oct 07 sudo: root : COMMAND=/bin/sh
  -> Oct 07 app: password="<redacted>
[query_processes]     mysqldump --password hunter2 mydb
  -> mysqldump --password hunter2 mydb

Blocking: an unterminated quote erases the rest of the output. value_range returns (start, raw.len()) when the closing quote never comes, so one log line with password=" and no closing quote takes every line after it, here an accepted root login and a sudo command. Any local process can write such a line to the journal, and from then on the planner reads nothing that follows it. Stopping an unquoted or unterminated value at the end of the line would keep the safety you wanted (no exposed tail) without that reach.

Fixing that one unblocks the PR. The other rows are worth a look while you're in there:

  • sg. on its own counts as a SendGrid prefix, so the Singapore Ubuntu mirror disappears from query_package_repos. SendGrid keys are SG.<22>.<43>; requiring that shape fixes it. hf_ and npm_ have the same looseness, which is how HF_HOME=... vanishes whole, key name included.
  • key: followed by a path counts as an assignment, which costs the sshd line its file name.
  • --password hunter2 (space, no =) passes untouched, and #552 names query_processes as a source.

Your count is consistent: three new tests, 1936 → 1939 in the evidence file and the three docs. #587 also edits crates/sysknife-brain/tests/planner.rs and moves the count, so whichever lands second needs a rebase and a regenerated baseline. That part is mechanical.

@QinXi-ai
QinXi-ai force-pushed the fix/552-redact-tool-output branch from 9447e55 to b49e394 Compare October 9, 2026 02:30
vladimirrott
vladimirrott previously approved these changes Oct 9, 2026

@vladimirrott vladimirrott left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving at b49e3940.

You fixed every row from my table, and each fix is narrower than the bug: value_range stops a quoted value at \r or \n, SG., hf_ and npm_ now need their real lengths, the path exception applies to the bare key label and no other, and --password hunter2 gets caught by the -- check. tool_output_unterminated_quotes_preserve_following_log_lines runs both quote kinds against both line endings, plus the empty and escaped-backslash values, which covers more than the one line I sent you.

My merge gate ran that test in a container, then again with the newline stop removed (if matches!(ch, '\r' | '\n') { replaced by if false {):

running 'tool_output_unterminated_quotes_preserve_following_log_lines' unmutated
rust suite: evidence of 1 executed unit(s), passing unmutated
applying the mutation and re-running
receipt recorded for #573 at b49e3940 (observed: clean pass, mutated fail)

Your count holds after the rebase: six new tests (five #[test], one #[tokio::test]) and 1943 → 1949 in the evidence file and all three docs. I did not build this branch on my machine.

Nothing blocks on the code. #571 and #587 also move the test count from 1943, so this one will need a rebase and a regenerated baseline once whichever of them lands first. That part is mechanical and not yours to apologise for. When you push it, I'll merge without a new round of review provided four things hold: the push changes no file under crates/**/src, apps/*/src, packaging/ or .github/; this same mutation still turns the test red on the new head; the board is fully green; and my approval is still on the PR.

…utput

* origin/main:
  fix(audit): chain SQLite status transitions and expose verdicts (lacs-project#571)
  fix(security): cover setup provider credential formats (lacs-project#578)

# Conflicts:
#	README.md
#	docs/distro-support.md
#	docs/introduction.md
#	tests/evidence/workspace-tests.json
@QinXi-ai

Copy link
Copy Markdown
Contributor Author

Merged current main to clear the conflicts. The only conflicting lines were the published Rust test count (README, docs/introduction.md, docs/distro-support.md, tests/evidence/workspace-tests.json). Per CONTRIBUTING ("whoever merges carries it"), I dropped this PR's count bump and left those four files at main's value, so the open PRs no longer conflict with each other on the figure. This PR adds 6 Rust tests, so rust may be red on the baseline alone until the figure is regenerated at merge. No other changes since approval. @vladimirrott could you re-approve when convenient?

vladimirrott and others added 2 commits October 12, 2026 09:37
The merge with main kept main's 1,952, which left out the six tests this
branch adds, so test_baseline saw 1,958 and failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UpyrhbDTW1gBwzGAC44GYU

@vladimirrott vladimirrott left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving again at 2b7d081. Since my review at b49e394 the branch only picked up main and the test count. The merge with main kept main's 1,952, so I pushed 1,958 (1,943 at your base, plus 9 from #571 and #578, plus your 6). Your unterminated-quote test still fails with the newline check removed, same as before.

@vladimirrott
vladimirrott merged commit 7c2d26a into lacs-project:main Oct 12, 2026
13 checks passed
vladimirrott added a commit to QinXi-ai/sysknife that referenced this pull request Oct 12, 2026
vladimirrott added a commit that referenced this pull request Oct 12, 2026
* fix(brain): narrow intent credential admission

* fix(brain): refuse flagged and punctuated credential values

* test: count #587's four tests on top of main (1,962)

Main now carries 1,958 after #573, and this branch adds four tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UpyrhbDTW1gBwzGAC44GYU

---------

Co-authored-by: hushen <[email protected]>
Co-authored-by: Vladimir Rotariu <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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.

Tool output reaches the LLM provider with no secret screen, though admission says credentials must not

2 participants