Skip to content

feat(sessions): optional AES-256-GCM encryption for saved sessions#8

Closed
JessicaMulein wants to merge 13 commits into
mainfrom
feat/session-encryption
Closed

feat(sessions): optional AES-256-GCM encryption for saved sessions#8
JessicaMulein wants to merge 13 commits into
mainfrom
feat/session-encryption

Conversation

@JessicaMulein
Copy link
Copy Markdown
Member

@JessicaMulein JessicaMulein commented May 27, 2026

Summary

  • Adds cecli/session_crypto.py (AES-256-GCM, magic header CECLI_ENCRYPTED_SESSION_v1)
  • --session-encrypt / --no-session-encrypt and --session-key-file on the CLI
  • SessionManager encrypts on save and decrypts on load/list when enabled; plaintext JSON unchanged when off
  • Key from CECLI_SESSION_KEY (urlsafe base64, 32 bytes) or key file
  • cryptography>=42 in requirements
  • 26 tests in tests/basic/test_session_{crypto,args,sessions_manager}.py

BrightVision

Parent repo PR will pin this commit and add Vision API + Tauri keychain wiring.

Test plan

  • python -m pytest tests/basic/test_session_crypto.py tests/basic/test_session_args.py tests/basic/test_sessions_manager.py -q

Made with Cursor

PR Summary by Typo

Overview

This PR introduces optional AES-256-GCM encryption for saved sessions, enhancing data security for sensitive information stored on disk. It allows users to encrypt session files using a provided key, with backward compatibility for unencrypted sessions.

Key Changes

  • Added --session-encrypt and --session-key-file command-line arguments to enable and configure session encryption.
  • Introduced a new module cecli/session_crypto.py containing AES-256-GCM encryption and decryption logic.
  • Modified cecli/sessions.py to handle encrypted and unencrypted session files during saving, loading, and listing.
  • Updated documentation to guide users on enabling and configuring session encryption.
  • Added cryptography as a new dependency for encryption functionality.
  • Included new tests for CLI arguments, session crypto functions, and the session manager's encryption capabilities.

Work Breakdown

Category Lines Changed
New Work 595 (98.2%)
Churn 3 (0.5%)
Rework 8 (1.3%)
Total Changes 606
To turn off PR summary, please visit Notification settings.

Summary by CodeRabbit

Release Notes

  • New Features

    • Optional AES-256-GCM encryption for saved session files to protect sensitive data
    • New --session-encrypt CLI flag to enable session encryption
    • New --session-key-file CLI option to specify a file containing the encryption key
  • Documentation

    • Added comprehensive guides for enabling and configuring session file encryption

Review Change Stack

JessicaMulein and others added 13 commits May 25, 2026 16:50
Port Vision integration (session, http_api, git_workspace, todos) from
aider_vision_core with async bridge to cecli coders. Adds bright-vision-core-serve
entrypoint, fastapi/uvicorn deps, and basic HTTP/workspace pytest.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add --session-encrypt with CECLI_SESSION_KEY or --session-key-file,
wire encrypt/decrypt through SessionManager save/load/list, and document
usage. Plaintext JSON remains the default when encryption is off.

Co-authored-by: Cursor <cursoragent@cursor.com>
@typo-app
Copy link
Copy Markdown

typo-app Bot commented May 27, 2026

Static Code Review 📊

🛑 10 quality checks failed!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

This PR adds optional AES-256-GCM encryption for cecli session files, enabling encrypted on-disk persistence. A new session_crypto module handles encryption primitives, SessionManager integrates encryption transparently into save/load/list operations, CLI arguments expose encryption configuration, and comprehensive tests validate the feature across unit and integration scenarios.

Changes

Session Encryption Support

