Skip to content

feat: add --password to soul export for AES-256-GCM encryption at rest#294

Open
sshekhar563 wants to merge 4 commits into
qbtrix:devfrom
sshekhar563:feat/export-password
Open

feat: add --password to soul export for AES-256-GCM encryption at rest#294
sshekhar563 wants to merge 4 commits into
qbtrix:devfrom
sshekhar563:feat/export-password

Conversation

@sshekhar563

Copy link
Copy Markdown
Collaborator

Description

This PR implements Phase 2 of the encryption feature by wiring the --password flag through the soul export CLI command. The underlying AES-256-GCM encryption and scrypt key derivation logic was already implemented in the runtime/export/crypto.py layer; this PR surfaces it securely to the user.

Key Changes

  • Secure Interactive Prompts: The --password option is implemented as a prompt-only flag (is_flag=True). This prevents users from passing passwords inline (e.g. --password mysecret), ensuring passwords do not leak into shell history or process listings.
  • Dependency Guard: Added a user-friendly error message if the cryptography package is missing.
  • Manifest Readability: The manifest.json file remains unencrypted so readers can inspect metadata and see the "encrypted": true flag without needing the password.
  • Documentation: Updated docs/cli-reference.md with the new option and an example of its interactive usage.
  • Testing: Added tests/test_cli/test_export_password.py with 9 tests covering archive structure, interactive prompt simulation, mismatched confirmation handling, format warnings, and async encryption/decryption round-trips.

Verification

  • ruff check and ruff format --check pass.
  • 9/9 tests pass in tests/test_cli/test_export_password.py.

- Changed --password to be a prompt-only flag (is_flag=True) so passwords aren't passed inline and leaked to shell history

- Tests now use interactive prompts for export

- Added warning when password is used with non-soul format

- Added missing cryptography dependency check
@sshekhar563
sshekhar563 requested a review from prakashUXtech July 4, 2026 08:02

@prakashUXtech prakashUXtech left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The export side here is well built. Making --password a flag with a hidden, confirmed prompt means the password never lands in shell history or ps, you route through the existing AES-256-GCM path instead of reimplementing crypto, and you handle the non-soul format and the cancelled-confirmation cases cleanly. The care shows.

What blocks merge is that this is encryption with no way back in from the CLI. After soul export --password, no soul command can open the resulting file, and the read commands (unpack, inspect, init --from-file) hit the encrypted container and raise a raw traceback instead of a clear message. So a user who encrypts their only copy through the CLI is locked out of it through that same CLI. The library already supports the read side (Soul.awaken(password=...)), so this is mostly a matter of surfacing it.

Before merge:

  1. Add a prompt-based --password to at least soul unpack and soul inspect, wired to Soul.awaken(source, password=...), mirroring the secure prompt you already wrote on the export side. If the team would rather split that into a follow-up, let's track it explicitly, and then item 2 still needs to land now.
  2. Catch SoulEncryptedError / SoulDecryptionError in the read commands and print a friendly "this soul is encrypted, pass --password" message instead of a traceback.
  3. A CLI round-trip test would make this solid: export --password, then read it back with the password and assert a memory survived, plus a wrong-password case giving a clean error. The current round-trip test asserts the name only and goes through the library rather than the CLI.
  4. The issue also asked for a short docs/encryption.md. Adding that (or a cli-reference section) would round it out.

To be clear, the part that's here is good work, and the secure prompt in particular shows real care. The ask is just to finish the loop: a soul you can lock, you should be able to unlock from the same CLI.

…, docs (qbtrix#294 review)

Addresses all 4 review items from prakashUXtech:

1. Added --password flag to soul inspect and soul unpack so encrypted
   souls can be decrypted from the CLI (mirrors the export --password flow).

2. Added _awaken_or_fail() helper that catches SoulEncryptedError and
   SoulDecryptionError, printing friendly messages instead of tracebacks.
   All 47 Soul.awaken() calls in the CLI now route through this helper.

3. Added TestCLIRoundTrip with 4 integration tests:
   - export --password -> inspect --password (correct password)
   - export --password -> unpack --password (correct password)
   - inspect encrypted without --password (friendly hint)
   - inspect --password with wrong password (friendly error)

4. Added docs/encryption.md covering AES-256-GCM spec, CLI usage,
   Python API, exception handling, and archive format details.
   Updated docs/cli-reference.md with --password option for inspect.

@prakashUXtech prakashUXtech left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The write-only problem is solved. soul inspect --password and soul unpack --password now decrypt through Soul.awaken(password=...), a wrong or missing password gives a clean message instead of a traceback, and the new docs/encryption.md is a good addition. The password hygiene is solid on both sides (hidden prompts, no inline value that could land in shell history). This is a real step up from last round.

One thing before merge, and it maps directly to the issue's acceptance criterion. The round-trip tests currently assert the soul's name survives, but the name lives in the plaintext manifest, so a bug that dropped the encrypted memory tiers during export or awaken would still pass. Could you add an assertion that a memory survives the encrypted round-trip? You already store one ("Secret memory" / "The secret code is 42"), so asserting restored.memory_count > 0 (or recalling it) is a one-liner, and inspect even prints a memory count the CLI test could check.

Optional, non-blocking: a line in encryption.md noting the manifest (name, DID, role) stays readable by design, so someone encrypting for privacy knows the memories are protected but the identity header is not. And the main.py top-of-file comment could use an (#294) Updated line.

Really close now.

…vacy note, header

1. Assert encrypted memory tiers survive round-trip (not just the plaintext manifest name)

2. Add privacy note to encryption.md: manifest (name/DID/role) stays readable by design

3. Add Updated: 2026-07-20 (qbtrix#294) header to cli/main.py

@prakashUXtech prakashUXtech left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The memory-survival assertion landed, and it's stronger than I asked for. test_awaken_with_correct_password stores "Secret memory", does the encrypted round-trip, and asserts the exact content comes back (any("Secret memory" in f.content ...)), so a soul that silently dropped its encrypted tiers would fail even if it had default facts. Both optional asks are in too: the encryption.md note that the manifest (name, DID, role) stays readable by design, and the (#294) header line. The decrypt path is unregressed and actually improved, since friendly errors now cover all the read commands.

Approving. One small non-blocking follow-up for later: the mutating commands (remember, note, forget) now hint "pass --password" on an encrypted soul, but they don't expose a --password flag yet, so the hint points at an option that isn't there. In-place editing of an encrypted archive is outside this PR's scope, so it's a future item, not a change here. Nice work.

@prakashUXtech

Copy link
Copy Markdown
Contributor

Heads-up: #294 now conflicts with dev because #299 landed first and both touch cli/main.py (its private-access cleanup overlaps your _awaken_or_fail routing). The review is an approve on the merits, so the only thing between this and merge is a rebase. Could you git fetch origin && git rebase origin/dev, resolve the cli/main.py overlap (keep your password wiring and the public-API accessors that #299 added), and force-push? Ping me after and I'll merge. Sorry for the ordering, that was on our side sequencing #299 first.

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.

2 participants