feat: consolidate config files into ~/.limacharlie.d/ directory#257
feat: consolidate config files into ~/.limacharlie.d/ directory#257
Conversation
Unify all CLI config files (credentials, JWT cache, search checkpoints) under a single directory with platform-appropriate defaults. CLI v2 (5.x) is the right time for this - establishing consistent conventions before the public release rather than accumulating tech debt. New layout: - Unix: ~/.limacharlie.d/config.yaml, jwt_cache.json, search_checkpoints/ - Windows: %APPDATA%/limacharlie/config.yaml, jwt_cache.json, search_checkpoints/ Backward compatible: reads from legacy ~/.limacharlie with deprecation warning. New env vars LC_CONFIG_DIR, LC_LEGACY_CONFIG. Existing LC_CREDS_FILE continues to work. Migration via `limacharlie config migrate`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
442b49f to
1f8c13f
Compare
…head Benchmarks cover cached (hot) and uncached (cold) path resolution, config loading at various sizes, credential resolution, config writing, migration overhead, and simulated CLI startup cost. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1f8c13f to
162c381
Compare
|
@tomaz-lc Code review for this PR: Bug: Priority Inconsistency Between
|
- Fix priority inconsistency: get_config_dir() now checks LC_LEGACY_CONFIG before LC_CONFIG_DIR, matching the priority order used by get_config_path() and all other path functions. Previously, setting both env vars would cause get_config_dir() and get_config_path() to disagree on where config lives. - Fix JWT cache directory permissions: _save_cache() now uses secure_makedirs() (0o700) instead of os.makedirs() (default 0o755) when creating the parent directory. JWT cache contains auth tokens and should have restricted permissions matching other config directories. - Fix redundant exception catch: _safe_content_match() now catches only OSError instead of (OSError, Exception), since OSError is already a subclass of Exception. - Extract duplicated _output helper: config_cmd.py and auth.py now import from shared _output_helpers.py instead of duplicating the same 4-line function. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add TestPriorityConsistency: verifies get_config_dir() and get_config_path() agree on priority when both LC_LEGACY_CONFIG and LC_CONFIG_DIR are set. - Add TestSaveCacheDirectoryPermissions: verifies _save_cache() creates parent directories with 0o700 permissions via secure_makedirs. - Add TestSafeContentMatch: verifies _safe_content_match() handles matching files, different files, missing files, and permission errors. - Update test_multiple_env_vars_set_simultaneously to expect the fixed priority behavior (legacy mode wins over LC_CONFIG_DIR in get_config_dir). - Remove 8 tests that exercised Python stdlib behavior rather than our path resolution logic: spaces in paths, unicode paths, null bytes, root path, very long paths, trailing slashes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… races
New test classes in test_file_utils.py covering gaps identified in review:
Symlink rejection (_reject_symlink, safe_open_read, atomic_write):
- Relative symlinks (os.symlink("target", link))
- Chained symlinks (link -> link -> file)
- Circular/self-referencing symlinks
- Dangling symlinks on read path
- Symlinks in secure_makedirs path components
- No temp file leak on symlink rejection
Permission model (secure_makedirs, atomic_write):
- secure_makedirs tightens existing permissive (0o755, 0o777) dirs to 0o700
- All intermediate dirs created with 0o700 (not just leaf)
- atomic_write overwriting world-readable (0o644) or world-writable (0o666)
file results in 0o600
- safe_open_read does not alter file permissions
Race conditions / concurrency (atomic_write):
- Concurrent writers produce no partial reads (atomicity via os.replace)
- Final file after concurrent writes is valid (not a mix of two writers)
- Permissions preserved at 0o600 after concurrent writes
- No temp files left after concurrent writes complete
- os.replace replaces symlink entry itself, does not follow (TOCTOU defense)
Integration (config.save_config, config.load_config, jwt_cache.clear):
- save_config refuses to write through symlinked config.yaml
- load_config raises OSError on symlinked config.yaml (does not silently
return attacker-controlled data)
- clear_jwt_cache removes symlink itself, not the symlink target
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root bypasses file permission checks (0o000 is still readable), so the test_unreadable_file_returns_false test fails in CI which runs as root. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ests Removed 14 tests that tested stdlib/OS behavior rather than our code: - test_o_nofollow_rejects_symlink_at_kernel_level (tested os.open directly) - test_os_replace_does_not_follow_symlinks (tested os.replace directly) - _reject_symlink variant tests for relative/chained/circular/self-ref symlinks (all exercise the same os.path.islink call) - Duplicate symlink variant tests on atomic_write and safe_open_read (relative, chained, dangling - same _reject_symlink code path) - test_rejects_symlink_in_path_components (secure_makedirs delegates to os.mkdir which follows symlinks - stdlib behavior) - test_read_does_not_change_file_permissions (verifying absence of code that doesn't exist) Kept: all tests that exercise our security logic - _reject_symlink core paths (file, parent dir, regular file accept, dangling), atomic_write symlink rejection + temp cleanup, safe_open_read fd leak prevention, permission tightening, concurrency, and integration tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add config_cmd to _COMMAND_MODULE_MAP in cli.py (missing after #257 and #261 were merged independently) - Add config to EXPECTED_TOP_LEVEL_COMMANDS, EXPECTED_MODULE_MAP, and EXPECTED_SUBCOMMANDS in regression tests - Fix test_cli_import_does_not_load_output to handle third-party deps already loaded by other tests in the same pytest process - Add ci.yml GHA workflow that runs unit tests and dist checks on every push and PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add config_cmd to _COMMAND_MODULE_MAP in cli.py (missing after #257 and #261 were merged independently) - Add config to EXPECTED_TOP_LEVEL_COMMANDS, EXPECTED_MODULE_MAP, and EXPECTED_SUBCOMMANDS in regression tests - Fix test_cli_import_does_not_load_output to handle third-party deps already loaded by other tests in the same pytest process - Add ci.yml GHA workflow that runs unit tests and dist checks on every push and PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add config_cmd to _COMMAND_MODULE_MAP in cli.py (missing after #257 and #261 were merged independently) - Add config to EXPECTED_TOP_LEVEL_COMMANDS, EXPECTED_MODULE_MAP, and EXPECTED_SUBCOMMANDS in regression tests - Fix test_cli_import_does_not_load_output to handle third-party deps already loaded by other tests in the same pytest process - Add ci.yml GHA workflow that runs unit tests and dist checks on every push and PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add config_cmd to _COMMAND_MODULE_MAP in cli.py (missing after #257 and #261 were merged independently) - Add config to EXPECTED_TOP_LEVEL_COMMANDS, EXPECTED_MODULE_MAP, and EXPECTED_SUBCOMMANDS in regression tests - Fix test_cli_import_does_not_load_output to handle third-party deps already loaded by other tests in the same pytest process - Add ci.yml GHA workflow that runs unit tests and dist checks on every push and PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add config_cmd to _COMMAND_MODULE_MAP in cli.py (missing after #257 and #261 were merged independently) - Add config to EXPECTED_TOP_LEVEL_COMMANDS, EXPECTED_MODULE_MAP, and EXPECTED_SUBCOMMANDS in regression tests - Fix test_cli_import_does_not_load_output to handle third-party deps already loaded by other tests in the same pytest process - Add ci.yml GHA workflow that runs unit tests and dist checks on every push and PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add config_cmd to _COMMAND_MODULE_MAP in cli.py (missing after #257 and #261 were merged independently) - Add config to EXPECTED_TOP_LEVEL_COMMANDS, EXPECTED_MODULE_MAP, and EXPECTED_SUBCOMMANDS in regression tests - Fix test_cli_import_does_not_load_output to handle third-party deps already loaded by other tests in the same pytest process - Add ci.yml GHA workflow that runs unit tests and dist checks on every push and PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add config_cmd to _COMMAND_MODULE_MAP in cli.py (missing after #257 and #261 were merged independently) - Add config to EXPECTED_TOP_LEVEL_COMMANDS, EXPECTED_MODULE_MAP, and EXPECTED_SUBCOMMANDS in regression tests - Fix test_cli_import_does_not_load_output to handle third-party deps already loaded by other tests in the same pytest process - Add ci.yml GHA workflow that runs unit tests and dist checks on every push and PR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…265) - Add config_cmd to _COMMAND_MODULE_MAP in cli.py (missing after #257 and #261 were merged independently) - Add config to EXPECTED_TOP_LEVEL_COMMANDS, EXPECTED_MODULE_MAP, and EXPECTED_SUBCOMMANDS in regression tests - Fix test_cli_import_does_not_load_output to handle third-party deps already loaded by other tests in the same pytest process - Add ci.yml GHA workflow that runs unit tests and dist checks on every push and PR Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Details
Consolidates all CLI config files (credentials, JWT cache, search checkpoints) under a single directory with platform-appropriate defaults. With CLI v2 (5.x) being a major release, this is the right time to establish consistent file layout conventions and follow platform best practices - rather than shipping with scattered dotfiles (
~/.limacharlie,~/.limacharlie_jwt_cache,~/.limacharlie.d/) and accumulating tech debt that would be harder to change post-release.The new layout unifies everything under
~/.limacharlie.d/on Unix and%APPDATA%/limacharlie/on Windows, with a singlepaths.pymodule as the source of truth for all path resolution. Full backward compatibility is preserved - existing~/.limacharlieconfigs are auto-detected with aUserWarning, and users can migrate vialimacharlie config migrate.Migration is intentionally not automatic (consistent with Docker, AWS CLI, Terraform conventions). The CLI reads from the legacy location and emits a warning; users control when migration happens.
Changes
New files
limacharlie/paths.py- Centralized path resolution module. Single source of truth for config file, JWT cache, and checkpoint directory paths. Handles platform detection (Unix vs Windows%APPDATA%), env var overrides (LC_CONFIG_DIR,LC_CREDS_FILE,LC_LEGACY_CONFIG,LC_NO_MIGRATION_WARNING), new-then-legacy fallback with warnings, and per-process caching.limacharlie/commands/config_cmd.py- NewconfigCLI command group:config show-paths- Display all resolved paths, existence flags, active env var overridesconfig migrate- Copy files from legacy to new layout (--dry-run,--remove-old,--force). Content verification before legacy file removal prevents data loss when destination has different content.Modified files
config.py- Delegates path resolution topaths.py, ensures parent directory exists on writejwt_cache.py- Delegates cache path topaths.pysearch_checkpoint.py- Delegates checkpoint dir topaths.pyconstants.py- Removed duplicateCONFIG_FILE_PATHcommands/auth.py,help_topics.py- Updated path references in help textdoc/authentication.md- Added config migration guide, JWT caching docsREADME.md- Added documentation section with linkspyproject.toml- Added pytest config:testpathsincludes microbenchmarks with--benchmark-disablecloudbuild_pr.yaml- Unit test steps now include microbenchmarks as correctness tests; dedicated benchmark step uses-o 'addopts='to override.github/workflows/publish-to-pypi.yml- Pre-publish tests now include microbenchmarksCONFIG_FILE_PATHto usingLC_CONFIG_DIRenv var +_reset_path_cache()New test files
tests/unit/test_paths.py(62 tests) - Path resolution correctness, env var priority, caching, security (path traversal, symlinks, null bytes), edge cases (unicode, spaces, long paths), warning type verification (UserWarningnotDeprecationWarning),LC_NO_MIGRATION_WARNINGstrict=="1"semanticstests/unit/test_config_cmd.py(32 tests) - Migrate correctness, dry-run, force, content verification before removal, security (symlink rejection, secure permissions), robustness (unwritable dirs, empty/large/unicode files, partial failure, content mismatch exit codes)tests/integration/test_config_directory.py(29 tests) - End-to-end scenarios: fresh install, legacy-to-migration lifecycle, LC_CONFIG_DIR override, LC_LEGACY_CONFIG mode, LC_CREDS_FILE override, cross-subsystem consistency, permissions, concurrent-like access patternstests/microbenchmarks/test_paths_microbenchmark.py(29 benchmarks) - Overhead measurement for all path resolution and config loading operationsPath resolution behavior
~/.limacharlie.d/config.yaml~/.limacharlie.d/jwt_cache.json~/.limacharlie.d/search_checkpoints/~/.limacharlie(+ warning)~/.limacharlie_jwt_cache~/.limacharlie.d/search_checkpoints/config migrate~/.limacharlie.d/config.yaml~/.limacharlie.d/jwt_cache.json~/.limacharlie.d/search_checkpoints/LC_LEGACY_CONFIG=1~/.limacharlie(no warning)~/.limacharlie_jwt_cache~/.limacharlie.d/search_checkpoints/LC_CONFIG_DIR=/foo/foo/config.yaml/foo/jwt_cache.json/foo/search_checkpoints/LC_CREDS_FILE=/foo/bar/foo/bar/foo/bar_jwt_cache/foo/bar.d/search_checkpoints/%APPDATA%/limacharlie/config.yaml%APPDATA%/limacharlie/jwt_cache.json%APPDATA%/limacharlie/search_checkpoints/New environment variables
LC_CONFIG_DIRLC_CREDS_FILELC_LEGACY_CONFIG=1LC_NO_MIGRATION_WARNING=1LC_EPHEMERAL_CREDSBlast radius / isolation
config.py,jwt_cache.py,search_checkpoint.pypath resolution (all now delegate topaths.py). Help text inauth.py,help_topics.py. CI config (cloudbuild_pr.yaml,publish-to-pypi.yml).LC_CREDS_FILEandLC_EPHEMERAL_CREDSbehavior is preserved unchanged.Performance characteristics
Path resolution uses per-process caching. Benchmark data from
test_paths_microbenchmark.py:get_config_path()cachedget_jwt_cache_path()cachedget_all_paths()cachedget_config_path()coldget_all_paths()coldload_config()cachedload_config()cold (small)load_config()cold (20 envs)save_config()smallSummary: Per-process caching makes the overhead negligible. After the first call (~156us cold startup), subsequent path/config lookups are ~57-66ns (pure dict return). No regression vs the previous direct
CONFIG_FILE_PATHconstant lookup.Notable contracts / APIs
config show-paths,config migrateLC_CONFIG_DIR,LC_LEGACY_CONFIG,LC_NO_MIGRATION_WARNINGUserWarning(notDeprecationWarning) so it's visible by default for installed packages. SDK consumers can filter via standardwarnings.filterwarnings().config migrate --remove-oldexits with code 3 when legacy files have different content than destination (requires manual review). Exit code 0 only when all operations succeed.Test plan
paths.py(correctness, priority, security, edge cases, warning semantics)config_cmd.py(migrate, show-paths, security, robustness, exit codes)--benchmark-disable)config show-paths,config migrate --dry-run, warning emission,LC_LEGACY_CONFIG=1/LC_NO_MIGRATION_WARNING=1suppression🤖 Generated with Claude Code