Conversation
Running the example host on hardware surfaced that it was a control
panel with no data source: nothing could register a miniapp, and there
was no pairing UI — and the dev-miniapp registration path (registerDevApp)
was /internal-only, unreachable by a boundary-respecting host.
- toolkit.dev.loadDevMiniapp(url): public side-loading — probes
<url>/miniapp.json via the already-public decideDevLaunchRoute,
registers into the single dev slot, refreshes the apps list, returns a
typed {ok} result. The OEM-host equivalent of the first-party
developer-URL screen.
- The example app gains a Pair section (markPendingSelection -> scan ->
tap a found device -> pair, plus reconnect/disconnect, live status row
from toolkit.glasses.onStatus / pairing.onScanning / pairing.onFound)
and a Miniapps section (URL field -> loadDevMiniapp -> start/stop).
Every call goes through the public entry; the two-phase identity
contract is followed (pair() never saves a default eagerly).
- jest island mock gains the loadDevMiniapp stub for mock parity.
Gates: island tsc + 220 tests; OEM app tsc against rebuilt public types;
expo export bundles clean; mobile tsc + 55 suites / 429 tests.
Bug reports and feedback moved to the Cloud V2 reports service, which posted nothing to Slack; the V1 feedback path that did is no longer invoked. Add a fire-and-forget notifier: feedback reports post from submitReport (ready as submitted), bug/automatic reports post from markReportReady on the collecting->ready transition with the artifact count. Webhook comes from CLOUD_REPORTS_SLACK_WEBHOOK_URL; unset is a silent skip, and errors are logged, never thrown, so the API response is never delayed. Message mirrors the V1 slack.service.ts conventions.
The incident-system tile in the internal admin console was a placeholder, and since the Mentra App reporting flow moved to Cloud V2 the only way to read a submitted bug report or feedback was querying Mongo by hand. Core gains a read-only admin surface behind the existing adminAuth gate: GET /api/admin/reports lists reports newest-first (kind/status filters, context excluded), GET /api/admin/reports/:reportId returns the full document plus its report_assets rows, and GET /api/admin/reports/:reportId/artifacts/:artifactId streams artifact payload bytes from blob storage with the stored content type. The router is sub-mounted inside the admin router so adminAuth runs exactly once per request; a second mount at /api/admin would run the sealed-session authenticate twice and could burn the single-use WorkOS refresh token. Client filenames are allowlist-sanitized before reaching the content-disposition header, and a createdAt index backs the list sort. The admin website replaces the incidents placeholder with a live Reports page: kind/status filter pills, newest-first list, and a detail drawer showing report details, device context, inline screenshot previews, and an expandable log-bundle viewer, all through the same-origin /api proxy. Integration tests drive the routes end-to-end with a fully local admin principal: an org API key is not a JWT, so the bearer path falls through to DB validation without WorkOS, and its synthetic api-key email is allowlisted via CLOUD_CORE_ADMIN_EMAILS. Covers the 401/403 gate, list shape and filters, detail with assets, and artifact byte round-trips.
A reporter controls both the bytes and the declared multipart type of a screenshot upload, so the admin artifact endpoint could be handed HTML or SVG and render it inline, same-origin with the admin console session. The upload path now stores the declared type only when it names a plausible screenshot format (jpeg/png/webp/gif/heic/heif), falling back to application/octet-stream. The admin artifact route independently allowlists what it serves inline (non-scriptable images plus JSON), downloads everything else as an opaque attachment, and stamps nosniff plus a deny-all sandbox CSP on every payload response. The admin UI only inlines screenshot previews whose stored type is an image. Covers both directions with integration tests: a genuine image stays inline with the hardening headers, and an HTML file claiming to be a screenshot comes back as an octet-stream attachment.
The shared static CPU/screen wake locks were replaced on every acquire: each acquireCpuWakeLock() released whatever was held and installed a fresh lock with the new timeout. So a short acquire (e.g. a 60s camera/util lock, or the 5-min BES lock) landing during a long operation would shorten the in-flight deadline. When that shortened lock expires the CPU — and the MTK SoC — can sleep mid-update, which stalls OTA (BES/MTK transfers fail). Make acquisition extend-only: track an absolute deadline per lock and, if a lock is already held whose deadline is at or beyond the requested one, keep it and ignore the shorter acquire. Only a later deadline swaps the lock. The timeout still auto-releases as a backstop, and explicit release() clears the tracked deadline. acquire/release are now synchronized so the deadline check is race-free across the OTA worker, main looper, and camera/streaming threads. This is the "longest deadline wins" fix — it stops shorter acquires from stomping a longer-lived lock without re-plumbing every call site. Adds WakeLockManagerTest (Robolectric) covering shorter-ignored, longer-extends, release-then-reacquire, and the screen-lock path.
Slack rejects the entire message when any section text exceeds 2000 chars, silently losing the notification. Trigger source/reason and other client strings were unbounded, and escaping after truncation could push even capped behavior text past the limit (500 '<' chars escape to 2000+). Cap every client-sourced string per field and hard-cap the escaped result under Slack's limit.
…b8eb7 Notify Slack when Cloud V2 reports are submitted or complete
Automatic watchdog reports shared the feedback channel webhook. Read CLOUD_REPORTS_SLACK_WEBHOOK_AUTOMATIC_URL for kind automatic, falling back to CLOUD_REPORTS_SLACK_WEBHOOK_URL when unset — the same routing V1 used for SLACK_WEBHOOK_AUTOMATIC_INCIDENTS. Bug and feedback reports keep using the main webhook.
Cloud V2's release lifecycle had no Slack notification on submit, so once console2 replaces the V1 developer console the team's #mini-app-submissions channel would silently stop receiving posts (V1 posts via notifyMiniAppSubmission / SLACK_WEBHOOK_MINI_APP_SUBMISSION). Mirrors the report-slack.service pattern: webhook from CLOUD_MINIAPP_SLACK_WEBHOOK_URL, silent skip when unset, errors logged never thrown, 10s timeout, fire-and-forget from submitRelease.
…utomatic-split Route automatic report Slack posts to their own webhook
Bugbot review finding: nullish coalescing meant a falsy-but-non-nullish manifest.type (empty string, or any non-string junk) blocked the appType fallback. Take the first candidate that is a non-empty string.
Issue 019 plans moving mobile login off Cloud V1: the app currently embeds supabase-js and relies on the legacy core token; V2's exchange accepts both via the symmetric mentra-tenant branch. Decision recorded: build the account module inside packages/core (api/account + services/account) rather than a separate deployable, dogfooding the OEM path with a mentra-issuer Ed25519 subject token. Phase 1 moves the surface (core drives Supabase server side), Phase 2 may swap the credential store. Deliverables gated: spike.md -> spec.md -> design.md.
…lacing) Records the security/reliability rationale: symmetric shared secrets (any env can forge any user), three client-glued token systems, credentials at rest on device + in URLs, no central revocation, third-party baked into the app binary. Notes scope honesty: 019 fixes auth-chain fragility, not the July ops/env drift.
…ency ledger) README: the headline goal is a MentraOS build with zero legacy Cloud V1 dependency; login is the enabling first cut, not the whole job. spike.md first pass: the current login chain (supabase-js client-side -> legacy /auth/exchange coreToken -> V1 glasses-ws; V2 exchange accepts both), the full V1 dependency ledger (RestComms REST surface incl. account deletion and settings/calendar/location/notifications feeds; the V1 WS still owning the OS dashboard + app bridge), ranked tech debt, a 4-cut sequencing (identity -> client data feeds -> dashboard/app-bridge -> V2-only build flag), and the open questions gating spec.md (OAuth redirect mechanics, device session model, legacy coexistence, migration, identity side-channels).
…-refcount ASG: make WakeLockManager extend-only so a short acquire can't cut OTA short
Decisions: core-hosted OAuth via system browser (mirrors console PKCE flow), V2-tokens-only on device (GoTrue used transiently + admin API, no Supabase material at rest), NO legacy coexistence bridge (SocketComms is being deprecated and V1 removal lands in the next PR, so V1 WS/REST break by design and the ledger in section 6 is that PR's work list), silent session migration via the existing exchange then retire the symmetric branch, single /api/account/me for identity side-channels, and account deletion moves in Phase 1 (store compliance cannot be in the broken list). Spike complete; spec.md unblocked.
Decision 4 revised: no silent bootstrap or migration ramp. On update, stored Supabase/legacy material is wiped and users sign in again, which IS the migration. Lets the symmetric Supabase/legacy exchange branch be deleted immediately instead of lingering behind a usage metric.
spec.md: the full /api/account contract (credentials, password/email flows, core-hosted OAuth with PKCE + one-time deep-link code, account deletion), error taxonomy extending the RFC-shaped oauth errors, token design (Ed25519 mentra-account subject token through the existing exchange; `mentra` becomes a real oems row; symmetric branch deleted), the authClient method mapping so mobile screens change minimally, and breaking-cutover rollout gated by min-version. design.md: module layout in core, server-side GoTrue integration table with error mapping isolated in gotrue.client.ts (the Phase 2 swap seam), keys + startup migration for the mentra oems row, the OAuth sequence end to end, mobile changes (delete supabase-js/anon key/exchangeToken; new provider behind authClient), deletion fan-out to V1 with reconciliation, test plan incl. negative proofs, and the staged rollout ending in the V1-removal PR.
…+ tests Implements the /api/account contract (issue 019 spec.md): gotrue.client (the only Supabase-aware file, error-mapped), account.service orchestration, one-time-code service + account-code model, account.api + oauth.api routes, Ed25519 mentra-account subject token fed through the existing OEM exchange, mentra oems-row startup migration, per-user session revoke, and AccountError rendered by the app error handler. Additive only; symmetric branch deletion deferred to the V1-cutover PR. Integration tests (mock GoTrue, 6/6): login mints a V2 session whose refresh works, invalid_credentials uniform, verification_required, /me, single-use reset codes, logout-everywhere. Typecheck clean.
… at the public exchange Refresh authorization now checks the mentra oems row like every OEM (the early-return is demoted to a transitional fallback for environments whose seed migration has not run; it dies at the V1 cutover). Two parity tests added: the account subject token exchanges at the PUBLIC /api/client/auth/exchange endpoint (no private path), and a disabled mentra oems row blocks refresh like any OEM. design.md gains section 9 mapping each account-module behavior to its external-OEM equivalent so the module doubles as OEM reference code.
session.service.ts was three concerns in one: session lifecycle, token minting, and crypto key management + JWKS. Move the key concern (keypair loading for access/miniapp/account, kid constants, MENTRA_ALG, requireEnv, getPublicJwks, resetSigningKeyCache, getAccountPublicKeyPem) into signing-keys.service.ts, so session.service is about sessions again. Clean dependency direction: signing-keys <- session. Re-exported the moved public symbols from session.service for existing importers; no behavior change. Also fixed test isolation: account-auth test now resets the signing-key cache in beforeAll (mirrors the audio tests) so it does not inherit another file's cached keys, and session.service.test seeds the account key that getPublicJwks now publishes. Full auth suite 32/32 + jwks unit 1/1, typecheck clean.
- POST /api/account/subject-token: authenticated by the current V2 session, mints a short-lived `mentra` subject token the device's cloud-client exchanges at the public /api/client/auth/exchange, exactly as an external OEM app would (OEM parity). - dropLegacyUserIdentityIndex: guard collection.indexes() so a fresh database (no users collection) boots instead of crashing on "ns does not exist".
…upabase) Replace the embedded Supabase client and the legacy Cloud V1 token exchange with AccountAuthProvider, which talks only to Cloud V2's first-party account backend (/api/account/*). The device holds V2 tokens directly; cloud-client mints a fresh subject token via getSubjectToken() and exchanges it, so Mentra behaves as just another OEM. - accountClient.ts: AccountAuthProvider (login/signup/me/reset/subject-token, MMKV token storage, single V2 refresh). Notifies AuthContext synchronously on sign-in so home-boot doesn't race the listener and bounce to /auth/start. - authClient.ts: default (non-China) provider is now AccountAuthProvider; adds getSubjectToken() to the AuthClient contract. - MantleManager.ts: toolkit.configure getSubjectToken pulls from the provider. - index.tsx: boot needs only a valid V2 session, then mantle.init(); drops the restComms/socketComms V1 exchange.
Bugbot review finding: the top-level text field is rendered as mrkdwn like the blocks, so developer-controlled strings in the fallback need the same escaping, mirroring V1's escaped fallback text.
…ithout account keys Addresses the cursor[bot]/codex review findings on #3382: - authorizeUrl now sends code_challenge (S256 of core's verifier) + code_challenge_method to GoTrue; without it GoTrue issues a non-PKCE code and the callback's pkce exchange fails. - Email change is verification-gated: /email/change re-verifies the password and emails a one-time code to the NEW address; the change is applied only by the new /email/change/confirm. (The old path used the GoTrue admin update, which applies immediately with no confirmation, contradicting the API's verification_sent contract.) - getPublicJwks degrades gracefully when MENTRA_ACCOUNT_JWT_* is unset: publishes access/runtime/miniapp kids and omits the account kid instead of 500ing the whole JWKS in environments where the account feature is dark (mirrors the startup migration's skip). - Tests: email-change gating (request applies nothing, code confirm applies), PKCE params on the authorize URL, JWKS with and without account keys.
Completes the provider methods the review flagged as unimplemented stubs:
- googleSignIn/appleSignIn: build the /api/account/oauth/:provider/start URL
with a fresh state + S256 PKCE challenge; verifier and state stay in MMKV.
New pkce.ts (pure-JS SHA-256, verified against NIST + RFC 7636 vectors;
Hermes has no WebCrypto and the app does not ship expo-crypto).
- completeOAuthHandoff: the /auth/callback deep link now handles the new
?code&state query form (alongside the legacy #fragment form), verifies
state, and swaps code + verifier at /oauth/complete for the V2 session.
- updateUserPassword/updateUserEmail/confirmEmailChange wired to
/api/account/password/change and /email/change{,/confirm}. The settings
screens now collect the current password (server re-verifies it; a stolen
session alone must not rotate credentials), and change-email gains the
code-confirmation step matching the new verified flow.
Photos saved on the glasses were named IMG_<timestamp>_<rand> with the originating take_photo requestId discarded, so bulk gallery imports could only correlate synced files with photo requests by timestamp matching, which breaks under clock skew and rapid bursts. Videos already embed the requestId in their capture directory name; photos now follow the same convention (sanitized, for both save:true gallery captures and transient save:false captures). - Add CaptureRequestId util: sanitizes requestIds for directory-name embedding and extracts them back out of capture IDs. - /api/sync capture groups and /api/gallery entries expose the embedded ID as an explicit request_id field (absent for button captures and legacy files). - Button captures have no originating request; their stable ID is the capture directory name itself, which now replaces the throwaway local_<timestamp> string in photo status messages. - Sanitize the requestId embedded in video capture directory names. Tested: scripts/check-android-compile.sh asg passes; new CaptureRequestIdTest unit suite (7 tests) passes.
…kflow) Personal sandbox in the aws-us-west-2 cluster while cloud-debug is frozen for enterprise customers. Same shape as cloud-debug: one Porter app (cloud-isaiah), core + runtime from the shared image, Doppler config dev_isaiah synced via the cloud-v2-isaiah-doppler env group, hostnames core/runtime.isaiah.us-west-2.mentraglass.com (Cloudflare DNS-only CNAMEs to the cluster ingress). Triggers: workflow_dispatch + push on mentra-account-auth. No per-env UDP NLB (WS fallback), matching dev/debug.
Verified against the cluster/DNS/Doppler on 2026-07-09: - No environment has a working public UDP path today: the Doppler configs advertise audio-udp.<env> hostnames that have no Cloudflare records, so clients fail UDP and fall back to WS. The runbook's 'What's deployed' section now says so instead of describing an NLB in front of an app name (cloud-v2) that no longer deploys. - NLB Services are per-environment (selector pins one app's runtime pods): audio-udp-nlb-debug.yaml is the template; audio-udp-nlb.yaml is marked LEGACY do-not-apply. - Provisioning steps rewritten per-env: clone the template, direct kubectl (porter kubectl is read-only; aws sso login if the token expired), set AUDIO_UDP_ADVERTISED_HOST in the env's OWN Doppler config (raw NLB hostname is fine for dev-grade envs), redeploy that env. - Fixed both manifests' comments that said 'porter kubectl apply', which contradicted the runbook's own read-only warning. - Annotated example now shows the per-env shape including the mandatory externalTrafficPolicy: Local.
…evice-errors Improve camera device error reporting
…sh-the-toolkit-on-npm-as-mentraengine OS-1668: Rename island and toolkit APIs to Mentra Engine
PR Agent HandoffExit reason: Budget exhausted — fix or review cycle cap reached.
CI status
Remaining blocking findingsNone Nits (informational)None Humans merge when ready. Agents do not auto-merge. Label |
PR Agent HandoffExit reason: Budget exhausted — fix or review cycle cap reached.
CI status
Remaining blocking findingsNone Nits (informational)None Humans merge when ready. Agents do not auto-merge. Label |
PR Agent HandoffExit reason: Budget exhausted — fix or review cycle cap reached.
CI status
Remaining blocking findingsNone Nits (informational)None Humans merge when ready. Agents do not auto-merge. Label |
- Drop the postinstall patch-package step: it pointed at the deleted patches-runtime/ dir, whose only patch was the retired Supabase one (a missing --patch-dir is a no-op, so this is dead weight, not a fix) - Verify logout against both MMKV account tokens so the check matches the "token pair" comment - Update stale comments that still described the deleted WebSocketManager/SocketComms/ws-types as live
Dev's #3411 renamed @mentra/island to @mentra/engine (toolkit → engine) across every file this branch deletes or edits. Resolution re-expresses the ripout in the engine world: - Files this branch deletes (host SocketComms/WSM/RestComms/ws-types, BackendUrl, NonProdWarning, and the module RestComms): deletion wins; dev's only changes to them were the mechanical rename. - Content conflicts: this branch's version plus dev's exact rename sweep (@mentra/island → @mentra/engine, toolkit → engine, useToolkitSnapshot → useEngineSnapshot), with comment wording aligned to dev's engine naming. tsc clean on the merged tree (mobile src + modules).
feat(020): rip out Cloud V1 from the mobile app
PR Agent HandoffExit reason: Budget exhausted — fix or review cycle cap reached.
CI status
Remaining blocking findingsNone Nits (informational)None Humans merge when ready. Agents do not auto-merge. Label |
…logs-v2 Port fetch-incident-logs.sh to Cloud V2 admin reports API
PR Agent HandoffExit reason: Budget exhausted — fix or review cycle cap reached.
CI status
Remaining blocking findingsNone Nits (informational)None Humans merge when ready. Agents do not auto-merge. Label |
PR Agent HandoffExit reason: Budget exhausted — fix or review cycle cap reached.
CI status
Remaining blocking findingsNone Nits (informational)None Humans merge when ready. Agents do not auto-merge. Label |
PR Agent HandoffExit reason: Budget exhausted — fix or review cycle cap reached.
CI status
Remaining blocking findingsNone Nits (informational)None Humans merge when ready. Agents do not auto-merge. Label |
…-playback-cutoff Fix Recorder playback and trailing audio cutoff
…ios-checkpoint Checkpoint: Add iOS-compatible BLE photo fast path
…-settings-copy Clean up camera settings copy and navigation
…ozen-docs Reconcile frozen documentation with dev
…data-sheet Add gallery media metadata sheet
PR Agent HandoffExit reason: Budget exhausted — fix or review cycle cap reached.
CI status
Remaining blocking findingsNone Nits (informational)None Humans merge when ready. Agents do not auto-merge. Label |
Summary by cubic
Cuts the mobile app over to Cloud V2 first‑party account auth and fully removes Cloud V1 + embedded Supabase, adds report triage with hardened artifact delivery and Slack notifications (with admin deep‑links), embeds request correlation in photo folders and EXIF, and renames the OEM toolkit to the Mentra Engine (
@mentra/engine). Also improves Wi‑Fi/hotspot reliability, battery/charging reporting, camera error handling, and the example OEM app with pairing + public dev miniapp side‑loading, plus per‑environment infra updates and OTA/docs.New Features
/api/account/*(login/signup, PKCE OAuth with deep‑link handoff, password/email change with verification, account deletion), short‑lived subject‑token minting, and per‑IP/email rate limits; devices now hold V2 tokens only; mobile provider adds silent refresh, offline boot with cached profile, cached identity, and in‑app deletion.@mentra/engine); example OEM app adds pairing andengine.dev.loadDevMiniapp(url)for public side‑loading.requestIdembedded in photo folder names and stamped into EXIF;/api/sync+/api/galleryreturnrequest_id; save‑only SDK photos use the gateless button path and includecaptureIdin responses.cloud-isaiahenv + deploy workflow (with UDP NLB manifest); Bluetooth SDK bumped to0.1.19(@mentra/bluetooth-sdk), and ASG version name set to the package version; Mentra Live OTA docs updated; engine docs added.Bug Fixes
x-forwarded-proto(orx-mentra-public-origin) to satisfy Supabase allowlists; PKCE enforced; verified email‑change flow; robust JWKS when account keys are dark; mentra tenant gate on account endpoints.wifiErroris sticky; hotspot “enabled” gates on real AP readiness with race‑hardened teardown and echo suppression.CameraOperationErrorthreaded through warm‑up/capture while preserving legacy string callbacks; EXIF write order pinned by tests.Written for commit 61bbbf5. Summary will update on new commits.