fix(installer): make Runtime Pack install reversible - #71
Conversation
…ys dead release-rehearsal caught it: the manifest and the wave's test both named scripts/runtime_paths.py, but this line deliberately retired that PEP 562 shim (a2469bb — control-plane sync calls the module; one module object per truth). My earlier manifest addition followed the stale test instead of the repo. Manifest entry and test expectations now agree with the retire decision; the git-drift parametrize row points at a file that exists. Gates: distribution manifest suite 107 passed. Authored-By: claude <agents@vetcoders.io> session_id: 97e159e7-6b2b-466a-95ba-55c33e4c3191 time: 2026-08-24T15:50:01+02:00 runtime: claude-code
…t a build-host name The payload anonymity gate walks from the checkout root up to the first directory every machine has. That list carried /tmp and /private/var but not /private/tmp — the realpath of /tmp on macOS. Building from a scratchpad worktree under /private/tmp therefore poisoned the forbidden literal set with the generic /private/tmp itself, and the gate FATALed on ten committed occurrences of that string (pytest fixtures, keychain examples, tmp-normalization docs) in eight shipped files. Both the in-build assert and release-rehearsal reproduced it on Vibecrafted_4.2.4-20260824-c7485299-portable.tar.gz. - scripts/lib/payload-hygiene.sh: add /private/tmp to _PAYLOAD_HYGIENE_GENERIC_ROOTS; a checkout at /private/tmp/x/repo now forbids /private/tmp/x (still host-specific), never the generic root. - tests/tui/test_payload_hygiene.py: pin both directions — /private/tmp/solo yields no workshop, /private/tmp/scratch/repo yields /private/tmp/scratch. Authored-By: claude <agents@vetcoders.io> session_id: 97e159e7-6b2b-466a-95ba-55c33e4c3191 time: 2026-08-24T15:58:00+02:00 runtime: claude-code
…ld host scratch root The rebuilt tarball at df5e706 passed the generic-root fix and the gate promptly earned its keep: two shipped files still carried the literal /private/tmp/claude-501/... — the vc-aicx skill (EN and PL) documented the background-task output path with the operator's real uid-scoped scratch directory baked in. PLAN_23 (unshipped, but on the deprivatize branch) carried three more /tmp/claude-501/ mentions plus a /Users/tester placeholder the host-paths gate refuses on principle. All now use /tmp/claude-<uid>/<project>/... and ~/vc-workspace/... shapes; loct find --regex 'claude-501' over 1213 of 1213 indexed files returns zero hits. Authored-By: claude <agents@vetcoders.io> session_id: 97e159e7-6b2b-466a-95ba-55c33e4c3191 time: 2026-08-24T16:08:00+02:00 runtime: claude-code
… Swift 6 strict concurrency First `make dmg` after the fp-wave merge was also the first time grok's native-notifications code (0a398d0) met the app build gate: Swift 6 refuses `static let shared` on a non-Sendable class (#MutableGlobalVariable), killing the DMG leg at SwiftCompile. @mainactor would be a lie — handleIpcEvent fires on whatever thread posts IpcEvent from the FFI. The house idiom for callback carriers is already in this AppDelegate (EventObserver: @unchecked Sendable), and the invariant holds: `started` and `presentWindow` are written only on the main thread during launch; cross-thread callbacks read, never mutate. Declared @unchecked Sendable with that invariant documented on the class. Authored-By: claude <agents@vetcoders.io> session_id: 97e159e7-6b2b-466a-95ba-55c33e4c3191 time: 2026-08-24T16:40:00+02:00 runtime: claude-code
Moves native App hydration behind the same receipt-owning installer used by CLI and source channels. The signed Runtime Pack now carries the installer closure, records exact owned files and backups, and rejects conflicts or tampered receipt paths before teardown. The App remains an optional transport overlay: uninstall preserves the application while removing only proven runtime, config, state, cache, and launcher ownership so first-run testing can begin again. Authored-By: codex <agents@vetcoders.io> session_id: 01a03396-782e-7703-93b5-84e7659ddb28 time: 2026-08-24T21:08:57+02:00 runtime: codex
There was a problem hiding this comment.
Pull request overview
This PR makes the signed/offline “Runtime Pack” install path reversible by embedding the shared installer (and its import closure) into the Runtime Pack, delegating native app runtime hydration/reset to that installer, and switching uninstall to a receipt-driven, fail-closed workflow.
Changes:
- Embed
scripts/vetcoders_install.py+ minimal import closure inside the Runtime Pack and include them in required-hash/product-contract checks. - Add
runtime-install/runtime-uninstallmodes to the shared installer with a hashed ownership receipt and pre-teardown drift/tamper checks. - Update the macOS app to call the embedded installer for install/uninstall instead of implementing install logic in Swift; adjust tests/docs to match.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vibecrafted-core/vibecrafted_core/skills/vc-aicx/SKILL.md | Update tmp-path example to /private/tmp/claude-<uid>/… form. |
| vibecrafted-core/vibecrafted_core/skills/pl/vc-aicx/SKILL.md | Polish mirror of the same tmp-path example update. |
| vibecrafted-core/vibecrafted_core/product_contract.py | Expand runtime required-hash set to include installer/import-closure scripts. |
| vibecrafted-core/vibecrafted_core/deck/vibecrafted | Make uninstall prefer the Runtime Pack’s bundled Python/installer when available. |
| vibecrafted-core/tests/test_runtime_receipt.py | Update runtime receipt fixture/hash-count expectations for new required files. |
| vibecrafted-app/shell-agent/app/Vibecrafted/NotificationManager.swift | Mark as @unchecked Sendable with documentation of threading assumptions. |
| vibecrafted-app/shell-agent/app/Vibecrafted/AppDelegate.swift | Delegate runtime install/uninstall to embedded installer; add --uninstall flag handling. |
| vibecrafted-app/docs/plans/PLAN_23_AGENT_OPERATOR_DASHBOARD.md | Normalize repo/tmp path literals away from user-specific values. |
| tests/tui/test_uv_bootstrap.py | Add scripts/installer_brand.py to required-file checks. |
| tests/tui/test_unified_app_contract.py | Update assertions to ensure AppDelegate uses embedded installer; validate embedded script presence. |
| tests/tui/test_release_contract.py | Shift terminal-policy/entry assertions from AppDelegate to installer script. |
| tests/tui/test_payload_hygiene.py | Add coverage for /private/tmp handling as /tmp realpath on macOS. |
| tests/tui/test_keys.py | Switch wrapper-verb parity check from Swift parsing to installer constant _RUNTIME_WRAPPER_VERBS. |
| tests/tui/test_installer_uninstall.py | Add round-trip install/uninstall tests and fail-closed conflict/tamper coverage for receipt mode. |
| tests/tui/test_installer_doctor.py | Expand runtime-generation fixture sources to include embedded installer/import-closure scripts. |
| tests/tui/test_install_bootstrap.py | Add scripts/installer_brand.py to required-file checks. |
| tests/tui/test_distribution_manifest.py | Update included payload list and mutation tests to reflect new required scripts. |
| scripts/vibecrafted | Same change as deck copy: prefer bundled installer Python when uninstalling from a Runtime Pack generation. |
| scripts/vetcoders_install.py | Implement receipt-based runtime-install/runtime-uninstall, add required-hash entries, and integrate runtime-uninstall into legacy uninstall. |
| scripts/lib/payload-hygiene.sh | Treat /private/tmp as a generic root for topmost-host-root logic on macOS. |
| scripts/distribution_manifest.py | Include scripts/installer_brand.py in distribution include list. |
| scripts/build-vibecrafted-release.sh | Install embedded installer/import-closure scripts into the Runtime Pack at build time. |
| install.sh | Add scripts/installer_brand.py to the bootstrap required-file set. |
| docs/installer/REQUIRED-SET.md | Update docs to distinguish receipt-based Runtime Pack uninstall vs legacy discovery uninstall and list new required surfaces. |
Suppressed comments (1)
scripts/vetcoders_install.py:14625
runtime-uninstalldrift detection skips any owned path that has been replaced with a symlink (or other non-regular file), so an attacker or accidental change can bypass the “fail closed before teardown” check. Treat any existing owned path that is a symlink or not a regular file as a conflict.
conflicts = [
raw_path
for raw_path, installed_hash in sorted(owned_files.items())
if (path := Path(raw_path)).is_file()
and not path.is_symlink()
and _sha256_path(path) != installed_hash
]
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| terminal_theme = product_config / "terminal-theme.toml" | ||
| if not terminal_theme.exists(): | ||
| shutil.copy2(generation / "config/vc-terminal/themes/dark.toml", terminal_theme) | ||
| receipt["owned_dirs"].append(str(terminal_theme)) | ||
| terminal_policy = generation / "config/vc-terminal/vibecrafted.toml" |
| var environment = ProcessInfo.processInfo.environment | ||
| environment["PYTHONNOUSERSITE"] = "1" | ||
| environment["PYTHONDONTWRITEBYTECODE"] = "1" | ||
| process.environment = environment |
Blacky runtime verification proved the previous teardown could delete the generation while App, terminal, shell, and vc-frame processes kept running on unlinked files. Census canonical product executables and runtime shell scripts by stable Darwin birth identity and argv, exclude the installer ancestry, and re-prove each PID before signaling. Treat an unlocked persistent supervisor.lock as historical state rather than a live service, while retaining fail-closed behavior for held, foreign, unreadable, or symlinked locks. macOS 27 remote-login EPERM uses a strict absolute-ps fallback only for the conservative ancestor exclusion set. Authored-By: codex <agents@vetcoders.io> session_id: 01a03396-782e-7703-93b5-84e7659ddb28 time: 2026-08-24T21:40:14+02:00 runtime: codex
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
scripts/vetcoders_install.py:14803
- Conflict detection for
owned_filesignores cases where an owned file has been replaced with a symlink or a non-regular file. That’s still local modification and should fail closed before teardown; otherwise uninstall will silently unlink the symlink and proceed.
if (path := Path(raw_path)).is_file()
and not path.is_symlink()
and _sha256_path(path) != installed_hash
]
scripts/vetcoders_install.py:14590
terminal-theme.tomlis a regular file but it’s being recorded underowned_dirs, so it won’t be hash-verified for drift and can be removed without triggering the conflict fail-closed behavior. Record it as anowned_filesentry (hash) instead.
terminal_theme = product_config / "terminal-theme.toml"
if not terminal_theme.exists():
shutil.copy2(generation / "config/vc-terminal/themes/dark.toml", terminal_theme)
receipt["owned_dirs"].append(str(terminal_theme))
terminal_policy = generation / "config/vc-terminal/vibecrafted.toml"
Decode installer paths as filesystem URLs so AppKit can spawn the bundled terminal. Resolve server binaries, site assets, and launchd environment from the active runtime generation. Authored-By: codex <agents@vetcoders.io> session_id: 01a03396-78e0-74e0-94c2-9adc16febc5c time: 2026-08-24T22:15:54+02:00 runtime: interactive
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (1)
scripts/vetcoders_install.py:14589
terminal-theme.tomlis recorded underowned_dirs(and not hashed), soruntime-uninstallwill not detect local edits to this managed file before teardown/removal. This breaks the “fail closed on locally modified managed files” guarantee and can silently delete operator changes to the theme.
terminal_theme = product_config / "terminal-theme.toml"
if not terminal_theme.exists():
shutil.copy2(generation / "config/vc-terminal/themes/dark.toml", terminal_theme)
receipt["owned_dirs"].append(str(terminal_theme))
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scripts/vetcoders_install.py:14589
terminal-theme.tomlis being tracked inowned_dirs, so it is not hashed and won’t be included in the modified-file conflict check duringruntime-uninstall. If the operator edits the theme after install, uninstall can silently delete it instead of failing closed as intended for receipted ownership.
terminal_theme = product_config / "terminal-theme.toml"
if not terminal_theme.exists():
shutil.copy2(generation / "config/vc-terminal/themes/dark.toml", terminal_theme)
receipt["owned_dirs"].append(str(terminal_theme))
vibecrafted-app/shell-agent/app/Vibecrafted/AppDelegate.swift:356
- The app runs the bundled installer under the inherited process environment. To reduce the risk of Python environment injection (e.g.,
PYTHONPATH/startup hooks) when executing a signed installer, run the bundled interpreter in isolated mode.
let process = Process()
let output = Pipe()
let errors = Pipe()
process.executableURL = python
process.arguments = [installer.path] + arguments
var environment = ProcessInfo.processInfo.environment
environment["PYTHONNOUSERSITE"] = "1"
environment["PYTHONDONTWRITEBYTECODE"] = "1"
process.environment = environment
Projects one active release into the CLI, App, and agent discovery views while preserving operator collisions through a checkpointed receipt. Adds deterministic interrupted-install reset and refuses drift, injected receipt paths, or symlinked projection ancestors. Exposes live server health, reconcile, diagnostics, About, and Help in the native tray. Authored-By: codex <agents@vetcoders.io> session_id: 01a03595-0d19-7943-af2a-0a5eff007ac3 time: 2026-08-24T23:37:36+02:00 runtime: vc-terminal
Makes project.yml the sole Xcode project authority, ignores the generated .xcodeproj, and makes the release fail closed if generated output is tracked again. This removes XcodeGen-version churn that dirtied the source tree after a successful build and blocked provenance signing. Authored-By: codex <agents@vetcoders.io> session_id: 01a03595-0d19-7943-af2a-0a5eff007ac3 time: 2026-08-24T23:56:02+02:00 runtime: vc-terminal
Stages pinned Loctree, AICX, PRView, and ScreenScribe payloads into the immutable Runtime Pack and rejects incomplete packs before launcher publication. Authored-By: codex <agents@vetcoders.io> session_id: 01a03595-0d19-7943-af2a-0a5eff007ac3 time: 2026-08-25T00:36:00+02:00 runtime: vc-terminal
Pins AICX v0.12.5 to commit ced57997 and applies Rust plus native compiler path remaps because the published release binaries retain their CI builder home. Authored-By: codex <agents@vetcoders.io> session_id: 01a03595-0d19-7943-af2a-0a5eff007ac3 time: 2026-08-25T00:57:00+02:00 runtime: vc-terminal
Build PRView against static OpenSSL on macOS and reject carrier binaries that retain non-system dynamic library dependencies. This keeps the mandatory PRView foundation runnable on clean customer machines. Authored-By: codex <agents@vetcoders.io> session_id: 01a03595-0d19-7943-af2a-0a5eff007ac3 time: 2026-08-25T01:27:00+02:00 runtime: vc-terminal
Publishes one manifest-bound Runtime Pack, canonicalizes vc-frame configuration, and makes doctor validate the active generation rather than legacy path projections. Authored-By: codex <agents@vetcoders.io> session_id: 01a03595-0d19-7943-af2a-0a5eff007ac3 time: 2026-08-25T02:43:10+02:00 runtime: vc-terminal
Ship a dedicated Agents workspace tab with New agent and voc doors. Keep interactive init/resume inside the current vc-frame tab and project live Agent faces from vc-frame pane truth. Authored-By: codex <agents@vetcoders.io> session_id: 01a03595-0d19-7943-af2a-0a5eff007ac3 time: 2026-08-25T04:57:47+02:00 runtime: vc-terminal
Resolves the installed adjacent libexec binary before ambient candidates and rejects shell wrappers as native vc-frame providers. Adds executable regressions for the installed layout and recursive data-root shadow. Authored-By: codex <agents@vetcoders.io> session_id: 01a03396-78e0-74e0-94c2-9adc16febc5c time: 2026-08-25T05:24:01+02:00 runtime: vc-terminal
Treat cleanup of the disposable Foundations staging tree as best-effort so a macOS metadata race cannot replace a successful carrier build with ENOTEMPTY. Lock the release contract with a focused regression assertion. Authored-By: codex <agents@vetcoders.io> session_id: 01a03396-782e-7703-93b5-84e7659ddb28 time: 2026-08-25T05:41:42+02:00 runtime: vc-terminal
Make spawn.py the single provider policy normalizer used by headless launches, interactive CLI entrypoints, and Agent Workspaces. The complete provider/runtime/permission matrix fails closed on unsupported semantics and reports host capability separately. Authored-By: codex <agents@vetcoders.io> session_id: 01a036f1-7603-7b43-a4d7-021030c00153 time: 2026-08-25T05:50:00+02:00 runtime: headless
Rename the tray actions to the product-facing VC names, keep server status first, and route both Quit actions through a control-plane active-lane warning including worktree-backed runs. Authored-By: codex <agents@vetcoders.io> session_id: 01a036f1-7603-7b43-a4d7-021030c00153 time: 2026-08-25T05:52:00+02:00 runtime: headless
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 59 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- vibecrafted-app/shell-agent/app/Vibecrafted.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
| def configure(self) -> None: | ||
| curses.curs_set(0) | ||
| curses.noecho() | ||
| curses.cbreak() | ||
| self.window.keypad(True) | ||
| self.window.timeout(500) |
Stacked on #70. Do not merge before the base PR.
Summary
Verification so far
Blacky fresh-onboarding runtime verification remains in progress; final Linux tarball smoke and PR-check observation will follow after implementation stabilizes.