Skip to content

fix: sanitize shell-derived env values before building auth headers#398

Merged
robinebers merged 2 commits into
robinebers:mainfrom
KYankee6:fix/zai-env-header-noise
Apr 28, 2026
Merged

fix: sanitize shell-derived env values before building auth headers#398
robinebers merged 2 commits into
robinebers:mainfrom
KYankee6:fix/zai-env-header-noise

Conversation

@KYankee6
Copy link
Copy Markdown
Contributor

@KYankee6 KYankee6 commented Apr 22, 2026

Description

Fixes the Z.ai failure mode where OpenUsage reads a valid API key from an interactive shell, but hidden ANSI/control bytes in shell startup output contaminate the resolved value and make the Authorization header invalid.

The fix hardens the shared host env resolver instead of patching only the Z.ai plugin:

  • add explicit start/end markers around interactive-shell env reads
  • sanitize ANSI/control sequences from resolved env values
  • add regression tests for noisy shell output and inline control bytes

Related Issue

Fixes #358

Type of Change

  • Bug fix
  • New feature
  • New provider plugin
  • Documentation
  • Performance improvement
  • Other (describe below)

Testing

  • I ran bun run build and it succeeded
  • I ran bun run test and all tests pass
  • I tested the change locally with bun tauri dev
  • I ran cargo test --manifest-path src-tauri/Cargo.toml
  • I ran cargo check --manifest-path src-tauri/Cargo.toml

Checklist


Summary by cubic

Sanitizes env values from interactive shells and direct env reads to prevent ANSI/control noise from corrupting API keys and breaking Authorization headers (notably for Z.ai). Hardens the shared host env resolver so all plugins benefit; also fixes empty-marker cases.

  • Bug Fixes
    • Use start/end markers for interactive shell reads; parse only the marked value. If markers exist but payload is empty, return None (no fallback). Keep sanitized fallback only when markers are missing.
    • Strip ANSI/control bytes and trim across shell and process env reads; added regression tests for noisy startup output, inline control bytes, and empty marked payload.

Written for commit 7cf7a6f. Summary will update on new commits.

@github-actions github-actions Bot added the rust Pull requests that update rust code label Apr 22, 2026
OpenUsage falls back to reading whitelisted env vars from an interactive
shell when the GUI process does not inherit them directly. Some zsh
setups emit ANSI/control sequences on stdout during startup, which can
stick to the resolved API key and make the Authorization header invalid.

This change wraps shell reads in explicit start/end markers, sanitizes
resolved env values, and adds regression tests for noisy shell output and
inline terminal control sequences.

Constraint: GUI launches may not inherit terminal-only env vars
Constraint: Interactive shell startup output can contain ANSI/control bytes
Rejected: Restrict fix to the Z.ai plugin | other providers share the same host env resolver
Rejected: Rely on shell config hygiene alone | the app still needs defensive parsing
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Treat shell stdout as untrusted whenever it is used as structured data
Tested: bun run build
Tested: bun run test
Tested: cargo test --manifest-path src-tauri/Cargo.toml
Tested: cargo check --manifest-path src-tauri/Cargo.toml
Not-tested: bun tauri dev manual UI verification
Related: robinebers#358
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src-tauri/src/plugin_engine/host_api.rs">

<violation number="1" location="src-tauri/src/plugin_engine/host_api.rs:84">
P1: Fallback parsing can misinterpret marker output for empty env vars and return `__OPENUSAGE_ENV_END__` as the value.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread src-tauri/src/plugin_engine/host_api.rs Outdated
Interactive-shell env reads use start/end markers so GUI launches can
recover terminal-only variables. An empty env var produced a complete
marker block with no payload, but the parser treated that the same as a
marker miss and fell back to whole-output parsing. That fallback could
select the end marker itself as the resolved value.

This change distinguishes complete marker blocks from true fallback
cases, returning None for empty marked output while preserving the
no-marker sanitization path.

Constraint: GUI env recovery still needs a no-marker fallback for noisy shell output
Constraint: Empty env vars must stay empty instead of becoming marker text
Rejected: Remove fallback parsing entirely | would regress shell-noise recovery for GUI launches
Rejected: Special-case only the exact end marker string | leaves the real state distinction ambiguous
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep marker-aware parsing separate from generic stdout sanitization so empty payloads stay empty
Tested: cargo test --manifest-path src-tauri/Cargo.toml parse_interactive_shell_env_output -- --nocapture
Tested: cargo test --manifest-path src-tauri/Cargo.toml extract_marked_value -- --nocapture
Tested: cargo check --manifest-path src-tauri/Cargo.toml
Not-tested: bun tauri dev manual UI verification
Related: robinebers#358
Related: robinebers#398
@validatedev validatedev requested a review from Copilot April 22, 2026 16:34
@validatedev
Copy link
Copy Markdown
Collaborator