Layer / File(s) Summary
Session Crypto Module
cecli/session_crypto.py
New module with AES-256-GCM encryption/decryption, magic-prefix payload detection, key resolution from environment variables or files, and SessionCryptoError for error handling.
SessionManager Encryption Integration
cecli/sessions.py
SessionManager refactored with helper methods to detect encryption settings, read raw session bytes with optional decryption, and write sessions as encrypted or plaintext JSON. Updates save_session, list_sessions, and load_session to handle both encrypted and legacy plaintext sessions.
CLI Arguments for Encryption
cecli/args.py
Adds --session-encrypt (boolean) and --session-key-file (file path with shell completion) arguments to configure session encryption behavior.
Dependency and User Documentation
requirements/requirements.in, cecli/website/docs/usage/sessions.md
Adds cryptography>=42.0.0 dependency and documents session encryption with CECLI_SESSION_KEY environment variable setup and CLI flag usage.
Test Fixtures for Session Keys
tests/basic/conftest.py
Provides session_key32, session_key_b64, and session_key_env pytest fixtures for test isolation and key management.
Session Crypto Unit Tests
tests/basic/test_session_crypto.py
Tests encryption/decryption roundtrips, plaintext JSON acceptance, key resolution from environment and files, error handling, and cryptographic dependency failures.
CLI Argument Parsing Tests
tests/basic/test_session_args.py
Validates default values and correct parsing of session encryption flags and key file arguments.
SessionManager Integration Tests
tests/basic/test_sessions_manager.py
Tests plaintext and encrypted session persistence, key availability handling, backward compatibility with legacy plaintext sessions, and encrypted session loading with optional decryption.

🐰 A session now sleeps secure,
With keys of base64 pure,
Encrypted at rest, AES-blessed,
No plaintext to obscure!

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(sessions): optional AES-256-GCM encryption for saved sessions' directly and clearly summarizes the main change—adding optional AES-256-GCM encryption for session files. It is specific, concise, and accurately reflects the primary objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/session-encryption

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add optional AES-256-GCM encryption for saved sessions

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Adds optional AES-256-GCM encryption for session files on disk
• Introduces --session-encrypt and --session-key-file CLI arguments
• Implements session_crypto module with encrypt/decrypt utilities
• Integrates encryption into SessionManager save/load/list operations
• Maintains backward compatibility with plaintext JSON sessions
• Adds 26 comprehensive tests for crypto, args, and session management
Diagram
flowchart LR
  CLI["CLI Arguments<br/>--session-encrypt<br/>--session-key-file"]
  KEY["Key Resolution<br/>CECLI_SESSION_KEY<br/>or key file"]
  CRYPTO["session_crypto module<br/>AES-256-GCM<br/>MAGIC header"]
  MANAGER["SessionManager<br/>save/load/list"]
  DISK["Session Files<br/>Encrypted or Plaintext"]
  
  CLI --> KEY
  KEY --> CRYPTO
  CRYPTO --> MANAGER
  MANAGER --> DISK

Loading

Grey Divider

File Changes

1. cecli/args.py ✨ Enhancement +18/-0

Add session encryption CLI arguments

cecli/args.py


2. cecli/session_crypto.py ✨ Enhancement +108/-0

New AES-256-GCM encryption module for sessions

cecli/session_crypto.py


3. cecli/sessions.py ✨ Enhancement +91/-11

Integrate encryption into SessionManager operations

cecli/sessions.py


View more (6)
4. tests/basic/conftest.py 🧪 Tests +24/-0

Shared test fixtures for session encryption

tests/basic/conftest.py


5. tests/basic/test_session_args.py 🧪 Tests +31/-0

Test CLI argument parsing for encryption

tests/basic/test_session_args.py


6. tests/basic/test_session_crypto.py 🧪 Tests +100/-0

Comprehensive tests for crypto operations

tests/basic/test_session_crypto.py


7. tests/basic/test_sessions_manager.py 🧪 Tests +219/-0

Test SessionManager encryption integration

tests/basic/test_sessions_manager.py


8. cecli/website/docs/usage/sessions.md 📝 Documentation +11/-0

Document optional session encryption feature

cecli/website/docs/usage/sessions.md


9. requirements/requirements.in Dependencies +1/-0

Add cryptography dependency for encryption

requirements/requirements.in


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 27, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0)

Grey Divider


Action required

1. UnicodeDecodeError not handled 🐞 Bug ☼ Reliability
Description
SessionManager._read_session_file decodes bytes as UTF-8 but does not catch UnicodeDecodeError, so a
non-UTF8/corrupted plaintext session file can crash session loading instead of producing a
user-facing error and returning False.
Code

