PunkBar — Punk in the macOS menu bar
PunkBar is the native macOS companion to the Punk gateway. It lives in the menu bar, polls the gateway's REST API, and puts the high-frequency operator actions one click away — above all deciding approvals, which is faster here than anywhere else in the product. It is a pure API client: it never touches the database, so it can point at any Punk gateway you can reach over HTTP (local, staging, or production).
No dock icon, no window clutter. When nothing needs you, it is a small square in the menu bar. When an approval is waiting, the square grows a count.
Requirements
- macOS 14 or newer.
- To run a built
Punk.app: nothing else. - To build: the Swift 6 toolchain (Xcode or the Command Line Tools).
- A reachable Punk gateway (
bun run devgives you one athttp://localhost:4100).
Install / build
# from the repo root — builds release and assembles the app bundle
bun run menubar
open apps/menubar/dist/Punk.app
# optional: keep it around like a real app
cp -R apps/menubar/dist/Punk.app /Applications/
# development loop (live code, no bundle)
cd apps/menubar && swift run
The bundle is ad-hoc signed. It runs without ceremony on the machine that built it; if you copy it to another Mac, Gatekeeper will object the first time — right-click the app and choose Open once, or sign it with a Developer ID before distributing.
To launch it at login: System Settings → General → Login Items → add Punk.app.
First run
PunkBar starts pointed at http://localhost:4100 with no API key — the right defaults for a local bun run dev gateway in open dev mode. The menu bar square is solid when the gateway is healthy and dashed when it is unreachable.
To point it elsewhere, open the popover → gear icon:
| Setting | Default | Notes |
|---|---|---|
| Gateway URL | http://localhost:4100 | any Punk gateway; applied live |
| API key | empty | sent as Authorization: Bearer; required when the gateway has auth enabled |
| Poll interval | 5s | 5 / 15 / 30s; automatically backs off to 30s while unreachable |
Two notes on keys:
- Approving and rejecting need an admin key (
POST /api/v1/keyswith"admin": true, - The key is stored in UserDefaults in v1. Treat it like the convenience credential it
or the bootstrap PUNK_API_KEY). With a non-admin key the inbox is read-only and decisions return 403.
is — Keychain storage is the production follow-up.
A 401 shows an "unauthorized — set API key in settings" state in the header rather than failing silently.
What the popover shows
Top to bottom:
- Header — PUNK wordmark, a square connection dot (acid = healthy, red =
- Savings strip — total saved (the big acid number), total spend, optimized share,
- Approvals inbox — every pending approval, newest first.
PROMOTErows (acid) are - Recent runs — the last six runs with route badges (artifact = solid acid, caches =
- Learning line — the latest learning-tick summary (synthesized / replays / eligible)
- Footer — Open Dashboard (browser), Copy gateway URL, Settings, Quit.
unreachable), the gateway URL, and the provider/mode reported by /health.
and — when any tenant traffic runs in observe mode — ghost savings: what Punk would have saved, with the reminder to flip the key to optimize to collect it.
artifact promotions waiting for sign-off; POLICY rows (blue) are agents asking for a time-limited exception to an approval_required rule. Each row shows the subject, the requesting agent/action, and its age, with instant ✓ APPROVE / ✗ REJECT buttons. There is deliberately no confirmation dialog — speed is the point; decisions land in the audit log either way, and a wrong promotion is one Rollback away in the dashboard.
outlined acid, live = alloy, blocked = red), cost, savings, and age.
and a TICK button to run the learner on demand.
The menu bar label itself is the at-a-glance signal: a plain square means "healthy, nothing pending"; ▪ 2 means two approvals are waiting on you.
The approval flow, end to end
- An agent hits a policy rule with
requiresApproval: true→ the gateway fails closed - PunkBar's menu bar count ticks up within one poll interval.
- Click the count → review the row → ✓ APPROVE.
(403 punk_approval_required) and opens a pending approval. Or: the learning loop's promotion gate passes and opens an artifact_promotion approval.
- A policy exception becomes active immediately for
approval_exception_ttl_hours - An artifact promotion routes matching traffic through the artifact from that moment.
(default 24) — the agent's next identical request passes, with the exception cited in its route explanation.
- The decision (who, when, why) is in the audit log, and any configured tenant
webhook_url receives the signed approval.decided event.
Headless probe (CI / scripting / sanity checks)
The binary doubles as an API smoke tester:
cd apps/menubar
swift run PunkBar --probe http://localhost:4100 # PUNK_API_KEY env honored
It fetches /health, savings, pending approvals, recent runs, and the learning report through the exact same Codable decoding layer the UI uses, prints a summary, and exits 0/1 — if the probe passes, the app will render.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Dashed square in the menu bar | Gateway unreachable — is bun run dev running? Check the URL in settings. PunkBar keeps the last data and retries every 30s. |
| "unauthorized" in the header | Gateway has auth enabled; set an API key in settings. |
| Approve/Reject returns an error | Your key isn't admin — create one with "admin": true or use the bootstrap PUNK_API_KEY. |
| App won't open on another Mac | Ad-hoc signature: right-click → Open once, or distribute a Developer-ID-signed build. |
| No dock icon / can't find the app | By design (LSUIElement) — it only exists in the menu bar. Quit from the popover footer. |
Uninstall
Quit from the popover footer, delete Punk.app, and (optionally) clear its settings: defaults delete com.punk.menubar.