Skip to content

fix(mdm): idempotent DeviceInformation attestation enqueue (coordinator side of Secure Mode)#187

Merged
DGaffney merged 2 commits into
mainfrom
fix/mdm-attestation-idempotent-enqueue
Jul 6, 2026
Merged

fix(mdm): idempotent DeviceInformation attestation enqueue (coordinator side of Secure Mode)#187
DGaffney merged 2 commits into
mainfrom
fix/mdm-attestation-idempotent-enqueue

Conversation

@DGaffney

@DGaffney DGaffney commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Context

With #186 the agent-side key mismatch is fixed and proven (serve = agent pubkey = attestation, all 1feH…). That isolated the last blocker for Secure Mode to the coordinator, which the live logs show precisely:

[mdm] discarding captured attestation chain … freshness does not bind the currently-requested key
(a stale queued DeviceAttestation command attested an old key)

The bug

requestDeviceInformationAttestation appended a new DeviceInformation command to NanoMDM's per-device FIFO queue on every call — and NanoMDM (standard -api build) has no clear-queue endpoint. Repeated calls (agent auto-request every 6h + every wizard toggle + every restart) piled up stale commands carrying old-key nonces. The device drains them FIFO, each yielding an attestation bound to an old key → #178 correctly discards it → the real, current-key command never lands a fresh attestation. And hammering Apple's rate-limited DeviceAttestation (~1/device/7d) makes it return a cached old-key chain regardless of the new nonce.

Fix

Make the enqueue idempotent. If we already requested attestation for this exact key within a 6h window, don't enqueue another command — just re-push so the device processes the one already queued. A key change still enqueues immediately. This stops both the pile-up and the Apple-rate-limit hammering; combined with #186's now-stable key, the single current-key command drains and finally attests the right key.

  • getExpectedAttestation(serial) returns the requested key + timestamp for the (pubkey, recency) dedup.
  • 4 new tests: first-request enqueues + pushes; same-key-recent re-pushes only (no duplicate enqueue); key-change enqueues immediately; stale-same-key (>6h) re-enqueues.

Notes

  • Console-only; deploys on Railway. No agent/app release needed.
  • 29 coordinator tests pass (25 existing + 4 new), tsc + oxlint clean.
  • For an already-piled-up device (like my test Mac), the next auto-request/toggle now re-pushes rather than re-enqueues, and the queued current-key command drains on subsequent check-ins.

🤖 Generated with Claude Code

…ing stale commands

The coordinator side of Secure Mode never binding (isolated after #186 fixed the
agent-side key mismatch): every request-attestation call APPENDED a new
DeviceInformation command to NanoMDM's per-device FIFO queue, and NanoMDM has no
clear-queue API. So repeated calls (agent auto-request every 6h + every wizard
toggle + every restart) piled up stale commands with OLD-key nonces. The device
drains them FIFO, each producing an attestation bound to an old key, which #178
correctly discards — so the real (current-key) command never gets a fresh
attestation. Worse, hammering Apple's rate-limited DeviceAttestation
(~1/device/7d) makes it return a CACHED old-key chain regardless of the new
nonce. Live symptom: the coordinator logging "discarding captured attestation
chain … freshness does not bind the currently-requested key" on a loop.

Fix: make the enqueue idempotent. If we already requested attestation for THIS
exact key within a 6h window, don't enqueue another command — just RE-PUSH so the
device processes the one already queued. A KEY CHANGE still enqueues immediately.
This stops the pile-up and the Apple-rate-limit hammering; combined with #186's
now-stable key, the single current-key command drains and finally attests the
right key.

- `getExpectedAttestation(serial)` returns the requested key + timestamp so the
  enqueue can dedup on (pubkey, recency).
- 4 new tests: first-request enqueues; same-key-recent re-pushes only (no
  duplicate enqueue); key-change enqueues; stale-same-key re-enqueues.

Console-only; deploys on Railway. 29 coordinator tests pass, tsc + oxlint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot 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.

DGaffney has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@railway-app railway-app Bot temporarily deployed to co/core / cocore-pr-187 July 6, 2026 21:38 Destroyed
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🛠️ Provider build (macOS arm64) — menu-bar app + CLI

Built from 0069248. The tarball bundles cocore.app (the menu-bar/tray app) plus the bare cocore CLI.

🔌 Pre-wired to this PR's stack — the app defaults to https://client-cocore-pr-187.up.railway.app + wss://advisor-cocore-pr-187.up.railway.app/v1/agent (overridable in Settings → Network).

⬇️ cocore-mac-arm64-pr187-0069248.tar.gz

(GitHub artifact — requires being signed in to this repo. The zip contains the .tar.gz + its .sha256.)

tar -xzf cocore-mac-arm64*.tar.gz && cd cocore-mac-arm64
./install.sh   # installs cocore.app to /Applications + the CLI, launches the tray

⚠️ Ad-hoc signed (not notarized) — but install.sh clears the Gatekeeper quarantine for you, so it launches without the "unidentified developer" block. (If you drag the .app in by hand instead, run xattr -dr com.apple.quarantine /Applications/cocore.app yourself first.)

CLI only? It's wired to this PR's stack too:

./bin/cocore agent pair  --console https://client-cocore-pr-187.up.railway.app
./bin/cocore agent serve --advisor wss://advisor-cocore-pr-187.up.railway.app/v1/agent

@railway-app railway-app Bot temporarily deployed to co/core / cocore-pr-187 July 6, 2026 21:51 Destroyed

@greptile-apps greptile-apps Bot 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.

DGaffney has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@DGaffney DGaffney merged commit 036e78d into main Jul 6, 2026
10 checks passed
@DGaffney DGaffney deleted the fix/mdm-attestation-idempotent-enqueue branch July 6, 2026 21:57
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.

1 participant