cecli/sessions.py[R51-61]

Evidence
The plaintext load path uses data.decode('utf-8') without handling UnicodeDecodeError, and
neither load_session() nor /load-session provides a protective catch for exceptions raised while
awaiting async command execution.

cecli/sessions.py[33-61]
cecli/session_crypto.py[69-79]
cecli/sessions.py[174-193]
cecli/commands/load_session.py[12-24]
cecli/commands/utils/base_command.py[74-90]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SessionManager._read_session_file()` decodes plaintext session bytes via `data.decode("utf-8")` and only catches `json.JSONDecodeError` and `SessionCryptoError`. If the file contains invalid UTF-8, `UnicodeDecodeError` will propagate and can terminate `/load-session`.

## Issue Context
- This affects plaintext session files (or corrupted files) and results in an unhandled exception instead of a clean `tool_error(...)` + `return None` path.
- `session_crypto.decrypt_session_bytes()` has the same plaintext decode pattern and also misses `UnicodeDecodeError`.

## Fix Focus Areas
- cecli/sessions.py[33-62]
- cecli/session_crypto.py[69-80]

## Suggested fix
1. In `SessionManager._read_session_file()`, add an `except UnicodeDecodeError as e:` handler (ideally alongside JSON errors) that emits a clear message (eg "Invalid session file encoding") and returns `None`.
2. Also update `session_crypto.decrypt_session_bytes()` plaintext branch to catch `UnicodeDecodeError` and raise `SessionCryptoError("Invalid session file encoding.")` (defensive for other call sites).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Encrypted list requires flag 🐞 Bug ≡ Correctness
Description
list_sessions() only decrypts encrypted session files when args.session_encrypt is True, so
encrypted sessions will appear as placeholders even when a valid key is configured but
--session-encrypt is not set (inconsistent with load behavior).
Code

cecli/sessions.py[R129-146]

Evidence
list_sessions() derives the key via _session_encrypt_settings(), which is disabled unless
session_encrypt is set, while _read_session_file() resolves a key without that gating, creating
inconsistent behavior between list and load.

cecli/sessions.py[26-31]
cecli/sessions.py[129-146]
cecli/sessions.py[40-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SessionManager.list_sessions()` uses `_session_encrypt_settings()` to decide whether it can decrypt encrypted files; `_session_encrypt_settings()` returns `(False, None)` unless `args.session_encrypt` is enabled. As a result, listing encrypted sessions won’t show real metadata unless the user passes `--session-encrypt`, even if `CECLI_SESSION_KEY` / `--session-key-file` is configured.

## Issue Context
`_read_session_file()` resolves a key and decrypts encrypted payloads regardless of `args.session_encrypt`, so load and list behave differently.

## Fix Focus Areas
- cecli/sessions.py[26-32]
- cecli/sessions.py[117-146]
- cecli/sessions.py[33-51]

