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.
http://127.0.0.1:3000.127.0.0.1; it must not bind to the LAN or all interfaces.ai-control-chat-ui remains ClusterIP; do not add NodePort, LoadBalancer, or Ingress resources.minikube and the namespace is explicitly aegida-services./usr/local/bin/kubectl.killall, pkill, SIGKILL, or a recursive deletion command.Files:
ops/macos/com.aegida.corp-ui-port-forward.plistk8s/README.mdops/macos/com.aegida.corp-ui-port-forward.plist with plutil and PlistBuddyInterfaces:
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.
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>
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
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` или принудительный сигнал.
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.
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.
Files:
ops/macos/com.aegida.corp-ui-port-forward.plist~/Library/LaunchAgents/com.aegida.corp-ui-port-forward.plist/tmp/aegida-corp-ui-port-forward.stdout.log/tmp/aegida-corp-ui-port-forward.stderr.logInterfaces:
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.
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.
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.
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.
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.