aegida-console / docs / superpowers / specs / 2026-08-04-corp-ui-macos-persistent-access-design.md
2026-08-04-corp-ui-macos-persistent-access-design.md
Raw

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:

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:

/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.