From 712e31ec0a889160d5394f8ab6af808dce4fed0f Mon Sep 17 00:00:00 2001 From: Tommi Saltiola Date: Sun, 9 Aug 2026 00:25:08 -0600 Subject: [PATCH 1/3] feat(shell): route native caches externally --- docs/specs/shell_auth_startup/BACKLOG.md | 1 + docs/specs/shell_auth_startup/CHANGELOG.md | 18 +++++++ docs/specs/shell_auth_startup/README.md | 46 ++++++++++++++-- dot_common_profile.tmpl | 18 +++++++ tests/test_terminal_environment.py | 61 ++++++++++++++++++++++ 5 files changed, 140 insertions(+), 4 deletions(-) diff --git a/docs/specs/shell_auth_startup/BACKLOG.md b/docs/specs/shell_auth_startup/BACKLOG.md index ced9f9c..cd83ea9 100644 --- a/docs/specs/shell_auth_startup/BACKLOG.md +++ b/docs/specs/shell_auth_startup/BACKLOG.md @@ -13,3 +13,4 @@ | AUTH-009 | Tailnet-only self-hosted Atuin and portable lmsh terminal profile | 2026-07-25 | `b44b1eb`, `2cee8ea` | | AUTH-010 | Deny-by-default lmsh source target boundary | 2026-07-25 | `4fdc25e` | | AUTH-010-session-credentials | Session-isolated GCP credentials with stale-file rematerialization | 2026-08-01 | `a8e0d9a` | +| AUTH-011-cache-relocation | Routed supported CLI caches and Pulumi home through sentinel-guarded native paths; paused PyCharm, Prefect, and Codex | 2026-08-09 | pending Gate Commit | diff --git a/docs/specs/shell_auth_startup/CHANGELOG.md b/docs/specs/shell_auth_startup/CHANGELOG.md index 10da904..b676f5c 100644 --- a/docs/specs/shell_auth_startup/CHANGELOG.md +++ b/docs/specs/shell_auth_startup/CHANGELOG.md @@ -1,5 +1,23 @@ # Shell Auth Startup Changelog +## 2026-08-09 + +- Routed Playwright, uv, pre-commit, npm, and Pulumi through their native path + controls only when the existing external-state sentinel is present. Inherited + managed values clear on the fallback path without overriding unrelated values. +- Copied Playwright browsers and Pulumi home to external storage while retaining + internal rollback copies. Live path checks passed for all CLI tools, including + a Playwright Chromium screenshot, uv package execution, npm cache verification, + pre-commit database initialization, and Pulumi plugin inventory. +- Rolled back the attempted PyCharm properties migration after review found no + sentinel-aware fallback. Prefect and Codex remain internal because their CLIs + are unavailable for migration integrity tests. Host cleanup remains operator + blocked by runtime filesystem policy and interactive `sudo`. +- Validation: 7 focused tests, Bash/Zsh render syntax, exact chezmoi deployment, + Git diff checks, and independent review passed after rollback hardening. + Accepted risk: `AUTH-011-AR1`. Release is not applicable. Final Push remains + pending lifecycle completion. + ## 2026-08-01 - Materialized GCP credentials in a private directory per loading shell and diff --git a/docs/specs/shell_auth_startup/README.md b/docs/specs/shell_auth_startup/README.md index b24830b..cec7713 100644 --- a/docs/specs/shell_auth_startup/README.md +++ b/docs/specs/shell_auth_startup/README.md @@ -15,10 +15,9 @@ | Maintenance | Pin image and clients, review updates and accepted risks, retain rollback until restore is proven | | Authorities | Chezmoi rendering, shell syntax, pytest contracts, Compose validation, health/sync probes, lifecycle audit, and independent review | -Current cycle `AUTH-010-session-credentials` is elevated-risk sensitive-data -handling work. The security module applies. Release, Deploy, Operate, and -Maintain/Retire are not applicable because this cycle changes only local -chezmoi source; Review/Integrate is required. +Current cycle `AUTH-011-cache-relocation` is elevated-risk local storage and +sensitive-data handling work. The security module applies. Release is not +applicable; Deploy, Operate, Maintain/Retire, and Review/Integrate are required. ## Domain @@ -34,6 +33,11 @@ Entities: - `ClockifyPoller`: SketchyBar plugin that checks current Clockify timer. - `TerminalProfile`: portable Bash, Atuin, zoxide, and Starship configuration selected by chezmoi machine intent and operating system. +- `ExternalCacheRoot`: mounted Mac mini cache storage below `/Volumes/ext/state/cache`. +- `NativeCacheClient`: Playwright, uv, pre-commit, or npm configured + through its supported cache-path interface. +- `PulumiHome`: Pulumi's credential, workspace, schema, and plugin directory, + selected through its supported `PULUMI_HOME` interface. - `TerminalTargetAllowlist`: deny-by-default set of files and scripts that the personal source may apply to an `lmsh` guest. - `AtuinClient`: one machine-local history database, record store, encryption @@ -219,6 +223,21 @@ Glossary: ### Feature: Private self-hosted history sync +### Feature: Native external caches + +**Scenario: External cache volume is available** +- Given the Mac mini state sentinel exists +- When an interactive shell starts +- Then Playwright, uv, pre-commit, and npm receive native paths below + `ExternalCacheRoot` +- And Pulumi uses `/Volumes/ext/state/pulumi` as `PulumiHome` + +**Scenario: External cache volume is unavailable** +- Given the Mac mini state sentinel is absent +- When an interactive shell starts +- Then no external CLI cache variable is exported +- And each CLI retains its native local default + **Scenario: Tailnet client reaches Atuin** - Given the pinned Atuin container is healthy on Mac loopback - When an authorized Mac or Lima client connects to the stable tailnet URL @@ -311,6 +330,15 @@ Glossary: - **Invariant:** optional shell tools are command-guarded. - **Invariant:** the lmsh target excludes SSH private keys, GitHub credentials, 1Password integration, GUI applications, and macOS service configuration. +- **Invariant:** external CLI cache exports are Mac-mini-only and require the + existing `/Volumes/ext/state/.dotfiles-ai-state` sentinel. +- **Invariant:** Playwright, uv, pre-commit, and npm use only their + documented native path controls; shell-wide `XDG_CACHE_HOME` and cache + symlinks are not used. +- **Invariant:** Pulumi uses its documented `PULUMI_HOME`; Prefect databases and + Codex session state remain internal until their installed runtimes can pass + migration integrity tests. PyCharm remains internal until its external paths + can fail back when the volume is unavailable. - **Invariant:** `TerminalTargetAllowlist` denies all targets by default and re-includes only `.bash_profile`, `.bashrc`, `.common_profile`, `.config/atuin/config.toml`, `.config/starship.toml`, and @@ -343,6 +371,13 @@ Glossary: - **Post:** remote failure does not prevent local history search or capture. ### Accepted Risk +- `AUTH-011-AR1`: the operator approved moving Pulumi credentials and executable + caches to the existing unencrypted, `noowners` external state volume after the + trust limitation was reported. The state sentinel, retained internal rollback + copies, and trusted physical custody compensate but do not provide encryption + or offline tamper resistance. Owner: operator. Review before the volume leaves + trusted custody, another local account gains access, or the storage policy + changes. - `AUTH-009-AR1`: the operator approved synchronizing MGM shell history through the same personal account, making work commands and paths decryptable on personal clients. End-to-end encryption, secret filters, tailnet-only ingress, @@ -352,6 +387,9 @@ Glossary: ## Verification - Shell syntax checks pass for edited scripts. +- Rendered Mac mini shell startup selects external CLI caches only with the + sentinel present; absent-sentinel startup retains local defaults. +- PyCharm custom properties remain absent after the attempted migration rollback. - Static search confirms no Herdr profile auto-`secret` block remains. - Static search confirms Clockify poller has no `op read` call. - Static search confirms Databricks config has no `onepasswordRead` call. diff --git a/dot_common_profile.tmpl b/dot_common_profile.tmpl index 7a32d1f..a77a02f 100644 --- a/dot_common_profile.tmpl +++ b/dot_common_profile.tmpl @@ -98,6 +98,24 @@ alias ch="clickhouse client --host \"\$(op read 'op://Personal/ClickHouse Cloud/ # --- Environment Variables --- +{{ if eq .machine_type "mac-mini" -}} +if [ -f "/Volumes/ext/state/.dotfiles-ai-state" ]; then + export DOTFILES_CACHE_ROOT="/Volumes/ext/state/cache" + export PLAYWRIGHT_BROWSERS_PATH="$DOTFILES_CACHE_ROOT/playwright" + export UV_CACHE_DIR="$DOTFILES_CACHE_ROOT/uv" + export PRE_COMMIT_HOME="$DOTFILES_CACHE_ROOT/pre-commit" + export npm_config_cache="$DOTFILES_CACHE_ROOT/npm" + export PULUMI_HOME="/Volumes/ext/state/pulumi" +else + [ "${PLAYWRIGHT_BROWSERS_PATH:-}" = "/Volumes/ext/state/cache/playwright" ] && unset PLAYWRIGHT_BROWSERS_PATH + [ "${UV_CACHE_DIR:-}" = "/Volumes/ext/state/cache/uv" ] && unset UV_CACHE_DIR + [ "${PRE_COMMIT_HOME:-}" = "/Volumes/ext/state/cache/pre-commit" ] && unset PRE_COMMIT_HOME + [ "${npm_config_cache:-}" = "/Volumes/ext/state/cache/npm" ] && unset npm_config_cache + [ "${PULUMI_HOME:-}" = "/Volumes/ext/state/pulumi" ] && unset PULUMI_HOME + [ "${DOTFILES_CACHE_ROOT:-}" = "/Volumes/ext/state/cache" ] && unset DOTFILES_CACHE_ROOT +fi +{{ end -}} + # Carapace completion bridges export CARAPACE_BRIDGES="zsh,fish,bash,inshellisense" diff --git a/tests/test_terminal_environment.py b/tests/test_terminal_environment.py index b0d9a59..ba8f524 100644 --- a/tests/test_terminal_environment.py +++ b/tests/test_terminal_environment.py @@ -1,3 +1,5 @@ +import os +import subprocess from pathlib import Path @@ -28,6 +30,65 @@ def test_lmsh_profile_is_portable_and_excludes_credentials(): assert "/opt/homebrew" in macos +def test_mac_mini_uses_native_external_cache_paths(): + profile = text("dot_common_profile.tmpl") + assert '[ -f "/Volumes/ext/state/.dotfiles-ai-state" ]' in profile + for setting in ( + 'PLAYWRIGHT_BROWSERS_PATH="$DOTFILES_CACHE_ROOT/playwright"', + 'UV_CACHE_DIR="$DOTFILES_CACHE_ROOT/uv"', + 'PRE_COMMIT_HOME="$DOTFILES_CACHE_ROOT/pre-commit"', + 'npm_config_cache="$DOTFILES_CACHE_ROOT/npm"', + 'PULUMI_HOME="/Volumes/ext/state/pulumi"', + ): + assert setting in profile + for variable in ( + "DOTFILES_CACHE_ROOT", + "PLAYWRIGHT_BROWSERS_PATH", + "UV_CACHE_DIR", + "PRE_COMMIT_HOME", + "npm_config_cache", + "PULUMI_HOME", + ): + assert f'unset {variable}' in profile + + +def cache_env_script(sentinel): + profile = text("dot_common_profile.tmpl") + return profile.split('{{ if eq .machine_type "mac-mini" -}}', 1)[1].split("{{ end -}}", 1)[0].replace( + "/Volumes/ext/state/.dotfiles-ai-state", str(sentinel) + ) + + +def test_external_cache_exports_and_fallback(tmp_path): + sentinel = tmp_path / "sentinel" + script = cache_env_script(sentinel) + managed = { + "DOTFILES_CACHE_ROOT": "/Volumes/ext/state/cache", + "PLAYWRIGHT_BROWSERS_PATH": "/Volumes/ext/state/cache/playwright", + "UV_CACHE_DIR": "/Volumes/ext/state/cache/uv", + "PRE_COMMIT_HOME": "/Volumes/ext/state/cache/pre-commit", + "npm_config_cache": "/Volumes/ext/state/cache/npm", + "PULUMI_HOME": "/Volumes/ext/state/pulumi", + } + + for shell in ("/bin/bash", "/bin/zsh"): + sentinel.touch() + exported = subprocess.run( + [shell, "-c", script + "\nenv"], check=True, capture_output=True, text=True + ).stdout + for name, value in managed.items(): + assert f"{name}={value}" in exported + + sentinel.unlink() + inherited = os.environ | managed | {"UV_CACHE_DIR": "/custom/uv"} + fallback = subprocess.run( + [shell, "-c", script + "\nenv"], env=inherited, check=True, capture_output=True, text=True + ).stdout + assert "UV_CACHE_DIR=/custom/uv" in fallback + for name in managed.keys() - {"UV_CACHE_DIR"}: + assert f"{name}=" not in fallback + + def test_lmsh_targets_are_deny_by_default(): ignored = text(".chezmoiignore") lmsh = ignored.split('{{ if eq .machine_type "lmsh" }}', 1)[1].split("{{ end }}", 1)[0] From 6be754b909e18318bc176e223aa0d4962ebeafa1 Mon Sep 17 00:00:00 2001 From: Tommi Saltiola Date: Sun, 9 Aug 2026 00:28:36 -0600 Subject: [PATCH 2/3] docs(shell): close cache relocation --- docs/specs/shell_auth_startup/BACKLOG.md | 2 +- docs/specs/shell_auth_startup/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specs/shell_auth_startup/BACKLOG.md b/docs/specs/shell_auth_startup/BACKLOG.md index cd83ea9..53a2662 100644 --- a/docs/specs/shell_auth_startup/BACKLOG.md +++ b/docs/specs/shell_auth_startup/BACKLOG.md @@ -13,4 +13,4 @@ | AUTH-009 | Tailnet-only self-hosted Atuin and portable lmsh terminal profile | 2026-07-25 | `b44b1eb`, `2cee8ea` | | AUTH-010 | Deny-by-default lmsh source target boundary | 2026-07-25 | `4fdc25e` | | AUTH-010-session-credentials | Session-isolated GCP credentials with stale-file rematerialization | 2026-08-01 | `a8e0d9a` | -| AUTH-011-cache-relocation | Routed supported CLI caches and Pulumi home through sentinel-guarded native paths; paused PyCharm, Prefect, and Codex | 2026-08-09 | pending Gate Commit | +| AUTH-011-cache-relocation | Routed supported CLI caches and Pulumi home through sentinel-guarded native paths; paused PyCharm, Prefect, and Codex | 2026-08-09 | `712e31e` | diff --git a/docs/specs/shell_auth_startup/CHANGELOG.md b/docs/specs/shell_auth_startup/CHANGELOG.md index b676f5c..21ebceb 100644 --- a/docs/specs/shell_auth_startup/CHANGELOG.md +++ b/docs/specs/shell_auth_startup/CHANGELOG.md @@ -16,7 +16,7 @@ - Validation: 7 focused tests, Bash/Zsh render syntax, exact chezmoi deployment, Git diff checks, and independent review passed after rollback hardening. Accepted risk: `AUTH-011-AR1`. Release is not applicable. Final Push remains - pending lifecycle completion. + pending lifecycle completion. Implementation Gate Commit: `712e31e`. ## 2026-08-01 From 36cc1f624224cc6ed702ddcd7096a29945933084 Mon Sep 17 00:00:00 2001 From: Tommi Saltiola Date: Sun, 9 Aug 2026 10:20:10 -0600 Subject: [PATCH 3/3] test(shell): skip unavailable shells --- tests/test_terminal_environment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_terminal_environment.py b/tests/test_terminal_environment.py index ba8f524..28bfcc9 100644 --- a/tests/test_terminal_environment.py +++ b/tests/test_terminal_environment.py @@ -71,7 +71,9 @@ def test_external_cache_exports_and_fallback(tmp_path): "PULUMI_HOME": "/Volumes/ext/state/pulumi", } - for shell in ("/bin/bash", "/bin/zsh"): + shells = [shell for shell in ("/bin/bash", "/bin/zsh") if Path(shell).exists()] + assert shells + for shell in shells: sentinel.touch() exported = subprocess.run( [shell, "-c", script + "\nenv"], check=True, capture_output=True, text=True