@codex review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Hardens the host-side environment variable resolver to prevent interactive-shell output (ANSI/control noise) from contaminating API keys and producing invalid Authorization headers (notably impacting the Z.ai provider).

Changes:

  • Adds start/end markers around interactive-shell env reads and parses only the marked payload.
  • Introduces sanitization to strip ANSI/control characters and normalize env values for both process and shell-derived reads.
  • Adds regression tests covering noisy shell output, inline control bytes, and empty marked payload handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +42 to +46
let mut cleaned = if let Ok(ansi_re) = regex_lite::Regex::new(r"\x1B\[[0-?]*[ -/]*[@-~]") {
ansi_re.replace_all(text, "").to_string()
} else {
Some(trimmed.to_string())
text.to_string()
};
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The ANSI/escape stripping regex only removes CSI sequences (ESC [ ...), but other common terminal escape sequences (e.g., OSC: ESC ] ... BEL / ESC \, single-char ESC sequences like ESC ( B) can survive. Because retain then drops the ESC control byte but leaves the rest of the sequence’s printable payload, sanitize_env_value can still return a contaminated key (e.g., "]0;title" prefix). Consider expanding sanitization to remove OSC and other ESC sequences (or stripping any ESC-initiated sequence) before filtering control chars.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +43
fn sanitize_env_value(text: &str) -> Option<String> {
let mut cleaned = if let Ok(ansi_re) = regex_lite::Regex::new(r"\x1B\[[0-?]*[ -/]*[@-~]") {
ansi_re.replace_all(text, "").to_string()
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

sanitize_env_value recompiles the same regex on every call. Since env resolution can happen frequently (and is used by both process and interactive-shell reads), consider compiling the regex once (e.g., via OnceLock/Lazy) and reusing it to avoid repeated compilation overhead.

Copilot uses AI. Check for mistakes.
Comment on lines +183 to +186
let script = format!(
"printf '{}\\n'; printenv {}; printf '{}\\n'",
START_MARKER, name, END_MARKER
);
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The interactive-shell script interpolates name directly into the command string (printenv {}) without quoting/escaping. Today this is mitigated by the JS-side whitelist, but this helper is generic and could be reused from Rust with non-whitelisted input later. Consider adding a strict env-var-name validation (e.g., [A-Z0-9_]+) before building the script and/or quoting the argument in a shell-safe way (printenv -- ...) to prevent command injection.

Copilot uses AI. Check for mistakes.
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

ℹ️ 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".

@robinebers
Copy link
Copy Markdown
Owner

Great, thank you!

@robinebers robinebers merged commit 77ef460 into robinebers:main Apr 28, 2026
6 checks passed
barramee27 pushed a commit to barramee27/crossusage that referenced this pull request Apr 28, 2026
Interactive-shell env reads use start/end markers so GUI launches can
recover terminal-only variables. An empty env var produced a complete
marker block with no payload, but the parser treated that the same as a
marker miss and fell back to whole-output parsing. That fallback could
select the end marker itself as the resolved value.

This change distinguishes complete marker blocks from true fallback
cases, returning None for empty marked output while preserving the
no-marker sanitization path.

Constraint: GUI env recovery still needs a no-marker fallback for noisy shell output
Constraint: Empty env vars must stay empty instead of becoming marker text
Rejected: Remove fallback parsing entirely | would regress shell-noise recovery for GUI launches
Rejected: Special-case only the exact end marker string | leaves the real state distinction ambiguous
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep marker-aware parsing separate from generic stdout sanitization so empty payloads stay empty
Tested: cargo test --manifest-path src-tauri/Cargo.toml parse_interactive_shell_env_output -- --nocapture
Tested: cargo test --manifest-path src-tauri/Cargo.toml extract_marked_value -- --nocapture
Tested: cargo check --manifest-path src-tauri/Cargo.toml
Not-tested: bun tauri dev manual UI verification
Related: robinebers#358
Related: robinebers#398
(cherry picked from commit 7cf7a6f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Z.AI provider fails with “Usage request failed. Check your connection.” despite valid API key

4 participants