//DOCS PunkBar

macOS menu bar companion for savings and approvals.
GitHub Docs

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 dev gives you one at http://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:

SettingDefaultNotes
Gateway URLhttp://localhost:4100any Punk gateway; applied live
API keyemptysent as Authorization: Bearer; required when the gateway has auth enabled
Poll interval5s5 / 15 / 30s; automatically backs off to 30s while unreachable

Two notes on keys:

  • Approving and rejecting need an admin key (POST /api/v1/keys with "admin": true,
  • or the bootstrap PUNK_API_KEY). With a non-admin key the inbox is read-only and decisions return 403.

  • The key is stored in UserDefaults in v1. Treat it like the convenience credential it
  • 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:

  1. Header — PUNK wordmark, a square connection dot (acid = healthy, red =
  2. unreachable), the gateway URL, and the provider/mode reported by /health.

  3. Savings strip — total saved (the big acid number), total spend, optimized share,
  4. 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.

  5. Approvals inbox — every pending approval, newest first. PROMOTE rows (acid) are
  6. 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.

  7. Recent runs — the last six runs with route badges (artifact = solid acid, caches =
  8. outlined acid, live = alloy, blocked = red), cost, savings, and age.

  9. Learning line — the latest learning-tick summary (synthesized / replays / eligible)
  10. and a TICK button to run the learner on demand.

  11. Footer — Open Dashboard (browser), Copy gateway URL, Settings, Quit.

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

  1. An agent hits a policy rule with requiresApproval: true → the gateway fails closed
  2. (403 punk_approval_required) and opens a pending approval. Or: the learning loop's promotion gate passes and opens an artifact_promotion approval.

  3. PunkBar's menu bar count ticks up within one poll interval.
  4. Click the count → review the row → ✓ APPROVE.
  • A policy exception becomes active immediately for approval_exception_ttl_hours
  • (default 24) — the agent's next identical request passes, with the exception cited in its route explanation.

  • An artifact promotion routes matching traffic through the artifact from that moment.
  1. The decision (who, when, why) is in the audit log, and any configured tenant
  2. 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

SymptomCause / fix
Dashed square in the menu barGateway unreachable — is bun run dev running? Check the URL in settings. PunkBar keeps the last data and retries every 30s.
"unauthorized" in the headerGateway has auth enabled; set an API key in settings.
Approve/Reject returns an errorYour key isn't admin — create one with "admin": true or use the bootstrap PUNK_API_KEY.
App won't open on another MacAd-hoc signature: right-click → Open once, or distribute a Developer-ID-signed build.
No dock icon / can't find the appBy 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.