# Persistent macOS Access for corp-ui Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Keep Minikube corp-ui available on this Mac at `http://127.0.0.1:3000` through a per-user LaunchAgent instead of a manually managed terminal process. **Architecture:** Keep the Kubernetes Service private as `ClusterIP`. A checked-in macOS LaunchAgent runs an explicit, loopback-only `kubectl port-forward` against the `minikube` context and lets `launchd` restart it after login, pod replacement, or transient cluster outages. **Tech Stack:** macOS `launchd`, property lists, `/usr/local/bin/kubectl`, Minikube, Kubernetes Service port-forwarding, `plutil`, `launchctl`, `curl`. ## Global Constraints - The public workstation URL is exactly `http://127.0.0.1:3000`. - The listener binds only to `127.0.0.1`; it must not bind to the LAN or all interfaces. - The Kubernetes Service `ai-control-chat-ui` remains `ClusterIP`; do not add `NodePort`, `LoadBalancer`, or Ingress resources. - The active Kubernetes context is explicitly `minikube` and the namespace is explicitly `aegida-services`. - The executable path on this Mac is exactly `/usr/local/bin/kubectl`. - Do not recreate or delete the Minikube cluster or any persistent volume. - Replace an existing port-3000 listener only when exactly one PID is present and its complete command is the known manual corp-ui port-forward. - Never use broad process matching, `killall`, `pkill`, `SIGKILL`, or a recursive deletion command. --- ### Task 1: Version the LaunchAgent and operator documentation **Files:** - Create: `ops/macos/com.aegida.corp-ui-port-forward.plist` - Modify: `k8s/README.md` - Test: `ops/macos/com.aegida.corp-ui-port-forward.plist` with `plutil` and `PlistBuddy` **Interfaces:** - Consumes: Kubernetes Service `ai-control-chat-ui` in context `minikube`, namespace `aegida-services`, Service port `3000`. - Produces: LaunchAgent label `com.aegida.corp-ui-port-forward`, loopback listener `127.0.0.1:3000`, and exact install/status/restart/uninstall commands for Task 2. - [ ] **Step 1: Run the precondition check and confirm the configuration is absent** Run: ```bash test -f ops/macos/com.aegida.corp-ui-port-forward.plist ``` Expected: exit code 1 because the LaunchAgent configuration does not exist yet. - [ ] **Step 2: Create the LaunchAgent property list** Create `ops/macos/com.aegida.corp-ui-port-forward.plist` with exactly: ```xml Label com.aegida.corp-ui-port-forward ProgramArguments /usr/local/bin/kubectl --context=minikube --namespace=aegida-services port-forward --address=127.0.0.1 service/ai-control-chat-ui 3000:3000 RunAtLoad KeepAlive ThrottleInterval 10 ProcessType Background StandardOutPath /tmp/aegida-corp-ui-port-forward.stdout.log StandardErrorPath /tmp/aegida-corp-ui-port-forward.stderr.log ``` - [ ] **Step 3: Validate the plist schema and exact safety-critical fields** Run: ```bash plutil -lint ops/macos/com.aegida.corp-ui-port-forward.plist /usr/libexec/PlistBuddy -c 'Print :Label' ops/macos/com.aegida.corp-ui-port-forward.plist /usr/libexec/PlistBuddy -c 'Print :ProgramArguments:4' ops/macos/com.aegida.corp-ui-port-forward.plist /usr/libexec/PlistBuddy -c 'Print :ProgramArguments:5' ops/macos/com.aegida.corp-ui-port-forward.plist /usr/libexec/PlistBuddy -c 'Print :ProgramArguments:6' ops/macos/com.aegida.corp-ui-port-forward.plist /usr/libexec/PlistBuddy -c 'Print :ThrottleInterval' ops/macos/com.aegida.corp-ui-port-forward.plist ``` Expected output contains, in order: ```text ops/macos/com.aegida.corp-ui-port-forward.plist: OK com.aegida.corp-ui-port-forward --address=127.0.0.1 service/ai-control-chat-ui 3000:3000 10 ``` - [ ] **Step 4: Replace the manual-only access section in the Kubernetes README** In `k8s/README.md`, replace the current port-forward-only access paragraph and command with this section: ````markdown ## Постоянный доступ с macOS UI остаётся приватным `ClusterIP` Service. На macOS с Docker-драйвером Minikube постоянный адрес `http://127.0.0.1:3000` поддерживает per-user LaunchAgent: ```bash mkdir -p "$HOME/Library/LaunchAgents" install -m 0644 ops/macos/com.aegida.corp-ui-port-forward.plist \ "$HOME/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist" launchctl bootstrap "gui/$(id -u)" \ "$HOME/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist" launchctl kickstart -k "gui/$(id -u)/com.aegida.corp-ui-port-forward" ``` Статус и health-check: ```bash launchctl print "gui/$(id -u)/com.aegida.corp-ui-port-forward" curl --fail-with-body http://127.0.0.1:3000/api/health ``` Логи: ```bash tail -n 100 /tmp/aegida-corp-ui-port-forward.stdout.log tail -n 100 /tmp/aegida-corp-ui-port-forward.stderr.log ``` Перезапуск: ```bash launchctl kickstart -k "gui/$(id -u)/com.aegida.corp-ui-port-forward" ``` Отключение и удаление установленного LaunchAgent: ```bash launchctl bootout "gui/$(id -u)" \ "$HOME/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist" rm "$HOME/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist" ``` Перед установкой освободите порт 3000 только после проверки владельца процесса. Не используйте `pkill`, `killall` или принудительный сигнал. ```` - [ ] **Step 5: Verify documentation and configuration together** Run: ```bash plutil -lint ops/macos/com.aegida.corp-ui-port-forward.plist rg -n '127\.0\.0\.1:3000|com\.aegida\.corp-ui-port-forward|launchctl bootstrap|launchctl bootout' k8s/README.md git diff --check ``` Expected: plist validation succeeds, README contains install and rollback commands, and `git diff --check` exits 0. - [ ] **Step 6: Commit the versioned configuration** Run: ```bash git add ops/macos/com.aegida.corp-ui-port-forward.plist k8s/README.md git commit -m "ops: keep corp ui available on macos" ``` Expected: one commit containing only the plist and README changes. --- ### Task 2: Install the LaunchAgent and safely replace the manual listener **Files:** - Read: `ops/macos/com.aegida.corp-ui-port-forward.plist` - Create outside repository: `~/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist` - Read for verification: `/tmp/aegida-corp-ui-port-forward.stdout.log` - Read for verification: `/tmp/aegida-corp-ui-port-forward.stderr.log` **Interfaces:** - Consumes: the validated plist from Task 1, the current GUI launchd domain `gui/501`, and the ready Kubernetes Service `ai-control-chat-ui`. - Produces: a LaunchAgent-managed listener on `127.0.0.1:3000` and health-checked browser access at `http://127.0.0.1:3000`. - [ ] **Step 1: Inspect the current listener and Kubernetes dependency without changing state** Run: ```bash /usr/sbin/lsof -nP -iTCP:3000 -sTCP:LISTEN kubectl --context=minikube -n aegida-services get deployment/ai-control-chat-ui service/ai-control-chat-ui kubectl --context=minikube -n aegida-services get endpointslice \ -l kubernetes.io/service-name=ai-control-chat-ui ``` Expected: port 3000 has either no listener or exactly one unique `kubectl` PID (it may own separate IPv4 and IPv6 sockets); the Deployment is `1/1` ready and the Service endpoint is ready. - [ ] **Step 2: Terminate only a verified manual corp-ui port-forward** Run this complete script in one foreground shell so every value is resolved and validated before `SIGTERM`: ```bash set -euo pipefail listener_pids="$(/usr/sbin/lsof -nP -iTCP:3000 -sTCP:LISTEN -t 2>/dev/null | /usr/bin/sort -u || true)" if [[ -n "$listener_pids" ]]; then if [[ ! "$listener_pids" =~ ^[0-9]+$ ]]; then print -u2 "port 3000 does not have exactly one numeric listener PID" exit 1 fi listener_pid="${listener_pids:?listener PID is required}" listener_command="$(/bin/ps -p "$listener_pid" -o command=)" expected_command='/Users/balamutik/.minikube/cache/darwin/arm64/v1.35.1/kubectl --cluster=minikube -n aegida-services port-forward service/ai-control-chat-ui 3000:3000' if [[ "$listener_command" != "$expected_command" ]]; then print -u2 "port 3000 belongs to an unexpected process" exit 1 fi /bin/kill -TERM "$listener_pid" for attempt in {1..20}; do if ! /bin/kill -0 "$listener_pid" 2>/dev/null; then break fi /bin/sleep 0.25 done if /bin/kill -0 "$listener_pid" 2>/dev/null; then print -u2 "verified manual port-forward did not stop after SIGTERM" exit 1 fi fi if /usr/sbin/lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1; then print -u2 "port 3000 is still occupied" exit 1 fi ``` Expected: the known manual port-forward exits after `SIGTERM`, or the step is a no-op when no listener exists; port 3000 is free afterward. - [ ] **Step 3: Validate and install the exact LaunchAgent plist** Run: ```bash set -euo pipefail source_plist="$PWD/ops/macos/com.aegida.corp-ui-port-forward.plist" target_directory="$HOME/Library/LaunchAgents" target_plist="$target_directory/com.aegida.corp-ui-port-forward.plist" test -f "${source_plist:?source plist is required}" plutil -lint "$source_plist" /bin/mkdir -p "$target_directory" if [[ -L "$target_plist" ]]; then print -u2 "refusing to replace a symbolic-link LaunchAgent target" exit 1 fi if [[ -e "$target_plist" ]] && ! /usr/bin/cmp -s "$source_plist" "$target_plist"; then print -u2 "installed LaunchAgent differs from the reviewed source" exit 1 fi /usr/bin/install -m 0644 "$source_plist" "$target_plist" plutil -lint "$target_plist" ``` Expected: both plist validations report `OK`; the installed target is a regular file identical to the repository source. - [ ] **Step 4: Bootstrap and start the per-user job** Run: ```bash set -euo pipefail launch_domain='gui/501' launch_label='com.aegida.corp-ui-port-forward' target_plist="$HOME/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist" if launchctl print "$launch_domain/$launch_label" >/dev/null 2>&1; then launchctl bootout "$launch_domain/$launch_label" fi launchctl bootstrap "$launch_domain" "${target_plist:?installed plist is required}" launchctl kickstart -k "$launch_domain/$launch_label" ``` Expected: `bootstrap` and `kickstart` exit 0. Only the exact per-user label is unloaded if it was already registered. - [ ] **Step 5: Verify launchd ownership, loopback binding, and HTTP health** Run: ```bash launchctl print gui/501/com.aegida.corp-ui-port-forward /usr/sbin/lsof -nP -iTCP:3000 -sTCP:LISTEN curl --fail-with-body --show-error http://127.0.0.1:3000/api/health ``` Expected: - `launchctl print` reports `state = running` and `/usr/local/bin/kubectl` as the program; - exactly one listener is present at `127.0.0.1:3000` and none at `0.0.0.0:3000`; - curl prints `{"status":"ok"}` and exits 0. - [ ] **Step 6: Verify Kubernetes remained healthy and review agent logs** Run: ```bash kubectl --context=minikube -n aegida-services get deployment/ai-control-chat-ui \ -o custom-columns='NAME:.metadata.name,READY:.status.readyReplicas,DESIRED:.spec.replicas,AVAILABLE:.status.availableReplicas' kubectl --context=minikube -n aegida-services get pod -l app=ai-control-chat-ui \ -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount' tail -n 100 /tmp/aegida-corp-ui-port-forward.stdout.log tail -n 100 /tmp/aegida-corp-ui-port-forward.stderr.log git status --short ``` Expected: Deployment is `1/1`, pod readiness is `true`, restart count is `0`, logs contain forwarding lifecycle messages without bind failures, and the repository working tree is clean.