TL;DR
- Canonical HS auth = CSRF-via-Chrome. PAT slot in Keychain is empty.
- Why PAT isn't an option: Joseph's HS user lacks Super Admin permission in the SkyRun portal (
23273108). Confirmed 2026-05-13: navigating tohttps://app.hubspot.com/private-apps/23273108displays "You don't have permission to access private apps. Contact your HubSpot admin to get access." This was verified via chrome_bridge and is also true through the Settings→Integrations route. - Lindsay path is foreclosed per
feedback_never_ask_lindsay.md. HS admin asks are not routable to her — this is a hard constraint. - Operational implication: Chrome must be open with the SkyRun HubSpot tab signed in. The session_keep_alive prober (every 5 min) protects this and ntfys Joseph if it drops.
The fortified CSRF path (2026-05-13)
Three layers of defense replaced what was previously a "hope it works" path:
Layer 1 — HS in the keep-alive prober fleet
session_keep_alive.pynow has anAPI_SOURCESarray with HubSpot wired throughhs_api_client.hs_auth_healthy().- Every 5 min, the prober fires a real 1-call HS probe.
- If HS flips from
alive→expiredorno_tab, an ntfy push fires: "⚠ HubSpot (CSRF/PAT) session expired". - HubSpot now shows up on the PWA Sources page next to Track / KeyData / SmartLead / BeenVerified.
Layer 2 — hs_auth_healthy() pre-flight in reconcilers
Three skills now call hs_auth_healthy() at the top of main() / reconcile():
sot_reconciler.py— aborts cleanly withaudit["fatal"]=True+errors=[{step:hs_auth_preflight, requires_operator_action:true}]engagement_reconciler.py— writes heartbeatstatus=partial, requires_operator_action=trueand returnsdeal_stage_reconciler.py— writes heartbeat via_write_heartbeat("partial", ...)and returns 0
Before this patch, a dead HS auth meant the reconcilers would load SoT, build Chrome state, then fail on the first HS call — burning ~30+ seconds before surfacing the issue. Now they abort within ~3 seconds and the heartbeat carries the right operator-action flag.
Layer 3 — CSRF retry-on-401 with tab refresh
hs_api_client._csrf_request now has a _retry_on_401 flag (default True). If the first call returns 401:
1. Force the HS tab to reload via window.location.reload()
2. Wait for document.readyState === 'complete'
3. Re-fire the call exactly once with _retry_on_401=False (bounded — no loops)
This catches the most common transient failure mode: csrf.app cookie aged out but the session is still valid. A page reload re-issues the cookie. Verified 2026-05-13 with a synthetic 401-injection test: bad CSRF → tab refresh → retry succeeds with status 200.
When PAT becomes available (future)
If Joseph's HS permissions change (promoted to Super Admin, or a new role is granted):
1. Verify access: navigate to https://app.hubspot.com/private-apps/23273108 — should show "Create a private app" button instead of "no permission" copy.
2. Create the app:
- Name: "SkyRun Ambient System"
- Scopes: crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.deals.read, crm.objects.deals.write, crm.schemas.contacts.read
3. Copy the access token (one-time visibility — copy immediately)
4. Set in Keychain:
python3 ~/Library/Application\ Support/SkyRun/secrets.py set hubspot_pat "<token>"
5. Verify:
python3 ~/Library/Application\ Support/SkyRun/hs_api_client.py health
Should return
auth_path: pat (was csrf).
After that, all HS-touching skills automatically prefer PAT and the Chrome dependency becomes a fallback only. The keep-alive prober continues to probe both paths.
Verification commands
bash
Probe HS auth right now
python3 ~/Library/Application\ Support/SkyRun/hs_api_client.py auth-healthy
Same via the full client
python3 ~/Library/Application\ Support/SkyRun/hs_api_client.py health
See HS in the keep-alive output
python3 ~/Library/Application\ Support/SkyRun/session_keep_alive.py
cat ~/Library/Application\ Support/SkyRun/pwa/source_health.json | python3 -m json.tool | grep -A8 hubspot
Files touched 2026-05-13
hs_api_client.py— addedhs_auth_healthy()module helper + CSRF retry-on-401 with tab refresh +auth-healthyCLI command. Backup:hs_api_client.py.bak.pre_csrf_hardening_20260513.session_keep_alive.py— addedAPI_SOURCES+_probe_api()+ main-loop wiring. Backup:session_keep_alive.py.bak.pre_hs_probe_20260513.sot_reconciler.py— added pre-flighths_auth_healthy()at start ofreconcile(). Backup:sot_reconciler.py.bak.pre_auth_healthy_20260513.engagement_reconciler.py— added pre-flight inmain(). Backup:engagement_reconciler.py.bak.pre_auth_healthy_20260513.deal_stage_reconciler.py— added pre-flight before_acquire_lock(). Backup:deal_stage_reconciler.py.bak.pre_auth_healthy_20260513.
No launchd plist changes needed — the existing com.skyrun.session-keep-alive already runs every 300s and now probes HS as a side-effect of the API_SOURCES loop.