# Persistent localhost access for corp-ui on macOS ## Goal Keep the Minikube `corp-ui` available on this Mac at `http://127.0.0.1:3000` without requiring a manually started terminal command. Access remains workstation-local and is not exposed to the LAN or public internet. ## Environment and constraint The active Minikube profile uses the Docker driver on macOS. A control connection to the existing `user-service` NodePort timed out from the host, confirming that a Kubernetes `NodePort` alone is not host-reachable in this profile. Docker port mappings cannot be added to the running Minikube container, so making NodePort host-reachable would require recreating the cluster and its persistent volumes. The current listener on port 3000 is a manual process: ```text kubectl --cluster=minikube -n aegida-services port-forward service/ai-control-chat-ui 3000:3000 ``` The persistent configuration will replace this process without changing the Minikube topology. ## Design decision Keep `ai-control-chat-ui` as a `ClusterIP` Service and manage a fixed `kubectl port-forward` with a per-user macOS LaunchAgent. This avoids cluster recreation, binds only to loopback, and lets `launchd` restore access after login, pod replacement, or a transient Minikube outage. The LaunchAgent will run the equivalent of: ```bash /usr/local/bin/kubectl \ --context=minikube \ --namespace=aegida-services \ port-forward \ --address=127.0.0.1 \ service/ai-control-chat-ui \ 3000:3000 ``` The fixed executable path matches this Mac. The explicit context, namespace, address, Service, and port prevent the agent from silently targeting a different cluster or exposing the listener beyond localhost. ## Configuration artifacts Add `ops/macos/com.aegida.corp-ui-port-forward.plist` with: - label `com.aegida.corp-ui-port-forward`; - the exact command and arguments above; - `RunAtLoad` enabled; - `KeepAlive` enabled so a lost pod connection or unavailable cluster is retried; - a 10-second throttle between restart attempts; - stdout and stderr paths under `/tmp` that contain only `kubectl port-forward` lifecycle messages. Update `k8s/README.md` with installation, status, verification, log, restart, and uninstall commands. The checked-in plist is copied to `~/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist` and loaded into the current GUI domain with `launchctl bootstrap gui/501`. No Kubernetes Service or Deployment change is required. Keeping the Service private preserves the existing security boundary. ## Safe transition from the manual process Before changing process state, resolve the listener on TCP port 3000 and inspect its complete command. Continue only when exactly one listener exists and it matches the known manual `kubectl port-forward` for `ai-control-chat-ui`. If the listener is absent, installation continues. If the port belongs to another process or the command differs, stop and request user direction. For the matching manual listener, send `SIGTERM` to that exact PID and confirm port 3000 is released. Validate the plist with `plutil -lint`, copy it to the per-user LaunchAgents directory, bootstrap it, and use `launchctl kickstart` to start it immediately. Do not use broad process matching or force-kill signals. ## Runtime and error behavior The LaunchAgent depends on Minikube being running and on the `ai-control-chat-ui` Service having a ready endpoint. When either dependency is unavailable, `kubectl` exits and `launchd` retries after the throttle interval. When the pod is replaced, the active port-forward exits and is re-established automatically against the Service. If port 3000 is occupied by an unrelated process after installation, the agent remains failed/retrying and records the bind error. Operators resolve the conflicting listener and kickstart the agent; the configuration never chooses a different port silently. ## Verification Installation is successful only when all of the following hold: - `plutil -lint` accepts both the repository and installed plist; - `launchctl print gui/501/com.aegida.corp-ui-port-forward` reports a running job; - exactly one process listens on `127.0.0.1:3000` and its command belongs to the LaunchAgent-managed `kubectl`; - `curl --fail http://127.0.0.1:3000/api/health` returns `{"status":"ok"}` with HTTP 200; - the `corp-ui` Deployment remains `1/1` ready with zero container restarts; - the repository working tree contains only the intended plist and documentation changes. ## Rollback Unload the job with `launchctl bootout gui/501 ~/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist`, then remove that exact installed plist. This stops permanent localhost exposure without changing or deleting any Kubernetes resource. A one-off manual port-forward remains available as a fallback.