diff --git a/desktop/src-tauri/src/native_tray_accounts.rs b/desktop/src-tauri/src/native_tray_accounts.rs index 54309ce6749..8d2394bcf6f 100644 --- a/desktop/src-tauri/src/native_tray_accounts.rs +++ b/desktop/src-tauri/src/native_tray_accounts.rs @@ -151,7 +151,7 @@ fn windows(quota: &Value, plan: &str) -> Vec { } pub fn provider(source: &Source, body: Option<&Value>, unavailable: bool) -> Value { - let parsed = body.and_then(parse_accounts); + let parsed = body.and_then(|body| parse_accounts(body, source.switch)); let malformed = body.is_some() && parsed.is_none(); let accounts = parsed.unwrap_or_default(); let mut row = json!({"id":source.name,"label":source.label, @@ -165,7 +165,7 @@ pub fn provider(source: &Source, body: Option<&Value>, unavailable: bool) -> Val row } -fn parse_accounts(body: &Value) -> Option> { +fn parse_accounts(body: &Value, kind: Option) -> Option> { let rows = body .get("accounts") .or_else(|| body.get("keys"))? @@ -175,9 +175,16 @@ fn parse_accounts(body: &Value) -> Option> { .find_map(|key| body[key].as_str()); rows.iter().enumerate().map(|(index,row)| { let id = row["id"].as_str()?; - let email = row["email"].as_str().map(mask_email); - let label = [row["alias"].as_str(),row["label"].as_str(),email.as_deref(),row["logLabel"].as_str(),Some(id)] - .into_iter().flatten().find(|v| !v.is_empty()).unwrap_or(id); + let oauth = kind == Some(AccountSwitchKind::OAuth); + let email = if oauth { None } else { row["email"].as_str().map(mask_email) }; + let label = if oauth { + [row["alias"].as_str(), row["maskedLabel"].as_str()] + .into_iter().flatten().map(str::trim).find(|v| !v.is_empty()) + .map(str::to_owned).unwrap_or_else(|| format!("Account {}", index + 1)) + } else { + [row["alias"].as_str(),row["label"].as_str(),email.as_deref(),row["logLabel"].as_str(),Some(id)] + .into_iter().flatten().find(|v| !v.is_empty()).unwrap_or(id).to_owned() + }; let unavailable = row["quotaUnavailable"].as_bool()==Some(true) || text(row,"quotaMode")=="unsupported" || !row["quota"].is_object(); let is_active = active.map_or(row["active"].as_bool()==Some(true),|selected|selected==id); @@ -230,6 +237,66 @@ fn exhausted(quota: &Value, plan: &str) -> bool { #[cfg(test)] mod tests { use super::*; + fn parse_accounts(body: &Value) -> Option> { + super::parse_accounts(body, None) + } + + fn oauth_accounts(body: Value) -> Vec { + let source = Source { + name: "example".into(), + label: "Example".into(), + path: None, + switch: Some(AccountSwitchKind::OAuth), + }; + provider(&source, Some(&body), false)["accounts"] + .as_array() + .unwrap() + .clone() + } + + #[test] + fn oauth_alias_is_trimmed_and_does_not_expose_legacy_email() { + let rows = oauth_accounts(json!({"accounts":[{"id":"private-id","alias":" Work ", + "maskedLabel":"m•••@e•••.com","email":"person@example.com"}]})); + assert_eq!(rows[0]["label"], "Work"); + assert!(rows[0]["email"].is_null()); + assert_eq!(rows[0]["accountId"], "private-id"); + assert_eq!(rows[0]["id"], "private-id:0"); + } + + #[test] + fn oauth_masked_label_follows_blank_alias() { + let rows = oauth_accounts(json!({"accounts":[{"id":"private-id","alias":" ", + "maskedLabel":" m•••@e•••.com ","label":"legacy","email":"person@example.com"}]})); + assert_eq!(rows[0]["label"], "m•••@e•••.com"); + assert!(rows[0]["email"].is_null()); + } + + #[test] + fn oauth_legacy_payload_uses_ordinal_instead_of_identifiers() { + let rows = oauth_accounts(json!({"accounts":[ + {"id":"private-id","email":"person@example.com","label":"legacy", "logLabel":"legacy-log"}, + {"id":"another-private-id","alias":" ","maskedLabel":" "}]})); + assert_eq!(rows[0]["label"], "Account 1"); + assert_eq!(rows[1]["label"], "Account 2"); + assert!(rows[0]["email"].is_null()); + assert!(rows[1]["email"].is_null()); + assert_eq!(rows[0]["accountId"], "private-id"); + assert_eq!(rows[1]["accountId"], "another-private-id"); + } + + #[test] + fn codex_and_api_key_labels_keep_the_existing_projection() { + let body = json!({"accounts":[{"id":"id","label":"Legacy label", + "maskedLabel":"ignored","email":"person@example.com"}]}); + for kind in [AccountSwitchKind::Codex, AccountSwitchKind::ApiKey] { + let rows = super::parse_accounts(&body, Some(kind)).unwrap(); + assert_eq!(rows[0]["label"], "Legacy label"); + assert_eq!(rows[0]["email"], "p•••@e•••.com"); + assert_eq!(rows[0]["accountId"], "id"); + } + } + #[test] fn source_routes_are_encoded_and_never_forward_credentials() { let rows = sources(&json!({"providers":{ diff --git a/devlog/_plan/261107_startup_safety_and_account_labels/000_plan.md b/devlog/_plan/261107_startup_safety_and_account_labels/000_plan.md new file mode 100644 index 00000000000..3d0501517df --- /dev/null +++ b/devlog/_plan/261107_startup_safety_and_account_labels/000_plan.md @@ -0,0 +1,79 @@ +# Startup safety and account labels + +Desktop users need a quiet, truthful startup verdict when login registration cannot +be checked, and OAuth pool users need recognizable, privacy-preserving account +names. Preserve conservative restart-protection credit while distinguishing an +unknown observation from a verified disabled setting. Render user aliases first, +then a server-projected masked identity, then a localized ordinal. + +- Loop: satisfy-spec, triggered by the owner request; goal is both changes merged to `dev`. +- Scope: Desktop login evidence and dashboard presentation; OAuth roster and account + label surfaces; their tests, all GUI locales, public and ownership documentation. +- Out: shared inline-label background style; real user installation, services and + login items; root full suite; new dependencies; native stack registration. +- Evidence: failing-first regressions, focused suites, typecheck, GUI checks, + file-size ratchet, i18n, React Doctor, privacy and structure checks, isolated + browser screenshots and exact-head CI plus a fresh independent review. +- Stop: after the authorized PR is merged and the squash SHA reported; blocked + execution and unmet gates are reported without declaring completion. +- Memory: this unit and its numbered phase documents; session goalplan and receipts. +- Escalate only for a necessary unavailable capability or an unauthorized action. + No token or time bounds stated; host limits apply. Keep host load moderate. + +## Dependency order + +1. Docs-only roadmap and independent audit (this cycle). +2. [010 implementation](010_implementation.md): evidence contract and safe + account projection, then their presentation and tests in one coherent change. +3. [020 integration](020_integration.md): exact-head review, CI and integration. + Use one ordinary PR unless the actual diff proves too large to review safely; + phase ordering by itself is not a reason to stack. + +## Source evidence and architect consultation + +Baseline `41f316f40e` matched freshly fetched `origin/dev`. The service diagnostic +collapses failed and disabled probes in `src/service/desktop-startup.ts`, while +`gui/src/pages/startup-sections.tsx` repeats one recovery key three times. The +OAuth roster returns config-dependent `email` from `src/oauth/index.ts` and +`src/server/management/oauth-account-routes.ts`; the GUI requests ordinal-only for +Anthropic in `gui/src/components/provider-workspace/ProviderAuthPanel.tsx`. +The macOS tray and service share enabled-credit fixtures as documented in +`structure/desktop-start-at-login.md`. + +Architect handle `node:delegated-task:command%3Amcp%3A2f716394-9556-49bf-a7d0-57ef9242bf2e%3Adelegate-task%3Astartup-architect-proposal-1`: +ARCH-01 additive tri-state accepted, with disabled only on stable evidence for +the actual app; absence without independently established ownership stays unknown. +ARCH-02 conservative backend credit and neutral GUI presentation accepted for +both live supervised and owned proxy; independently known risks remain visible. +ARCH-03 one explanation accepted. ARCH-04 masked email projection accepted; +no opaque ID fallback, malformed identities use ordinal. Aliases remain first. +Reflection round 1 was MISALIGNED: stable negative checks, stale/cached presentation, +system-disabled guidance and all OAuth fallback consumers needed detail. These four +findings were accepted and incorporated in `010_implementation.md`. The same +architect's reflection round 2 returned ALIGNED for ARCH-01 through ARCH-04, +with no remaining material gaps. Independent audit round 1 returned FAIL: omitted +Classic/page-level presentation, an explicit fixture-backed browser harness, security +review, and `ci:full` were accepted and added to 010/020. The same independent +reviewer's round 2 returned PASS: all four findings closed, no new high or critical +findings, and all 35 named paths resolved. This is plan-level evidence only. + +## Baseline checks + +After `bun install --frozen-lockfile` in root and `gui/` (both exit 0), +`bun test tests/gui/startup-health-ui.test.ts tests/gui/provider-workspace-auth.test.ts tests/service/service-desktop-login-registration.test.ts` +exited 0, 100 pass; it directly reads the named tests and their imports. +`bun run structure:check` exited 0 and reads `structure/` through +`scripts/structure-ssot.ts`. `cd gui && bun run lint:i18n` exited 0 and +reads its configured `gui/src/pages`, `gui/src/components` and other named GUI files; +it is not proof of translation completeness by itself. + +## Roadmap cycle conclusion + +The independent re-audit passed after four findings were folded in. A fresh bound +document check found three non-empty documents, required contract/integration gates, +and clean whitespace. Next: revalidate 010 against current source and begin its +failing-first implementation cycle. No product code was changed in the roadmap cycle. +The initial assumption that an existing roster label was always safe did not hold: +the email field follows a configurable masking policy, so the plan now requires a +dedicated masked projection. A test with masking disabled would refute that direction +if the projected label exposed the original identity. diff --git a/devlog/_plan/261107_startup_safety_and_account_labels/010_implementation.md b/devlog/_plan/261107_startup_safety_and_account_labels/010_implementation.md new file mode 100644 index 00000000000..644b50f02bd --- /dev/null +++ b/devlog/_plan/261107_startup_safety_and_account_labels/010_implementation.md @@ -0,0 +1,247 @@ +# Implementation: evidence, safe labels and presentation + +Depends on the audited roadmap. Revalidate paths and current `origin/dev` at P. + +## Desktop state contract + +MODIFY `src/service/desktop-startup.ts`: retain `loginEnabled` and existing +fail-closed `viable` semantics; add additive `loginState: enabled | disabled | +unknown` to `DesktopStartupDiagnostic`. Creation: verified macOS registration +and launchctl evidence for the same app yields enabled; stable, unambiguous +disabled evidence for that app yields disabled; every mismatch, failure, +unsupported platform, race and older/default snapshot yields unknown. Never infer +disabled solely from `loginEnabled === false`. Establish the running app through +independent ownership/supervision evidence before attributing a negative registration. +Linux negatives and Windows are unknown until independently proven. Preserve +the existing protection credit and shared Rust/TS enabled-registration parity. +Map the existing two-capture Linux verified positive to enabled; never make the +presentation contradict its existing protection credit. Map both owned and unowned +supervised Linux positives to enabled; `tests/service/service-desktop-startup-linux.test.ts` +must cover the supervised path through health API and both UI presentations. The owned macOS path +currently derives executable identity from registration, so a missing registration +must use independently verified supervision plus durable ownership or stay unknown; +an install ID alone does not establish the executable. + +For a disabled override require a uniquely parsed `disabled`/`true` override on +a valid registration for the independently identified app. Compare registration, +executable identities and override output across two captures, and revalidate +ownership revision and live parent/child process chain across the observation. +For absence require explicit `ENOENT`, independently established app ownership, +two absent observations and revalidated ownership and process identity. A dangling +link, permission error, another error, replacement, other copy or changed input is +unknown. If the required evidence cannot be obtained, absence stays unknown. Test +override changes, absent-to-created registration, ownership and parent changes, +and malformed/ambiguous `print-disabled` output. The tray's `Off` alone is not a +stable negative observation. + +Field chain: creation in `src/service/desktop-startup.ts`; serialization via +`src/codex/autostart-health.ts`, its startup-health subprocess/cache and management +response; deserialization in `gui/src/pages/startup-shared.ts` and dashboard poll; +consumers in `gui/src/startup-health-ui.ts`, `gui/src/pages/startup-sections.tsx`, +`gui/src/pages/dashboard-core-poll.ts`, `gui/src/pages/use-dashboard-data.ts`, +`gui/src/pages/dashboard-overview-head.tsx`, `gui/src/pages/Startup.tsx`, +`gui/classic/src/pages/Startup.tsx`, `gui/classic/src/pages/startup-sections.tsx`, +`gui/classic/src/pages/dashboard-overview-head.tsx`, `gui/src/pages/Home.tsx`, +`gui/src/pages/SetupHub.tsx` and settings-seed types. Audit and test Home/SetupHub +checklist semantics against the evidence state. Search all +`loginEnabled`, `DesktopStartupDiagnostic`, `startupHealth` and status consumers +before coding. Missing/invalid field in an older response means unknown. + +MODIFY GUI decision/presentation files above: a known desktop-owned or supervised +local proxy with unknown, stale, unavailable or missing login evidence presents a +neutral, accessible unverified state, not a warning or green protected verdict. +Verified disabled plus no independently verified protection displays action required; +verified enabled and viable displays protected. Service protection wins; custom +routing and independent real risks retain truthful handling. A total fetch failure +without reliable Desktop context remains an unavailable/error state, not an assumed +Desktop. Keep backend reboot safety conservative. Carry this classification through +settings seeds, dedicated polls, session cache and revisits; invalidate or safely +reinterpret old status-only cached entries. Exercise cold ownership-only fallback, +stale prior enabled/off, failed refresh, older payload, seed/poll ordering and an +independent risk. Preserve fast stale refresh and do not suppress unrelated risks. +Update parent-level stale/failure banners as well as section heroes in both editions; +Classic's dashboard must explicitly handle the new neutral presentation, never fall +through to the green/native state. Render tests for both editions must cover stale, +failed refresh and old cache, plus the other state cases above. +Use a single concise hero explanation and a clear menu action; cards give short +status and recovery contains commands, not repeated prose. An existing desktop +command may be used only if its availability and ownership safeguards are verified; +otherwise give the menu instruction without mutating login items. Preserve keyboard, +semantic status and live-region accessibility. Do not change shared label styles. +For verified-off guidance direct the user through the OpenCodex menu without claiming +that toggling the item resolves a macOS system-disabled override: the tray opens Login +Items settings for that case. Update the unconditional backend recommendation too. +Also update the separate CLI summary in `src/codex/autostart-health.ts` so it does +not claim a menu toggle can clear a system override. + +## OAuth identity contract + +MODIFY `src/server/management/oauth-account-routes.ts` and, if appropriate, a +small existing privacy helper: add a named masked label from the stored identity, +independent of `privacy.maskEmails`. Validate email shape before masking because +`src/lib/privacy.ts` returns malformed non-email input unchanged; absent, malformed +or ambiguous identities produce no label. Keep current email configuration behavior +for other consumers. Never log raw account identifiers or expose raw email through +the new label. Field chain: stored credential -> roster projection -> JSON response +(including quota projection) -> `OAuthAccount` in `gui/src/hooks/useProviderAccountPools.ts` +-> `gui/src/provider-workspace/auth.ts` -> `ProviderAuthPanel.tsx`, dialogs and +confirmation/notification consumers. Check secondary text for raw-email leaks. +Replace `ordinalOnly` with alias, the new masked label only, then localized ordinal +for all OAuth pools, including both Anthropic instances; an old server without the +field must not fall back to the config-dependent email. Update both `OAuthAccount` +and `OAuthAccountRow` in `gui/src/components/provider-workspace/types.ts`, row +headings, accessible names, threshold controls, coupon/grant dialogs and switch, +pause and remove confirmations and notifications. Remove raw `account.email` from +secondary identification and replace the no-roster `oauth.email` status fallback +with a safe label or generic localized status. Test these surfaces with masking off, +missing field, malformed identity, alias and both Anthropic instances. Never use +opaque IDs as labels. +Include `gui/src/pages/tray-data.ts`, both `gui/src/pages/Tray.tsx` and +`gui/classic/src/pages/Tray.tsx`, their accessible switch names and +`gui/tests/tray-data.test.ts`: OAuth rows use alias -> safe masked label -> localized +ordinal while preserving existing Codex and API-key behavior. Pass source kind and +translation context where necessary instead of treating all roster shapes alike. +OAuth secondary identification may use only the server-projected safe label or be +omitted; never derive it from the configurable email, including for older servers. +Render both editions' trays with alias, safe label and a missing safe field plus a +populated legacy email; verify secondary text and accessible switch names. Preserve +Codex and API-key secondary behavior. + +## Regression and documentation map + +MODIFY `tests/service/service-desktop-login-registration.test.ts`, shared login +fixtures as necessary, `tests/service/service-desktop-startup.test.ts`, +`tests/service/service-desktop-startup-health.test.ts`, and startup API/cache +tests (`tests/service/autostart-health.test.ts`, `tests/config/settings-startup-health-seam.test.ts`, +`tests/server/startup-health-packaged-probe.test.ts`) for enabled, verified off, missing/invalid evidence, races, another copy, +stale state and service protection. Preserve Rust fixture parity and run its tests +if the shared fixture or Rust contract changes. MODIFY `tests/gui/startup-health-ui.test.ts` +and dashboard render/poll tests for unknown/off/enabled and old payload, including +single explanation and Home/SetupHub checklist cases. MODIFY `tests/gui/provider-workspace-auth.test.ts` and OAuth +management tests (`tests/oauth/oauth-accounts-api.test.ts`, +`tests/server/account-pool-management-api.test.ts`, `tests/server/provider-account-quota-routes.test.ts`) +for both Anthropic instances, aliases, masked vs unmasked privacy +configuration, malformed identities, quota response and accessible labels. Write +the behavior regressions first, run them and record red before implementing. + +MODIFY every GUI locale for new/changed copy; check complete key parity, not only +lint. MODIFY `docs-site/src/content/docs/guides/desktop-app.md` and relevant +translated descriptions to avoid contradiction; MODIFY the owners identified in +`structure/INDEX.md`, especially `structure/desktop-start-at-login.md`, +`structure/ops/service-and-sidecars.md`, and `structure/gui-and-management-api.md`. +Do not add a new test file without both layout registration and expected fixture. + +Verification: run named focused tests, `bun run test:changed`, GUI tests/lint/build, root typecheck, +`bun run structure:check`, privacy scan, file-size ratchet and GUI i18n plus +React Doctor. Baseline commands and their target coverage are in `000_plan.md`; +revalidate additional commands against package scripts and tests before execution. +No full root suite: owner explicitly requires moderate host load. Run the GUI with +temporary isolated `OPENCODEX_HOME`, `CODEX_HOME`, Claude homes, a non-default +port and synthetic accounts; capture before/after using GOAT browser, upload only +through the `pr-assets` branch and link immutable commit SHAs. Never touch the real +app, service, login items or credentials. Check each conditional path through its +named tests and visually inspect both startup and account surfaces. + +Concrete focused commands (in repository root): `bun test tests/service/service-desktop-login-registration.test.ts tests/service/service-desktop-startup.test.ts tests/service/service-desktop-startup-linux.test.ts tests/service/service-desktop-startup-health.test.ts tests/service/autostart-health.test.ts tests/config/settings-startup-health-seam.test.ts tests/server/startup-health-packaged-probe.test.ts`; `bun test tests/oauth/oauth-accounts-api.test.ts tests/server/account-pool-management-api.test.ts tests/server/provider-account-quota-routes.test.ts tests/gui/provider-workspace-auth.test.ts tests/gui/startup-health-ui.test.ts`; and from `gui/`, `bun test tests/startup-minimal.test.tsx tests/startup-revisit-cache.test.tsx tests/dashboard-contracts.test.ts tests/tray-data.test.ts` plus affected account render tests. The named tests import their targets directly; the baseline startup/UI and OAuth commands ran successfully as recorded in 000, while the added suite commands must be run and recorded before relying on them. Add main/Classic rendered regressions through the real Vite Classic overlay, not raw Bun imports: `vite.classic.config.ts` resolves Classic files to overrides or shared `src` dependencies. Exercise the built `/classic/` through the fixture-backed proxy in GOAT alongside the main edition; assert stale, failed refresh and old cache there, and record observable DOM and screenshots. `bun run build` in `gui/` builds both editions using the overlay. + +Baseline command `bun test tests/config/settings-startup-health-seam.test.ts tests/server/startup-health-packaged-probe.test.ts tests/server/account-pool-management-api.test.ts tests/server/provider-account-quota-routes.test.ts tests/service/service-desktop-startup-linux.test.ts` exited 0 with 110 passing tests; these files directly import the named backend modules. This validates baseline targets, not the future change. + +Isolation must account for `src/config/owner-registry.ts`: a custom OpenCodex +home alone still writes its locator under the default home. Use the existing test +guard and a temporary `OCX_OWNER_REGISTRY_DIR`, and verify all client and service +paths are isolated before starting any proxy. macOS `homedir()` may still resolve +the real account; do not rely on a HOME rewrite alone. A guard refusal is a test +setup failure, not permission to use the real installation. + +Use a fixture-backed test proxy rather than a real desktop process: the existing +management reader seam in `src/server/management/context.ts` injects controlled +startup-health data, and the settings seed and dedicated startup endpoint must both +return the same chosen enabled, disabled or unknown fixture. Seed synthetic OAuth +accounts through test-owned state; block external quota/network probes. Confirm the +real GUI is served by that isolated proxy and inspect the relevant two pages in GOAT. + +## Implementation-cycle planning + +Prior D: roadmap audit passed; the original assumption that the configured email +projection was always masked was rejected. At `51db5cd279` the implementation +architect proposed P-ARCH-01 through P-ARCH-06: accept the additive conservative +contract, independently anchored negative evidence, shared neutral presentation, +state-specific guidance, dedicated masked roster projection and fixture harness. +The plan incorporates the existing Linux positive, Home/SetupHub checklist and CLI +summary. The same architect returned ALIGNED on all six decisions; its `test:changed` +verification omission was accepted and added above. Implementation audit round 1 +identified three medium findings: tray consumer chain, unowned Linux positive and +specific verification targets. All were accepted and added above. Same-architect +reflection initially found missing secondary tray identity coverage; that was added, +and its targeted recheck returned ALIGNED. Same-reviewer re-audit returned PASS, +with all three findings closed and no new high or critical gaps. + +## Implementation evidence so far + +The new UI regressions first failed on the unknown Desktop "Action required" banner +and on an Anthropic alias rendered as an ordinal; the new API regression failed on +the absent masked label. Focused reruns passed after implementation. The GUI suite +ran 3,950 tests with zero failures before the final target-switch guard; that guard was +subsequently covered by `gui/tests/dashboard-resource-deadline.test.tsx` (2 pass). +The latest focused backend run had 296 pass, and the latest GUI slice had 55 pass. +Root typecheck, GUI build/lint/i18n, locale parity, file-size ratchet, privacy scan, +structure check and React Doctor changed-file scan passed. The full root suite was +not run as requested. `bun run test:changed --parallel=1` exceeded the runner's +900-second limit and exited 124; it did not complete, and one GUI classifier edit +occurred during the run. Do not describe it as passing. The focused files and +`ci:full` cover the pending verification; exact-head hosted results are still needed. + +The isolated browser reproduced the old warning and ordinal labels, then observed a +neutral unknown state and masked synthetic account names in both main and Classic. +The proxy ran with temporary homes, test guard, an in-memory provider keychain, +loopback-only sandbox networking, and denied `security` and `launchctl` execution; +quota requests failed under that deliberate network restriction. Before/after images +are stored at the `pr-assets` commit 90a1122d79edcf3ae74d8b3cccc15dd70d9d9f4f. +GOAT's credential expired after its server restarted, so enabled/disabled browser +scenarios and the required fresh independent implementation/security review remain +unverified. The fixture-based tests cover those state mappings, but do not replace +the requested review or the remaining browser observation. + +The implementation also verifies a consistently absent macOS login item for the +independently observed running Desktop, including owned and supervised cases. Its +failing-first regression initially returned unknown; after the fix, 159 desktop/login +registration tests passed. A reappearing item, dangling link or changed ownership +does not receive a disabled verdict. Typecheck, the file-size ratchet, privacy scan +and structure check passed after this addition. The draft PR remains subject to the +same pending review and exact-head CI gates. + +A further presentation regression showed that a verified enabled login item with +an independent viability problem was incorrectly treated as unverified. The focused +UI test failed first (expected `at-risk`, received `unverified`); after restricting +the neutral classification to unknown or stale evidence it passed, 20 tests total. +This does not change the conservative backend protection calculation. + +An interim independent implementation review requested follow-up for the native +tray's account projection, stale readings without Desktop context, process/owner +races, and checklist retries. The new stale and enabled-evidence race regressions +failed before their fixes and pass afterward. A later recheck found that an exception +during the final process read could still retain enabled credit; its regression also +failed first and passed after the fix. Native tray tests passed in an isolated Rust +harness (11 tests); the rendered Home/Setup checklist tests passed (5 tests), including +stale-to-protected and stale-to-disabled transitions. The native panel follows its +existing English UI convention for ordinal fallback. The required fresh GOAT review +on the final head remains pending. + +After those fixes, `cd gui && bun test tests` exited 0 with 3,956 tests and +`cd gui && bun run build` exited 0 for both editions. The focused startup and +checklist suites, root typecheck, GUI lint and i18n lint, privacy scan, structure +check, and file-size ratchet also passed. The root full suite remains unrun by +owner request, and the timed-out `test:changed` run is not passing evidence. + +The branch was combined with `origin/dev` at `35d5010f502a` after the dashboard +layout change landed. The sole documentation conflict was resolved with both +contracts preserved. On that combined tree the GUI suite passed 3,965 tests; +typecheck, GUI lint and i18n lint, structure check and file-size ratchet passed. +Final-head CI, refreshed browser observations and the fresh GOAT review are still +required before integration. + +Exact-head Ubuntu shard 5 on `91f86939c4` found three Windows fixture assertions +that omitted the additive `loginState: "unknown"` field. The Windows supervision +suite reproduced all three failures locally; after updating those expectations, +all 17 tests passed, including the conservative claim-only cases. The failed CI +run remains failed evidence and must be replaced by checks on the corrected head. diff --git a/devlog/_plan/261107_startup_safety_and_account_labels/020_integration.md b/devlog/_plan/261107_startup_safety_and_account_labels/020_integration.md new file mode 100644 index 00000000000..428d7c50eed --- /dev/null +++ b/devlog/_plan/261107_startup_safety_and_account_labels/020_integration.md @@ -0,0 +1,26 @@ +# Integration: exact-head review and merge + +Depends on implementation and its verification. Re-fetch `origin/dev` and inspect +the union for overlap, especially the separate GUI sweep branch; test the union if +behind or overlapping. Never modify its inline-label background rule. + +Use the repository PR template, target `dev`, document exact commands, results and +the full-suite resource exception, and include before/after screenshots via immutable +`pr-assets` commit links. Link the PR to this GOAT thread. The owner explicitly +authorizes push, PR creation and merge. Complete a fresh independent `task.create` +review with role `review` on a model available in `provider.list`; record findings +and fixes, and require PASS on the final head. Inspect and resolve all review threads. +The final fresh reviewer must explicitly review credential-derived identity projection +and masking/privacy boundaries as security review, and its explicit security result +must be recorded with the integration evidence per `MAINTAINERS.md`. + +Verify current PR head and require green exact-head `ci` aggregate, `enforce-target`, +`hygiene`, and all full Ubuntu shards on the final head; missing, pending, skipped, cancelled or +older-head results do not count. Include file-size ratchet, all locale parity, +React Doctor, union checks and local validation evidence. If checks/review are +pending, use GOAT PR watch and end the turn; do not poll. Merge only after every +gate holds, with a short maintainer-integration comment and +`gh pr merge --squash --admin --match-head-commit `. Verify the merge and +report PR URL and actual squash SHA via `origin.report`; unwatch at handback. +Apply `ci:full` before the implementation push as required for subprocess and +source-as-data coverage; verify the label and full-lane selection on the exact head. diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index e8dd0e6f48a..d3e46fbbd37 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -78,7 +78,8 @@ inactive, while only active model-specific weekly limits affect that model famil The dashboard shows Pro, Max 5x, or Ultra beside Claude account quotas, using subscription metadata from the authenticated profile. Both `ultra` and `claude_max_20x` display as Ultra (the existing `ultra` and `max_20x` tier aliases are retained). Unfamiliar validated tiers retain their original name without -failing the usage read. Claude account rows use ordinal labels and omit account IDs and emails. +failing the usage read. Claude account rows prefer your alias, then a masked email, and +fall back to a numbered label when no safe identity is available. You can log in multiple Claude accounts via the Providers dashboard (`ocx login anthropic` / add-account). By default every request uses the **active** account only. diff --git a/docs-site/src/content/docs/guides/desktop-app.md b/docs-site/src/content/docs/guides/desktop-app.md index 73e3822f56d..33a593d15e4 100644 --- a/docs-site/src/content/docs/guides/desktop-app.md +++ b/docs-site/src/content/docs/guides/desktop-app.md @@ -105,7 +105,8 @@ when they verify that the app runs its bundled proxy, even without recorded owne CLI service repairs for that runtime. On macOS and Linux, turn on **Start at Login** in the OpenCodex menu instead of installing a background service. Startup safety credits the app only when its login registration is verified; if it cannot be verified, follow the Desktop guidance in status. -The Startup page then labels the Desktop row **Running · Start at Login not verified**. +The Startup page shows a quiet **Start at Login not verified** note when the app's +registration cannot be checked. A verified disabled registration is shown as action required. On macOS the menu's **Start at Login** item is checked only when Startup safety would verify the same registration. Otherwise the item is unchecked and its label says why: **disabled by macOS** opens System Settings → General → Login Items & Extensions, where @@ -174,7 +175,7 @@ On Linux, the pinned autostart backend writes the login entry to `~/.config/autostart/OpenCodex.desktop`, even when `$XDG_CONFIG_HOME` is set. Startup safety reads that entry and the desktop install-id under `~/.config`. When `$XDG_CONFIG_HOME` points elsewhere, the login session searches a different autostart -directory, so startup safety stays **At risk** instead of crediting the entry. +directory, so startup safety does not credit the entry as restart protection. The entry counts only while it is not marked `Hidden=true` or `X-GNOME-Autostart-enabled=false`, and has no `OnlyShowIn`, `NotShowIn`, or `TryExec` condition. Its `Exec` must be an unquoted absolute path to `opencodex-desktop`, without @@ -183,12 +184,15 @@ executable must still be named `opencodex-desktop`, with its bundled `ocx` besid Startup safety reads the full evidence chain twice and grants protection only when both reads agree. -AppImage installations remain **At risk**: the autostart backend registers the outer +AppImage installations do not receive verified Desktop protection: the autostart backend registers the outer AppImage path, while the live desktop process runs inside its mount. Startup safety cannot verify that relationship and does not credit AppImage protection. -A missing or stale check remains **At risk**. If the desktop app owns the proxy but -protection cannot be verified, reopen OpenCodex and check **Start at Login**. Service -and launcher installation or repair stays disabled while that ownership remains; +A missing or stale check does not earn restart-protection credit. If Desktop owns or +supervises the proxy, the page shows an informational note and asks you to check +**Start at Login** in the OpenCodex menu. Service installation or repair stays disabled +while that ownership remains. The launcher shim can still be installed when diagnostics are fresh; +its Install button is disabled while diagnostics are stale. On Linux without a tray, inspect +`~/.config/autostart/OpenCodex.desktop` and the conditions described above. The command `ocx restore` is still available to undo Codex routing. Normal desktop updates replace the bundled CLI with the fixed startup probe. No local diff --git a/docs-site/src/content/docs/ko/guides/desktop-app.md b/docs-site/src/content/docs/ko/guides/desktop-app.md index 37ff66aba54..6da1eded1fa 100644 --- a/docs-site/src/content/docs/ko/guides/desktop-app.md +++ b/docs-site/src/content/docs/ko/guides/desktop-app.md @@ -81,7 +81,8 @@ macOS, Linux, Windows에서 데스크톱 앱이 번들 프록시를 실행하는 설치하는 대신 OpenCodex 메뉴에서 **Start at Login**을 켜세요. 앱의 로그인 등록이 확인되어야 재시작 후 보호가 적용됩니다. 등록을 확인할 수 없으면 status에 표시되는 데스크톱 안내를 따르세요. 실행 관계 확인은 소유권을 생성하지 않습니다. -이때 시작 안전 페이지의 데스크톱 앱 행에는 **실행 중 · 로그인 시 시작 미확인**이 표시됩니다. +등록을 확인할 수 없으면 시작 안전 페이지에 **로그인 시 시작 미확인** 안내가 표시됩니다. +꺼진 상태가 확인되고 다른 보호가 없으면 조치가 필요하다고 표시합니다. macOS에서 메뉴의 **Start at Login** 항목은 시작 안전 페이지도 같은 등록을 확인할 때만 체크됩니다. 그렇지 않으면 체크가 해제된 채 이유가 라벨에 붙습니다. **disabled by macOS**는 시스템 설정 → 일반 → 로그인 항목 및 확장 프로그램을 열며, 거기서 OpenCodex가 로그인 시 diff --git a/gui/classic/src/pages/Startup.tsx b/gui/classic/src/pages/Startup.tsx index add0b2cc45c..52f9d1ccb60 100644 --- a/gui/classic/src/pages/Startup.tsx +++ b/gui/classic/src/pages/Startup.tsx @@ -5,6 +5,7 @@ import { readSessionListCache, writeSessionListCache } from "../session-list-cac import { Notice } from "../ui"; import { useDataSurface } from "../data-surface"; import { DataSurfaceSkeleton } from "../components/data-surface"; +import { startupPresentation } from "../startup-health-ui"; import { StartupDetailsSection, StartupHeroSection, @@ -249,6 +250,7 @@ export default function Startup({ apiBase, machineApiBase = apiBase, connected = // warm revisits where `data` is already seeded from session cache. const loading = loadState.refreshing; const failed = Boolean(data?.diagnosticStale) || loadState.showError; + const unverified = data !== null && startupPresentation(data, failed) === "unverified"; useEffect(() => { if (!data?.diagnosticStale) return; @@ -346,8 +348,8 @@ export default function Startup({ apiBase, machineApiBase = apiBase, connected = ) : data ? ( <> - {loadState.showError && {t("startup.error")}} - {failed && ( + {loadState.showError && !unverified && {t("startup.error")}} + {failed && !unverified && (
{t("startup.staleData")}
diff --git a/gui/classic/src/pages/Tray.tsx b/gui/classic/src/pages/Tray.tsx index 02399615ce7..d2552296304 100644 --- a/gui/classic/src/pages/Tray.tsx +++ b/gui/classic/src/pages/Tray.tsx @@ -122,7 +122,7 @@ export default function Tray({ resolveApiBase = resolveTrayApiBase }: TrayProps payload.activeCodexAccountId = selection.activeCodexAccountId ?? '__main__'; } catch { /* Missing selection is unknown, never inferred from quota. */ } } - return { name: source.name, switchKind: source.switchKind, accounts: parseAccounts(payload) }; + return { name: source.name, switchKind: source.switchKind, accounts: parseAccounts(payload, source.switchKind, index => t('pws.accountOrdinal', { count: index })) }; } catch { return { name: source.name, switchKind: source.switchKind, accounts: [], unavailable: true }; } })); @@ -187,7 +187,7 @@ export default function Tray({ resolveApiBase = resolveTrayApiBase }: TrayProps window.removeEventListener('blur', blur); window.removeEventListener('focus', focus); window.removeEventListener('opencodex:tray-visibility', nativeVisibility); }; - }, [revision, apiBase]); + }, [revision, apiBase, t]); const number = (value: unknown) => finite(value) ? formatTokens(value, locale) : '—'; const totals = (raw: TrayTotals | undefined) => { diff --git a/gui/classic/src/pages/dashboard-overview-head.tsx b/gui/classic/src/pages/dashboard-overview-head.tsx index c956ca1f4b9..664b809d20d 100644 --- a/gui/classic/src/pages/dashboard-overview-head.tsx +++ b/gui/classic/src/pages/dashboard-overview-head.tsx @@ -94,10 +94,11 @@ export function DashboardOverviewHead({
{startupHealth ? (
{data.desktop && (
-
{t("startup.protection.desktop")}{t(!failed && data.desktop.viable ? "startup.desktopHint" : startupRiskDetailKey(data))}
+
{t("startup.protection.desktop")}{!failed && data.desktop.viable ? {t("startup.desktopHint")} : !failed && data.desktop.running ? {t("startup.desktopRunning")} : null}
- + {startupPresentation(data, failed) === "unverified" ? {t("startup.loginUnverifiedTitle")} : }
)} @@ -261,9 +263,8 @@ export function StartupRecoverySection({ The one-click install/repair buttons above are the primary path; the copyable commands are the fallback. Open by default only while protection is missing. */} -
+
{t(desktopManagesStartup(data) ? "startup.protection.desktop" : "startup.recoveryHint")} - {desktopManagesStartup(data) &&

{t(startupRiskDetailKey(data))}

}
{data.serviceSupported && !desktopManagesStartup(data) && (
diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx index 21da80c8bb7..4ee68d79c16 100644 --- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx +++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx @@ -487,7 +487,7 @@ export default function ProviderAuthPanel({