Skip to content

Commit c043917

Browse files
committed
Auto-commit: Multi-agent iteration 1 - 2026-02-11 12:45
1 parent 6af37d2 commit c043917

15 files changed

Lines changed: 2088 additions & 60 deletions

assistant/cli/__main__.py

Lines changed: 155 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
python -m assistant.cli schedule enable <task_id>
2828
python -m assistant.cli schedule disable <task_id>
2929
python -m assistant.cli schedule history <task_id>
30+
python -m assistant.cli settings encryption-status
3031
python -m assistant.cli settings encrypt
32+
python -m assistant.cli settings clear-invalid --confirm
33+
python -m assistant.cli settings reencrypt
3134
python -m assistant.cli settings status
3235
"""
3336
import argparse
@@ -816,32 +819,56 @@ async def schedule_validate_command(args):
816819

817820
# Settings commands
818821

819-
async def settings_encrypt_command(args):
820-
"""Migrate existing plaintext API keys to encrypted format."""
822+
async def settings_encryption_status_command(args):
823+
"""Show detailed encryption status for all sensitive keys."""
821824
service = SettingsService(config.DATABASE_PATH)
825+
status = await service.get_encryption_status()
822826

823-
if args.status_only:
824-
# Just show encryption status
825-
status = await service.get_encryption_status()
827+
if args.json:
828+
print(json.dumps(status, indent=2))
829+
return
826830

827-
if args.json:
828-
print(json.dumps(status, indent=2))
829-
return
831+
print("Encryption Status")
832+
print("=" * 60)
833+
print(f"Encryption available: {'Yes' if status['encryption_available'] else 'No'}")
834+
print(f"All keys encrypted: {'Yes' if status['all_encrypted'] else 'No'}")
835+
print(f"All keys decryptable: {'Yes' if status.get('all_decryptable', True) else 'No'}")
836+
print()
830837

831-
print("Encryption Status")
832-
print("=" * 50)
833-
print(f"Encryption available: {'Yes' if status['encryption_available'] else 'No'}")
834-
print(f"All keys encrypted: {'Yes' if status['all_encrypted'] else 'No'}")
838+
if status.get('errors'):
839+
print("Decryption Errors (logged):")
840+
for key, error_type in status['errors'].items():
841+
print(f" - {key}: {error_type}")
835842
print()
836-
print("API Key Status:")
837-
for key, info in status['keys'].items():
838-
status_icon = "OK" if info['is_encrypted'] else ("WARN" if info['has_value'] else "N/A")
839-
has_value_str = "set" if info['has_value'] else "not set"
843+
844+
print("API Key Status:")
845+
for key, info in status['keys'].items():
846+
has_value_str = "set" if info['has_value'] else "not set"
847+
if not info['has_value']:
848+
print(f" [N/A] {key}: {has_value_str}")
849+
elif info['can_decrypt']:
840850
encrypted_str = "encrypted" if info['is_encrypted'] else "plaintext"
841-
if info['has_value']:
842-
print(f" [{status_icon}] {key}: {has_value_str}, {encrypted_str}")
843-
else:
844-
print(f" [N/A] {key}: {has_value_str}")
851+
status_icon = "OK" if info['is_encrypted'] else "WARN"
852+
print(f" [{status_icon}] {key}: {has_value_str}, {encrypted_str}, decryptable")
853+
else:
854+
print(f" [ERR] {key}: {has_value_str}, encrypted, CANNOT DECRYPT")
855+
856+
print()
857+
print("Actions:")
858+
if not status.get('all_encrypted'):
859+
print(" - Run 'python -m cli settings encrypt' to encrypt plaintext keys")
860+
if not status.get('all_decryptable', True):
861+
print(" - Run 'python -m cli settings clear-invalid' to remove undecryptable keys")
862+
print(" - Or restore encryption key salt from backup and run 'python -m cli settings reencrypt'")
863+
864+
865+
async def settings_encrypt_command(args):
866+
"""Migrate existing plaintext API keys to encrypted format."""
867+
service = SettingsService(config.DATABASE_PATH)
868+
869+
if args.status_only:
870+
# Just show encryption status (legacy - redirect to encryption-status)
871+
await settings_encryption_status_command(args)
845872
return
846873

847874
# Perform migration
@@ -869,6 +896,77 @@ async def settings_encrypt_command(args):
869896
sys.exit(1)
870897

871898

899+
async def settings_clear_invalid_command(args):
900+
"""Clear all encrypted keys that cannot be decrypted."""
901+
service = SettingsService(config.DATABASE_PATH)
902+
903+
# Confirm unless --confirm flag is set
904+
if not args.confirm:
905+
print("This will DELETE all encrypted keys that cannot be decrypted.")
906+
print("You will need to re-enter these API keys in the Settings UI.")
907+
print()
908+
print("Run with --confirm to proceed.")
909+
sys.exit(0)
910+
911+
result = await service.clear_invalid_encrypted_keys()
912+
913+
if args.json:
914+
print(json.dumps(result, indent=2))
915+
return
916+
917+
if result['success']:
918+
print("Clear Invalid Keys Complete")
919+
print("=" * 50)
920+
if result['cleared']:
921+
print(f"Cleared {len(result['cleared'])} key(s):")
922+
for key in result['cleared']:
923+
print(f" - {key}")
924+
else:
925+
print("No invalid keys found.")
926+
if result['skipped']:
927+
print(f"\nSkipped {len(result['skipped'])} key(s) (not encrypted or decryptable):")
928+
for key in result['skipped']:
929+
print(f" - {key}")
930+
else:
931+
print(f"Error: {result['error']}", file=sys.stderr)
932+
sys.exit(1)
933+
934+
935+
async def settings_reencrypt_command(args):
936+
"""Re-encrypt all sensitive keys with the current encryption key."""
937+
service = SettingsService(config.DATABASE_PATH)
938+
939+
print("Re-encrypting all sensitive keys with current encryption key...")
940+
result = await service.reencrypt_with_current_key()
941+
942+
if args.json:
943+
print(json.dumps(result, indent=2))
944+
return
945+
946+
if result['success']:
947+
print()
948+
print("Re-encryption Complete")
949+
print("=" * 50)
950+
if result['reencrypted']:
951+
print(f"Re-encrypted {len(result['reencrypted'])} key(s):")
952+
for key in result['reencrypted']:
953+
print(f" - {key}")
954+
else:
955+
print("No keys needed re-encryption.")
956+
if result['skipped']:
957+
print(f"\nSkipped {len(result['skipped'])} key(s) (no value set):")
958+
for key in result['skipped']:
959+
print(f" - {key}")
960+
else:
961+
print()
962+
print(f"Re-encryption failed: {result['error']}", file=sys.stderr)
963+
if result['failed']:
964+
print("\nFailed keys:")
965+
for failure in result['failed']:
966+
print(f" - {failure['key']}: {failure['error']}")
967+
sys.exit(1)
968+
969+
872970
async def settings_status_command(args):
873971
"""Show current settings (with masked API keys)."""
874972
service = SettingsService(config.DATABASE_PATH)
@@ -1294,19 +1392,48 @@ def main():
12941392
settings_parser = subparsers.add_parser("settings", help="Manage settings and encryption")
12951393
settings_subparsers = settings_parser.add_subparsers(dest="settings_command", help="Settings commands")
12961394

1395+
# settings encryption-status
1396+
settings_enc_status_parser = settings_subparsers.add_parser("encryption-status", help="Show detailed encryption status")
1397+
settings_enc_status_parser.add_argument(
1398+
"--json", "-j",
1399+
action="store_true",
1400+
help="Output in JSON format"
1401+
)
1402+
12971403
# settings encrypt
12981404
settings_encrypt_parser = settings_subparsers.add_parser("encrypt", help="Migrate API keys to encrypted format")
12991405
settings_encrypt_parser.add_argument(
13001406
"--status-only", "-s",
13011407
action="store_true",
1302-
help="Only show encryption status without migrating"
1408+
help="Only show encryption status without migrating (redirects to encryption-status)"
13031409
)
13041410
settings_encrypt_parser.add_argument(
13051411
"--json", "-j",
13061412
action="store_true",
13071413
help="Output in JSON format"
13081414
)
13091415

1416+
# settings clear-invalid
1417+
settings_clear_parser = settings_subparsers.add_parser("clear-invalid", help="Clear undecryptable encrypted keys")
1418+
settings_clear_parser.add_argument(
1419+
"--confirm", "-y",
1420+
action="store_true",
1421+
help="Confirm deletion"
1422+
)
1423+
settings_clear_parser.add_argument(
1424+
"--json", "-j",
1425+
action="store_true",
1426+
help="Output in JSON format"
1427+
)
1428+
1429+
# settings reencrypt
1430+
settings_reencrypt_parser = settings_subparsers.add_parser("reencrypt", help="Re-encrypt keys with current key")
1431+
settings_reencrypt_parser.add_argument(
1432+
"--json", "-j",
1433+
action="store_true",
1434+
help="Output in JSON format"
1435+
)
1436+
13101437
# settings status
13111438
settings_status_parser = settings_subparsers.add_parser("status", help="Show settings status")
13121439
settings_status_parser.add_argument(
@@ -1389,8 +1516,14 @@ def main():
13891516
schedule_parser.print_help()
13901517
sys.exit(1)
13911518
elif args.command == "settings":
1392-
if args.settings_command == "encrypt":
1519+
if args.settings_command == "encryption-status":
1520+
asyncio.run(settings_encryption_status_command(args))
1521+
elif args.settings_command == "encrypt":
13931522
asyncio.run(settings_encrypt_command(args))
1523+
elif args.settings_command == "clear-invalid":
1524+
asyncio.run(settings_clear_invalid_command(args))
1525+
elif args.settings_command == "reencrypt":
1526+
asyncio.run(settings_reencrypt_command(args))
13941527
elif args.settings_command == "status":
13951528
asyncio.run(settings_status_command(args))
13961529
else:

0 commit comments

Comments
 (0)