## Suggested fix
- In `list_sessions()`, for encrypted payloads resolve the key the same way as `_read_session_file()` (eg `session_crypto.resolve_key(key_file=args.session_key_file)`), without requiring `args.session_encrypt`.
- Keep the placeholder behavior only when no key is available (or decryption fails), but don’t gate key lookup on the save/encrypt flag.
- Optionally refactor into a shared helper like `_resolve_decrypt_key()` used by both `_read_session_file()` and `list_sessions()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copy link
Copy Markdown

@typo-app typo-app Bot left a comment

Choose a reason for hiding this comment

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

AI Code Review 🤖

Files Reviewed: 9
Comments Added: 0
Lines of Code Analyzed: 614
Critical Issues: 0

PR Health: Needs Attention

Give 👍 or 👎 on each review comment to help us improve.

Comment thread cecli/sessions.py
Comment on lines +51 to +61
parsed = json.loads(data.decode("utf-8"))
if not isinstance(parsed, dict):
self.io.tool_error("Invalid session format.")
return None
return parsed
except session_crypto.SessionCryptoError as e:
self.io.tool_error(str(e))
return None
except json.JSONDecodeError as e:
self.io.tool_error(f"Error loading session: {e}")
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Unicodedecodeerror not handled 🐞 Bug ☼ Reliability

SessionManager._read_session_file decodes bytes as UTF-8 but does not catch UnicodeDecodeError, so a
non-UTF8/corrupted plaintext session file can crash session loading instead of producing a
user-facing error and returning False.
Agent Prompt
## Issue description
`SessionManager._read_session_file()` decodes plaintext session bytes via `data.decode("utf-8")` and only catches `json.JSONDecodeError` and `SessionCryptoError`. If the file contains invalid UTF-8, `UnicodeDecodeError` will propagate and can terminate `/load-session`.

## Issue Context
- This affects plaintext session files (or corrupted files) and results in an unhandled exception instead of a clean `tool_error(...)` + `return None` path.
- `session_crypto.decrypt_session_bytes()` has the same plaintext decode pattern and also misses `UnicodeDecodeError`.

## Fix Focus Areas
- cecli/sessions.py[33-62]
- cecli/session_crypto.py[69-80]

## Suggested fix
1. In `SessionManager._read_session_file()`, add an `except UnicodeDecodeError as e:` handler (ideally alongside JSON errors) that emits a clear message (eg "Invalid session file encoding") and returns `None`.
2. Also update `session_crypto.decrypt_session_bytes()` plaintext branch to catch `UnicodeDecodeError` and raise `SessionCryptoError("Invalid session file encoding.")` (defensive for other call sites).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
cecli/session_crypto.py (1)

95-96: 💤 Low value

Minimum payload length check could be more precise.

GCM produces a 16-byte authentication tag appended to the ciphertext. For an empty plaintext, the minimum blob size would be 12 (nonce) + 16 (tag) = 28 bytes. The current check (< 13) allows through payloads that will definitely fail decryption due to a truncated tag.

The decryption will still fail gracefully (caught at line 100-101), but the error message "payload is too short" would be more accurate with a threshold of 28.

Suggested fix
-    if len(blob) < 13:
+    if len(blob) < 28:  # 12-byte nonce + 16-byte GCM tag minimum
         raise SessionCryptoError("Encrypted session payload is too short.")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cecli/session_crypto.py` around lines 95 - 96, The minimum-length check for
the encrypted payload is too low: update the condition that raises
SessionCryptoError when validating blob (the variable named blob in
session_crypto.py) so it requires at least 28 bytes (12-byte nonce + 16-byte GCM
tag) instead of 13; adjust the check and error message raised by
SessionCryptoError accordingly so truncated-tag cases are rejected early with
the accurate "Encrypted session payload is too short." message.
tests/basic/test_sessions_manager.py (1)

154-172: ⚡ Quick win

Add a regression test for list_sessions with session_encrypt=False and key present.

