fix: [SDK-5192] detect push credentials from non-secret fields - #54
sherwinski wants to merge 2 commits into
Conversation
The server plans to remove the plaintext credential fields from
GET /api/v1/apps/{app_id}. The credentials and setup skills read those
fields to decide if a platform already has credentials. After the
server change, a configured platform would read as unconfigured.
Read the non-secret presence fields instead: apns_env, apns_key_id,
apns_team_id, fcm_sender_id, and chrome_web_origin. Treat null, an
empty string, and an absent key the same.
- onesignal_api.py app: print a per-platform verdict and a
channels.push.platforms cross-check; never print values
- api-reference.md: document the presence fields as the contract
- credentials and setup skills: point Step 1 and the credentials gate
at the script verdict and the presence fields
- api-uploaded-credentials.md: drop the legacy gcm_key check
- bump the plugin version to 1.1.1
- onesignal_api.py app: a 2xx whose body is not a JSON object now returns status error instead of a "not configured" verdict - setup skill: the no-Python curl path uses whichever key env var is set and keeps only the presence fields, so the plaintext credentials never reach the transcript - api-reference and credentials skill: say what a cross-check mismatch means and that a script error is "presence unknown"
kalley
left a comment
There was a problem hiding this comment.
Clean change. Failing closed on a body that isn't a JSON object was a good call. Approving on the understanding that the live run and the evals from the description happen before merge. One small note inline.
Minor: scripts/onesignal_api.py:196-197 repeats the module docstring and what _present already shows.
| apns_configured = _present(data.get("apns_env")) | ||
| if _present(data.get("apns_key_id")) and _present(data.get("apns_team_id")): | ||
| apns_auth = "p8" | ||
| elif apns_configured: |
There was a problem hiding this comment.
If only one of apns_key_id / apns_team_id is set, this reports p12. The reference says p12 means both are empty. Should the half-set case be null instead? That case is rare, so it doesn't block.
abdulraqeeb33
left a comment
There was a problem hiding this comment.
Two fallbacks still fail open. onesignal_api.py app returns status: error when the body is not a JSON object, and the credentials skill treats that as presence unknown. The no-Python curl in setup, and the raw GET in credentials, can record that same failure as unconfigured. That is the before-state the 409 recovery rule reads as "your upload landed."
| The single worst onboarding failure is installing the SDK before push credentials exist: the app builds, the device registers, and it shows up **unsubscribed** because OneSignal has nothing to hand APNs/FCM. Close credentials FIRST. Never skip this step silently. | ||
|
|
||
| 1. **Check what's configured.** With an app-scoped key available (`$ONESIGNAL_SETUP_TOKEN` / `$ONESIGNAL_REST_API_KEY`), `GET /api/v1/apps/{APP_ID}` (app auth works) and check the platform you're about to install: Android → FCM service-account configured? iOS → APNs key configured? Web → Site URL/origin configured? No key available → ask the user to check the dashboard (Settings > Push Platforms) and tell you. | ||
| 1. **Check what's configured.** With an app-scoped key available (`$ONESIGNAL_SETUP_TOKEN` / `$ONESIGNAL_REST_API_KEY`), run `<plugin>/scripts/onesignal_api.py app <APP_ID>` — the view-app read, `GET /api/v1/apps/{APP_ID}` (app auth works) — and read the verdict for the platform you're about to install: Android → `platforms.fcm.configured`, iOS → `platforms.apns.configured`, Web → `platforms.web.configured` (or the free `web-probe` below). The script decides from the **non-secret presence fields** only (`apns_env`, `fcm_sender_id`, `chrome_web_origin`; api-reference "View an app"), and treats `null`, `""`, and an absent key the same. Without Python (Step 0.2), `curl` the same `GET` with whichever key env var is set, and keep only the presence fields — the raw body still carries the plaintext credentials until the server removes them, so never print or paste the whole body: `curl -fsS -H "Authorization: Key ${ONESIGNAL_REST_API_KEY:-$ONESIGNAL_SETUP_TOKEN}" "https://api.onesignal.com/api/v1/apps/<APP_ID>" | grep -oE '"(apns_env|apns_key_id|apns_team_id|fcm_sender_id|chrome_web_origin)":[^,}]*'`. Read those fields and nothing else — never the plaintext credential fields (`apns_p8`, `fcm_v1_service_account_json`, `gcm_key`). No key available → ask the user to check the dashboard (Settings > Push Platforms) and tell you. |
There was a problem hiding this comment.
The no-Python curl -fsS | grep path treats a failed read as not configured. curl -f writes no body on 401, 404, or 5xx, and a non-object 2xx greps to nothing, which is the same signal as absent presence fields. Step 2 then starts the credentials skill, and that skill records the platform as unconfigured. A later 409 on the recovery re-call counts as success only when the platform was unconfigured before the first attempt, so a failed read can be reported as a landed upload.
Dropping -f, and treating a non-zero curl status or a non-object body as presence unknown, would match the script's status: error path.
| The probe reads and their response semantics come from [../../references/api-reference.md](../../references/api-reference.md) (the view-app read and the "Web platform config probe"), and `<plugin>/scripts/onesignal_api.py` encodes them as commands. Use those; do not hand-roll the calls. | ||
|
|
||
| 1. **Push platforms (iOS / Android):** run `onesignal_api.py app <app_id>` — the view-app read, `GET /api/v1/apps/{app_id}` — with an app-scoped key (the script takes `--key` or reads `$ONESIGNAL_REST_API_KEY` / `$ONESIGNAL_SETUP_TOKEN`). No MCP tool returns the per-app platform config (api-reference.md), so this read has no MCP path. Populated credential fields for the target platform mean the platform is configured. The script reports only the response's field *names*, which cannot make that call — the raw `GET` is the read that decides (inspect the target platform's field values); use the script output for reachability and auth errors. | ||
| 1. **Push platforms (iOS / Android):** run `onesignal_api.py app <app_id>` — the view-app read, `GET /api/v1/apps/{app_id}` — with an app-scoped key (the script takes `--key` or reads `$ONESIGNAL_REST_API_KEY` / `$ONESIGNAL_SETUP_TOKEN`). No MCP tool returns the per-app platform config (api-reference.md), so this read has no MCP path. The script prints a per-platform presence verdict under `platforms` — `apns.configured`, `fcm.configured`, `web.configured` — and that verdict decides. It reads only the **non-secret presence fields** the api-reference names (`apns_env` for APNs, `fcm_sender_id` for FCM, `chrome_web_origin` for web), and treats `null`, `""`, and an absent key the same. Never decide from the plaintext credential fields (`apns_p8`, `apns_certificates`, `fcm_v1_service_account_json`, `gcm_key`): the server plans to remove them from the response, and their absence does not mean the platform is unconfigured. If you must read the raw `GET` without the script, inspect the same presence fields and nothing else. `cross_check.mismatch: true` (from `channels.push.platforms`) is not a different verdict: it usually means the dashboard platform toggle and the stored credentials disagree, so tell the user in one sentence to check Settings > Push Platforms, and keep the verdict (api-reference.md "View an app"). A `status: error` from the script is "presence unknown", not "not configured" — re-run it or fall back to the dashboard before you record a Step 1 state. |
There was a problem hiding this comment.
The raw-GET fallback says to inspect the presence fields and nothing else, with no check that the response was a 2xx JSON object. A 401 body or a non-object 2xx has those fields absent, which this paragraph reads as not configured. The script path fail-closes. Item 7 covers a hard GET failure, but it fights this sentence.
Applying the presence fields only after a 2xx JSON object, and leaving Step 1 unset otherwise, would keep the 409 disambiguation on a real before-state.
One Line Summary
The credentials and setup skills now decide credential presence from non-secret fields of
GET /api/v1/apps/{app_id}, not from the plaintext credential fields.Linear: SDK-5192. Related server change: CORE-4599.
Motivation
The team plans to strip
apns_p8,apns_certificates,fcm_v1_service_account_json, andgcm_keyfrom the view-app response; which are all fields that plugin reads from currently. After the server change, a configured platform would read as unconfigured, the agent would start a portal walkthrough the user does not need, and the 409 disambiguation would trust a wrong Step 1 state. Every presence field in the new plan exists in the response today, so this change is safe to ship before the server change.Scope
references/api-reference.md: new "View an app" section. It names the presence fields (apns_env,apns_key_id,apns_team_id,fcm_sender_id,chrome_web_origin), thenull/""/ absent rule, thefcm_sender_idvschrome_web_gcm_sender_idtrap, and the cross-check-only status ofchannels.push.platforms. It also forbidsandroid_params.jsas a pre-upload cross-check, because that read primes the CDN cache on a fresh app.scripts/onesignal_api.pycmd_app: printsplatforms.{apns,fcm,web}.configured,platforms.apns.auth_type(p8/p12/null), and across_checkobject with amismatchflag. Replaceskeys_present. The script still never prints values.skills/credentials/SKILL.md: Step 1 item 1 and the 409 disambiguation note point at the script verdict and the presence fields.skills/setup/SKILL.md: the Step 3 "Check what's configured" item runs the script and names the fields for the no-Pythoncurlpath.skills/credentials/api-uploaded-credentials.md: removes the "Verifying which apps still use legacy" section (gcm_keyread).1.1.0→1.1.1in the 3 version fields.Not in scope: the verify skill (it does not read the view-app response), and any dependency on future state fields such as
apns_p8_state.Testing
Local checks from
AGENT.md, all green:python3 -m py_compile scripts/*.pypython3 -m json.toolon the 4 JSON filesskills/andreferences/: no outputpython3 scripts/package_directory_bundle.py --check:bundle check passed (version 1.1.1; nothing written)rg 'apns_p8"|gcm_key"|fcm_v1_service_account_json"' skills references scripts: no reads leftOffline test of
cmd_appwith a stubbed_get(script outside the repo, removed after the run). 5 cases passed: p8 + FCM, web-only (chrome_web_gcm_sender_idset,fcm_sender_idnull), no platform keys at all, p12 with empty strings, and achannels.push.platformsmismatch. The output contained no field values.Not done in this PR, and needed before merge:
onesignal_api.py app <app_id>against a p8 + FCM app, a web-only app, and an app with no platforms. No app-scoped key was available in the session.AGENT.mdcheck 7. Record the verdicts and session IDs here.Affected code checklist
skills/credentials,skills/setup)scripts/onesignal_api.py,scripts/checkpoint.shversion only)references/api-reference.md)Checklist