Update test workflow with shell module tests.
- Add dedicated shell module test step to .gitea/workflows/test.yml
- Tests run with --test-threads=1 for consistency
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Comprehensive wiki updates with sanitized content. Add new Shell-Execution
guide.
- Add Shell-Execution.md guide (665 lines, sanitized)
- Update AI-Providers.md with Ollama function calling
- Update Architecture.md with shell execution system
- Update IPC-Commands.md with shell commands
- Update Database.md with new tables
- Update CICD-Pipeline.md for Gitea Actions
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The test was trying to spawn a process which requires a Tokio runtime.
Changed the test to only verify validation logic by checking that safe
environment variables don't trigger 'Dangerous environment variable' errors.
Uses /usr/bin/nonexistent as command so spawn will fail (command not found)
but validation will pass for safe env vars like DEBUG, API_KEY, PATH, etc.
All 243 tests now passing.
Add defense-in-depth security validation for stdio transport to reject
environment variables that could be used for privilege escalation attacks.
Blocks the following dangerous variables (case-insensitive):
- LD_PRELOAD (Linux)
- LD_LIBRARY_PATH (Linux)
- DYLD_INSERT_LIBRARIES (macOS)
- DYLD_LIBRARY_PATH (macOS)
- DYLD_FRAMEWORK_PATH (macOS)
- DYLD_FALLBACK_LIBRARY_PATH (macOS)
These variables can inject malicious libraries into spawned processes and
should never be user-configurable for MCP servers.
Add comprehensive tests:
- test_rejects_relative_path: Verify existing path validation
- test_rejects_dangerous_env_vars: Test all blocked variables
- test_rejects_dangerous_env_vars_case_insensitive: Verify lowercase variants blocked
- test_allows_safe_env_vars: Verify legitimate vars (DEBUG, PATH, API_KEY) allowed
All tests passing.
Change plaintext_env input field from type='password' to type='text' since
this field is explicitly for non-sensitive values (DEBUG, LOG_LEVEL, etc.).
Using password type for plaintext config was misleading and prevented
copy/paste of legitimate non-sensitive configuration.
Only the encrypted_env and http_headers fields remain as type='password'
for sensitive values like API keys and tokens.
Add clearer placeholder and helper text to explain that encrypted environment
variables are never displayed for security reasons. When editing an existing
server, the encrypted_env field shows a placeholder explaining that leaving it
blank will preserve existing values.
Also apply cargo fmt formatting fixes to store.rs.
Add dual-mode environment variable support for stdio MCP servers and custom
HTTP headers for HTTP-based MCP servers to enable proper authentication and
configuration.
Backend changes (Rust):
- Add migration 023 for env_config column in mcp_servers table
- Add env_config field to McpServer, CreateMcpServerRequest, UpdateMcpServerRequest
- Encrypt env_config using AES-256-GCM on create/update in store.rs
- Add get_server_env_config() helper to decrypt and parse env vars
- Parse plaintext env from transport_config.env (stdio only)
- Parse custom headers from transport_config.headers (HTTP only)
- Merge plaintext and encrypted env vars (encrypted takes precedence)
- Update connect_stdio() to accept HashMap<String, String> for env vars
- Update connect_http() to accept HashMap<String, String> for headers
- Apply env vars to tokio::process::Command via .env() method
- Add warning for HTTP headers (rmcp v1.7.0 limitation - no .header() method)
- Add comprehensive tests for encryption, merging, and clearing
Frontend changes (TypeScript/React):
- Add env_config field to CreateMcpServerRequest and UpdateMcpServerRequest
- Add plaintext_env, encrypted_env, http_headers to ServerForm interface
- Add parsing helpers: parseEnvVars(), formatEnvVars(), parseHeaders(), formatHeaders()
- Update startEdit() to extract and format env vars/headers from transport_config
- Update handleSave() to build transport_config with env/headers and env_config JSON
- Add conditional UI fields: stdio (plaintext + encrypted env), HTTP (custom headers)
- Use password input type for all sensitive fields
Security:
- Encrypted env vars stored using AES-256-GCM (matching auth_value pattern)
- Plaintext env vars in transport_config for non-sensitive values
- UI masks all env/header fields with password input type
- Never display decrypted values when editing
Fixes inability to configure MCP servers that require environment variables
(e.g., GitHub MCP server with GITHUB_PERSONAL_ACCESS_TOKEN).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Parse plaintext env from transport_config.env (stdio)
- Decrypt encrypted env from env_config column (stdio)
- Merge env vars with encrypted taking precedence
- Parse custom headers from transport_config.headers (HTTP)
- Update connect_stdio() to accept HashMap<String, String> for env
- Update connect_http() to accept HashMap<String, String> for headers
- Apply env vars to tokio::process::Command in stdio transport
- Log warning for HTTP custom headers (rmcp v1.7.0 limitation)
All 19 MCP tests passing.
- Add env_config field to McpServer, CreateMcpServerRequest, UpdateMcpServerRequest
- Encrypt env_config using encrypt_token() on create/update
- Decrypt env_config in get_server_env_config() helper function
- Handle clearing env_config with empty string
- Add comprehensive tests:
- test_env_config_encrypted_at_rest()
- test_update_env_config()
- test_clear_env_config_with_empty_string()
- test_env_config_none_preserves_existing()
All tests passing. Follows same encryption pattern as auth_value.
- Add test_023_mcp_env_config_column() to verify env_config column exists
- Add test_023_idempotent() to ensure migration runs only once
- Following TDD methodology: test written first, then implementation
Two credential patterns were missing from the PiiDetector, confirmed
by live audit log showing was_pii_redacted: false with plaintext creds:
1. Abbreviated key form (pass: abc123!!): the pattern only matched
password|passwd|pwd. Added pass, passphrase, secret with a word
boundary to prevent substring false positives (bypass:, compass:).
2. Natural language form (Is the password password123 good): added a
second Password sub-pattern for keyword-adjacent values without a
key separator. Value must contain a digit or special char to avoid
flagging plain words (password strength, password policy).
5 new regression tests added. 233/233 Rust tests pass.
Add was_pii_redacted and pii_types_redacted to the ai_chat audit log
entry. Both are tracked through the full_message build block (typed
message + attachments) so any redaction that occurs is always
reflected in the compliance record.
Fix response.user_message + suffix potentially yielding 'undefined...'
when user_message is absent. Now unconditionally calls
updateMessageContent with (response.user_message ?? message) + suffix,
so the bubble always shows a valid string regardless of backend build.
Update TICKET-pii-bypass-chat-attachments.md to reflect the final
auto-redact design (not block/warn) so automated review comparisons
against the ticket stop flagging design decisions as defects.
Remove frontend detectPiiCmd pre-scan loop — backend is sole redaction
authority; bubble update via response.user_message covers user feedback.
Detect PII on full file content before truncating. Previous order
(truncate to 8000 bytes then scan) could miss PII straddling the
boundary. Now: read full content, scan, redact, then truncate to
EMBED_LIMIT (8000 bytes) at a valid UTF-8 char boundary.
logFileIds IPC: pass undefined (not null) for empty array so Tauri
serialises it correctly to Rust Option::None.
Add MAX_TEXT_SCAN_BYTES (32 KB) guard in scan_text_for_pii to prevent
unbounded regex evaluation on oversized payloads.
Fix clippy uninlined_format_args in ai.rs.
Addresses three findings from the third automated review:
[BLOCKER] No frontend PII pre-check on attachments.
Added detectPiiCmd call for each logFileId before chatMessageCmd.
PII is not blocked (per explicit product decision: auto-redact and
send) but the user now sees a non-blocking amber notice listing
each file and the PII types that will be auto-redacted. Backend
remains the authoritative redaction layer.
[WARNING 2] Chat bubble showed original PII-laden message even though
only the redacted form was sent to AI.
Added updateMessageContent to sessionStore. After chatMessageCmd
returns, if response.user_message is set the user bubble is updated
to reflect what was actually stored in the DB, so the UI is
consistent with the audit log.
CI fix: cargo fmt changes to analysis.rs were not staged in the prior
commit. Committed here — fmt check now passes cleanly.
Resolves all three findings from the second automated review and
fixes the cargo fmt --check CI failure (formatting drift in analysis.rs
from a prior merge).
[BLOCKER 1 + BLOCKER 2 + WARNING]
Frontend no longer performs any PII scanning or redaction. All three
concerns stemmed from the same root cause: outMessage was derived
on the frontend and used for display, DB storage (via lastUserMsgRef
and the chat bubble), and the AI payload — causing the original message
to be silently replaced before the backend received it.
Fix: frontend sends the original message verbatim. Backend is now the
sole authority. chat_message auto-redacts the typed message text using
PiiDetector + apply_redactions() before building the full payload, logs
the PII types via tracing::warn, and stores only the redacted form in
ai_messages and the audit log. The redacted form is returned to the
caller as ChatResponse.user_message (Option<String>, absent from direct
provider calls).
Frontend uses message (original) for the chat bubble and
lastUserMsgRef — resolution steps show natural language, not
[Password] tokens. The AI and DB see only the redacted version.
CI fix: cargo fmt applied to analysis.rs; all format checks now pass.
Resolves all four findings from the automated review:
[BLOCKER 1] Attachment PII scan error path left pendingFiles intact,
allowing retry with stale file references. Fix: file content is no
longer held in frontend state at all — PendingFile drops the content
field entirely. logFileIds are captured before setPendingFiles([]) and
passed directly to the backend.
[BLOCKER 2] Raw file content stored in PendingFile.content created a
UI-visible PII surface and a data-residency risk. Fix: frontend never
reads or stores file content. The backend loads file data from disk,
auto-redacts PII in-memory using pii::apply_redactions(), and embeds
the clean text into the AI message. No PII ever touches the frontend.
[WARNING 1] String-based attachment header parsing was fragile and
bypassable. Fix: parsing is gone — backend identifies attachments by
log_file_id, reads them directly from the DB/disk path, and applies
redaction at that level.
[WARNING 2] Error message disclosed PII type list to the caller. Fix:
PII types are logged via tracing::warn only; no type details in the
user-facing error or API response.
Additionally: typed chat messages are now auto-redacted rather than
blocked. scanTextForPiiCmd runs on the typed text; detected spans are
replaced in reverse-offset order before the message is sent to the AI
and stored in the DB. The user sees the redacted form in their chat
bubble.
Architecture:
- chat_message now accepts log_file_ids: Option<Vec<String>>
- Backend reads file → detects PII → redacts in memory → embeds
- Frontend: no readTextFile, no content field, no frontend PII gate
File attachments were embedded into AI messages without any PII
scanning, allowing credentials, tokens, and other sensitive data
to be forwarded to AI providers in plaintext.
Typed chat messages had the same gap: a user could type a password
or API key directly and it would be sent unscanned.
Changes:
- chat_message (Rust): defence-in-depth scan of all attachment body
content (between --- Attached: markers); hard rejects if PII found
- detect_pii (Rust): fix return type from pii::PiiDetectionResult
(spans/original_text) to db::models::PiiDetectionResult
(detections/total_pii_found) to match the TypeScript contract; the
LogUpload PII review workflow was receiving undefined for detections
- scan_text_for_pii (Rust): new command — scans arbitrary text for PII
without creating DB records; used for typed message warnings
- Triage/index.tsx: PendingFile now carries logFileId; handleSend gates
each text attachment through detectPiiCmd (hard block on PII found);
typed message text scanned via scanTextForPiiCmd with a one-time
warning — second send of same message proceeds as acknowledgment
- compress_text now returns Result<Vec<u8>, String>; callers propagate
the error instead of silently storing empty BLOB on gzip failure
- upload_image_attachment_by_content and upload_paste_image now validate
decoded byte length against MAX_IMAGE_FILE_BYTES before DB storage,
closing the size-bypass gap that existed for base64 content uploads
- Image View modal in AttachmentsTab now surfaces the error string when
get_image_attachment_data fails, replacing the opaque "could not be
loaded" message with actionable diagnostic text
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Store compressed log content and raw image bytes in SQLite so attachments
are self-contained regardless of source file availability on disk.
DB (migrations 020-022):
- log_files.content_compressed BLOB — gzip-compressed extracted text
- image_attachments.image_data BLOB — raw image bytes
- Views v_log_files_with_issue and v_image_attachments_with_issue for
cross-incident queries with joined issue title
Rust backend:
- compress_text / decompress_text helpers (flate2 rust_backend / miniz_oxide)
with 100 MB decompression-bomb guard
- upload_log_file*, upload_log_file_by_content store content_compressed
- upload_image_attachment*, upload_paste_image store image_data
- New commands: get_log_file_content, list_all_log_files (analysis.rs)
- New commands: get_image_attachment_data, list_all_image_attachments (image.rs)
- All commands fall back to file_path for pre-migration records
Frontend:
- LogFileSummary, ImageAttachmentSummary types in tauriCommands.ts
- attachmentStore (Zustand) — loadAttachments, searchAttachments
- History page: Issues tab (existing) + Attachments tab (new)
with log/image tables, search bar, View modals, lazy thumbnails
Tests: 227 Rust (+16 new), 103 frontend (+9 new), tsc clean, clippy clean
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Three issues addressed together:
1. Race condition (was PR #56): changelog job now CREATES the Gitea
release rather than assuming build jobs have already created it.
Build jobs continue to use create-or-skip + upload unchanged.
2. Detached HEAD push: 'git push origin master' fails when HEAD is
detached (no local branch named master). Changed to 'HEAD:master'.
3. git-cliff tag guard: verify tag is present locally before running
git-cliff, to fail fast with a clear message rather than silently
generating a wrong changelog.
4. git commit idiom: replaced 'git commit || echo' (swallows all
non-zero exit codes including real failures) with an explicit
'git diff --staged --quiet' guard so set -euo pipefail is not
undermined.
The changelog job checks out a specific SHA (detached HEAD) then
commits CHANGELOG.md and tries to push with 'git push origin master'.
Since there is no local branch named 'master', git rejects the push
with 'src refspec master does not match any'.
Fix: use 'git push origin HEAD:master' which explicitly maps the
current detached HEAD to the remote master branch regardless of
local branch state.
Addresses the review warning: git rev-parse confirms the tag is
present in the local repo after git fetch --tags before git-cliff
or git tag --sort= runs against it. Fails fast with a clear error
if the tag is missing rather than silently generating an incomplete
changelog.
The changelog and build-* jobs all fan out from autotag in parallel.
Build jobs create the Gitea release with 'curl ... || true', but the
changelog job was trying to GET the release before any build job had
run, reliably failing with 'Could not find release for tag vX.Y.Z'.
Fix: changelog job owns release creation. It creates the release with
the git-cliff body if it does not exist, or patches the body if a
prior run already created it. Build jobs continue using their existing
create-or-skip + upload pattern unchanged.