You already cover this mode for load_session; adding the list-path equivalent would prevent behavior drift between listing and loading encrypted sessions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/basic/test_sessions_manager.py` around lines 154 - 172, Add a new
regression test mirroring the load_session case to ensure list_sessions respects
session_encrypt=False when a key is present: create a test similar to
test_list_encrypted_placeholder_without_key (or add a new test function) that
sets up SessionManager and workspace, ensures the encryption key env var
(session_crypto.KEY_ENV) is present, sets
encrypt_coder.args.session_encrypt=False (and session_key_file=None), calls
manager.list_sessions(), and asserts the returned row still shows ["encrypted"]
model and encrypted True; target SessionManager.list_sessions behavior to
prevent divergence from load_session handling of the session_encrypt flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cecli/sessions.py`:
- Around line 51-61: The _read_session_file code can raise UnicodeDecodeError
when decoding non-UTF8 plaintext; add an except UnicodeDecodeError handler
alongside the existing except json.JSONDecodeError and
session_crypto.SessionCryptoError handlers in the function (the block that calls
json.loads(data.decode("utf-8"))), call self.io.tool_error with the error (e.g.
"Error loading session: {e}") and return None so non-UTF8 files are treated as
invalid sessions rather than letting the exception bubble up.
- Around line 129-133: The list_sessions function currently checks for
encryption using _session_encrypt_settings(), tying key resolution to the
session_encrypt flag and causing encrypted session files to appear locked even
when a valid key is available (mismatching load_session behavior); update
list_sessions to resolve the decrypt key the same way load_session does (i.e.,
use the same key-resolution path rather than relying on
_session_encrypt_settings()'s session_encrypt boolean), so when
session_crypto.is_encrypted_payload(raw) is true you call the same key lookup
used by load_session and treat the session as unlocked if that lookup returns a
usable key.

In `@cecli/website/docs/usage/sessions.md`:
- Around line 163-170: Update the sessions.md text near the session-encrypt
example (the block using --session-encrypt and CECLI_SESSION_KEY) to add a clear
note that encrypted session files are not readable JSON on disk and require the
same key to be provided to read them; explicitly state that the
CECLI_SESSION_KEY environment variable or the --session-key-file must be
available when using commands that read sessions (e.g., /load-session and
/list-sessions), so users won’t mistake normal encrypted files for corrupted
JSON.

In `@tests/basic/test_session_crypto.py`:
- Around line 88-100: The test test_cryptography_import_error may not trigger
ImportError if the target module is already cached; before patching
builtins.__import__ in the test, remove
"cryptography.hazmat.primitives.ciphers.aead" (and optionally its parent
packages like "cryptography.hazmat.primitives.ciphers" and
"cryptography.hazmat.primitives") from sys.modules, then apply the monkeypatch
and call session_crypto.encrypt_session_dict to ensure the fake_import runs;
after the assertion, restore any removed modules or avoid persistent mutation so
other tests are unaffected.

---

Nitpick comments:
In `@cecli/session_crypto.py`:
- Around line 95-96: The minimum-length check for the encrypted payload is too
low: update the condition that raises SessionCryptoError when validating blob
(the variable named blob in session_crypto.py) so it requires at least 28 bytes
(12-byte nonce + 16-byte GCM tag) instead of 13; adjust the check and error
message raised by SessionCryptoError accordingly so truncated-tag cases are
rejected early with the accurate "Encrypted session payload is too short."
message.

In `@tests/basic/test_sessions_manager.py`:
- Around line 154-172: Add a new regression test mirroring the load_session case
to ensure list_sessions respects session_encrypt=False when a key is present:
create a test similar to test_list_encrypted_placeholder_without_key (or add a
new test function) that sets up SessionManager and workspace, ensures the
encryption key env var (session_crypto.KEY_ENV) is present, sets
encrypt_coder.args.session_encrypt=False (and session_key_file=None), calls
manager.list_sessions(), and asserts the returned row still shows ["encrypted"]
model and encrypted True; target SessionManager.list_sessions behavior to
prevent divergence from load_session handling of the session_encrypt flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3386c5a-318c-48e1-a0f3-c90ad4ef4099

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2b32c and 2ad0ae3.

📒 Files selected for processing (9)
  • cecli/args.py
  • cecli/session_crypto.py
  • cecli/sessions.py
  • cecli/website/docs/usage/sessions.md
  • requirements/requirements.in
  • tests/basic/conftest.py
  • tests/basic/test_session_args.py
  • tests/basic/test_session_crypto.py
  • tests/basic/test_sessions_manager.py

Comment thread cecli/sessions.py
Comment on lines +51 to +61
parsed = json.loads(data.decode("utf-8"))
if not isinstance(parsed, dict):
self.io.tool_error("Invalid session format.")
return None
return parsed
except session_crypto.SessionCryptoError as e:
self.io.tool_error(str(e))
return None
except json.JSONDecodeError as e:
self.io.tool_error(f"Error loading session: {e}")
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle non-UTF8 plaintext files in _read_session_file.

Line 51 can raise UnicodeDecodeError, which is currently uncaught and can bubble up during load. Treat it as an invalid session read and return None like JSON parse failures.

Proposed fix
-        except json.JSONDecodeError as e:
+        except (UnicodeDecodeError, json.JSONDecodeError) as e:
             self.io.tool_error(f"Error loading session: {e}")
             return None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
parsed = json.loads(data.decode("utf-8"))
if not isinstance(parsed, dict):
self.io.tool_error("Invalid session format.")
return None
return parsed
except session_crypto.SessionCryptoError as e:
self.io.tool_error(str(e))
return None
except json.JSONDecodeError as e:
self.io.tool_error(f"Error loading session: {e}")
return None
parsed = json.loads(data.decode("utf-8"))
if not isinstance(parsed, dict):
self.io.tool_error("Invalid session format.")
return None
return parsed
except session_crypto.SessionCryptoError as e:
self.io.tool_error(str(e))
return None
except (UnicodeDecodeError, json.JSONDecodeError) as e:
self.io.tool_error(f"Error loading session: {e}")
return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cecli/sessions.py` around lines 51 - 61, The _read_session_file code can
raise UnicodeDecodeError when decoding non-UTF8 plaintext; add an except
UnicodeDecodeError handler alongside the existing except json.JSONDecodeError
and session_crypto.SessionCryptoError handlers in the function (the block that
calls json.loads(data.decode("utf-8"))), call self.io.tool_error with the error
(e.g. "Error loading session: {e}") and return None so non-UTF8 files are
treated as invalid sessions rather than letting the exception bubble up.

Comment thread cecli/sessions.py
Comment on lines +129 to +133
raw = session_file.read_bytes()
if session_crypto.is_encrypted_payload(raw):
_, key = self._session_encrypt_settings()
if not key:
sessions.append(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Key resolution in list_sessions is incorrectly tied to session_encrypt.

Line 131 uses _session_encrypt_settings(), so when session_encrypt=False, encrypted sessions are always treated as locked even if a valid env/key-file key exists. This diverges from load_session behavior and can hide metadata unnecessarily.

Proposed fix
-                    _, key = self._session_encrypt_settings()
+                    args = getattr(self.coder, "args", None)
+                    key_file = getattr(args, "session_key_file", None) if args else None
+                    key = session_crypto.resolve_key(key_file=key_file)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raw = session_file.read_bytes()
if session_crypto.is_encrypted_payload(raw):
_, key = self._session_encrypt_settings()
if not key:
sessions.append(
raw = session_file.read_bytes()
if session_crypto.is_encrypted_payload(raw):
args = getattr(self.coder, "args", None)
key_file = getattr(args, "session_key_file", None) if args else None
key = session_crypto.resolve_key(key_file=key_file)
if not key:
sessions.append(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cecli/sessions.py` around lines 129 - 133, The list_sessions function
currently checks for encryption using _session_encrypt_settings(), tying key
resolution to the session_encrypt flag and causing encrypted session files to
appear locked even when a valid key is available (mismatching load_session
behavior); update list_sessions to resolve the decrypt key the same way
load_session does (i.e., use the same key-resolution path rather than relying on
_session_encrypt_settings()'s session_encrypt boolean), so when
session_crypto.is_encrypted_payload(raw) is true you call the same key lookup
used by load_session and treat the session as unlocked if that lookup returns a
usable key.

Comment on lines +163 to +170
When enabled, session files on disk are encrypted (plaintext JSON is unchanged when disabled).

```bash
export CECLI_SESSION_KEY="$(python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')"
cecli --session-encrypt --auto-save
```

Or use `--session-key-file` pointing at a file with the same urlsafe-base64 32-byte key. BrightVision stores the key in the OS keychain and sets `CECLI_SESSION_KEY` for the Vision API process.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify that encrypted session files are not JSON and require a key to read.

This section should explicitly note that encrypted session files won’t be valid JSON on disk and that /load-session or /list-sessions needs CECLI_SESSION_KEY or --session-key-file; otherwise users may misdiagnose normal encrypted files as corrupted.

Suggested doc patch
 ### Optional encryption (AES-256-GCM)

 When enabled, session files on disk are encrypted (plaintext JSON is unchanged when disabled).

 ```bash
 export CECLI_SESSION_KEY="$(python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')"
 cecli --session-encrypt --auto-save

Or use --session-key-file pointing at a file with the same urlsafe-base64 32-byte key. BrightVision stores the key in the OS keychain and sets CECLI_SESSION_KEY for the Vision API process.
+
+> Note: encrypted session files are intentionally not readable JSON on disk.
+> To load or list encrypted sessions, provide the same key via CECLI_SESSION_KEY
+> or --session-key-file.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @cecli/website/docs/usage/sessions.md around lines 163 - 170, Update the
sessions.md text near the session-encrypt example (the block using
--session-encrypt and CECLI_SESSION_KEY) to add a clear note that encrypted
session files are not readable JSON on disk and require the same key to be
provided to read them; explicitly state that the CECLI_SESSION_KEY environment
variable or the --session-key-file must be available when using commands that
read sessions (e.g., /load-session and /list-sessions), so users won’t mistake
normal encrypted files for corrupted JSON.


</details>

<!-- fingerprinting:phantom:poseidon:hawk -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +88 to +100
def test_cryptography_import_error(monkeypatch):
import builtins

real_import = builtins.__import__

def fake_import(name, *args, **kwargs):
if name == "cryptography.hazmat.primitives.ciphers.aead":
raise ImportError("blocked for test")
return real_import(name, *args, **kwargs)

monkeypatch.setattr(builtins, "__import__", fake_import)
with pytest.raises(session_crypto.SessionCryptoError, match="cryptography"):
session_crypto.encrypt_session_dict({"version": 1}, os.urandom(32))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test may not trigger ImportError if cryptography is already imported.

Python caches imported modules in sys.modules. If cryptography.hazmat.primitives.ciphers.aead was imported by an earlier test (e.g., test_roundtrip_encrypted), the patched __import__ won't be called—Python returns the cached module directly. This could make the test pass spuriously or become order-dependent.

To reliably test the ImportError path, also clear the module from sys.modules before the patched call.

Suggested fix
 def test_cryptography_import_error(monkeypatch):
     import builtins
+    import sys
 
     real_import = builtins.__import__
 
     def fake_import(name, *args, **kwargs):
         if name == "cryptography.hazmat.primitives.ciphers.aead":
             raise ImportError("blocked for test")
         return real_import(name, *args, **kwargs)
 
     monkeypatch.setattr(builtins, "__import__", fake_import)
+    # Remove cached module so __import__ is actually called
+    monkeypatch.delitem(sys.modules, "cryptography.hazmat.primitives.ciphers.aead", raising=False)
     with pytest.raises(session_crypto.SessionCryptoError, match="cryptography"):
         session_crypto.encrypt_session_dict({"version": 1}, os.urandom(32))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_cryptography_import_error(monkeypatch):
import builtins
real_import = builtins.__import__
def fake_import(name, *args, **kwargs):
if name == "cryptography.hazmat.primitives.ciphers.aead":
raise ImportError("blocked for test")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", fake_import)
with pytest.raises(session_crypto.SessionCryptoError, match="cryptography"):
session_crypto.encrypt_session_dict({"version": 1}, os.urandom(32))
def test_cryptography_import_error(monkeypatch):
import builtins
import sys
real_import = builtins.__import__
def fake_import(name, *args, **kwargs):
if name == "cryptography.hazmat.primitives.ciphers.aead":
raise ImportError("blocked for test")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", fake_import)
# Remove cached module so __import__ is actually called
monkeypatch.delitem(sys.modules, "cryptography.hazmat.primitives.ciphers.aead", raising=False)
with pytest.raises(session_crypto.SessionCryptoError, match="cryptography"):
session_crypto.encrypt_session_dict({"version": 1}, os.urandom(32))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/basic/test_session_crypto.py` around lines 88 - 100, The test
test_cryptography_import_error may not trigger ImportError if the target module
is already cached; before patching builtins.__import__ in the test, remove
"cryptography.hazmat.primitives.ciphers.aead" (and optionally its parent
packages like "cryptography.hazmat.primitives.ciphers" and
"cryptography.hazmat.primitives") from sys.modules, then apply the monkeypatch
and call session_crypto.encrypt_session_dict to ensure the fake_import runs;
after the assertion, restore any removed modules or avoid persistent mutation so
other tests are unaffected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant