Skip to content

fix: [SDK-5192] detect push credentials from non-secret fields - #54

Open
sherwinski wants to merge 2 commits into
mainfrom
sherwin/sdk-5192
Open

sherwinski wants to merge 2 commits into
mainfrom
sherwin/sdk-5192

Conversation

@sherwinski

@sherwinski sherwinski commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

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, and gcm_key from 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), the null / "" / absent rule, the fcm_sender_id vs chrome_web_gcm_sender_id trap, and the cross-check-only status of channels.push.platforms. It also forbids android_params.js as a pre-upload cross-check, because that read primes the CDN cache on a fresh app.
  • scripts/onesignal_api.py cmd_app: prints platforms.{apns,fcm,web}.configured, platforms.apns.auth_type (p8 / p12 / null), and a cross_check object with a mismatch flag. Replaces keys_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-Python curl path.
  • skills/credentials/api-uploaded-credentials.md: removes the "Verifying which apps still use legacy" section (gcm_key read).
  • Version bump 1.1.0 → 1.1.1 in 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/*.py
  • python3 -m json.tool on the 4 JSON files
  • portability grep on skills/ and references/: no output
  • python3 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 left

Offline test of cmd_app with a stubbed _get (script outside the repo, removed after the run). 5 cases passed: p8 + FCM, web-only (chrome_web_gcm_sender_id set, fcm_sender_id null), no platform keys at all, p12 with empty strings, and a channels.push.platforms mismatch. The output contained no field values.

Not done in this PR, and needed before merge:

  • Live run of 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.
  • The behavior evals that cover the credentials and setup skills, per AGENT.md check 7. Record the verdicts and session IDs here.

Affected code checklist

  • Skills (skills/credentials, skills/setup)
  • Scripts (scripts/onesignal_api.py, scripts/checkpoint.sh version only)
  • References (references/api-reference.md)
  • Manifests (version bump)
  • Templates
  • CI / workflows

Checklist

  • Commit subject uses a conventional prefix
  • No internal ticket IDs in skills, scripts, or references
  • No secrets in the diff
  • The 3 version fields agree
  • Eval run recorded (owner to run)

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 kalley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/onesignal_api.py
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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Comment thread skills/setup/SKILL.md
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants