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

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:

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 version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.aegida.corp-ui-port-forward</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/kubectl</string>
    <string>--context=minikube</string>
    <string>--namespace=aegida-services</string>
    <string>port-forward</string>
    <string>--address=127.0.0.1</string>
    <string>service/ai-control-chat-ui</string>
    <string>3000:3000</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>ThrottleInterval</key>
  <integer>10</integer>
  <key>ProcessType</key>
  <string>Background</string>
  <key>StandardOutPath</key>
  <string>/tmp/aegida-corp-ui-port-forward.stdout.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/aegida-corp-ui-port-forward.stderr.log</string>
</dict>
</plist>
  • Step 3: Validate the plist schema and exact safety-critical fields

Run:

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:

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:

## Постоянный доступ с 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:

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:

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:

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

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:

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:

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:

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:

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.