From a69bf5128c3b700be09190ddcc24178dfc4a1344 Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Fri, 13 Feb 2026 19:17:42 +0000 Subject: [PATCH 1/9] docs(governance): roll sprint forward and reconcile backlog statuses --- CURRENT-SPRINT.md | 114 ++++++++++++---------------------- docs/05-governance/backlog.md | 63 ++++++++++--------- 2 files changed, 71 insertions(+), 106 deletions(-) diff --git a/CURRENT-SPRINT.md b/CURRENT-SPRINT.md index e34e2ec..8fd6802 100644 --- a/CURRENT-SPRINT.md +++ b/CURRENT-SPRINT.md @@ -1,114 +1,78 @@ # Current Sprint -Sprint window: 2026-02-13 onward (Sprint 2) +Sprint window: 2026-02-13 onward (Sprint 3) Owner: Platform Engineering + Security + Frontend -Status: Active (core scope complete; stretch pending) +Status: Active ## Sprint Goal -Advance post-refactor hardening by improving auth lifecycle completeness, IPC integration confidence, and API contract typing safety. +Finish remaining high-value hardening work without re-opening completed Sprint 1/2 scope. ## In Scope (Committed) +- `BL-028` Enforce robust file signature validation parity across all privileged ingress paths (remaining scope beyond Python sidecar baseline). +- `BL-029` Standardize official Python runtime distribution for sidecar bundling (artifact + checksum + CI reproducibility). +- `BL-020` Continue incremental renderer i18n migration for non-lab production-facing surfaces. + +## Explicitly Completed (Do Not Re-Scope) + - `BL-015` Add IdP global sign-out and token revocation flow. +- `BL-021` Consolidate renderer route/nav metadata into a single typed registry. - `BL-023` Expand IPC integration harness for preload-main real handler paths. - `BL-025` Strengthen compile-time typing for API operation contracts end-to-end. +- `BL-026` Exclude lab routes/features from production bundle surface. +- `BL-027` Provide deterministic bundled update demo patch cycle. +- `BL-030` Deterministic packaged Python sidecar runtime baseline. +- CI hardening: targeted `format:check` base fetch now uses `FETCH_HEAD` to avoid non-fast-forward failures. -## Stretch Scope (If Capacity Allows) - -- `BL-020` Complete renderer i18n migration for hardcoded user-facing strings. - -## Additional Delivered Work (Unplanned but Completed) - -- Production hardening: exclude lab routes/navigation from production bundle surface. -- Update model proof: deterministic bundled-file demo patch cycle (`v1` to `v2`) with integrity check and UI diagnostics. - -## Highest Priority Follow-Up +## Blocked / External Dependency -- `BL-028` Enforce robust file signature validation for privileged file ingress (extension + header/magic validation with fail-closed behavior before parser execution). +- `BL-014` remains blocked by IdP vendor token audience behavior. -## Out Of Scope (This Sprint) +## Execution Plan (Coherent + Testable) -- `BL-019`, `BL-022`, `BL-024`. +1. `BL-028A` Privileged ingress parity -## Execution Plan (Coherent + Individually Testable) - -### Workstream A: `BL-015` auth sign-out completeness - -1. `BL-015A` Implement explicit sign-out mode handling in main auth service. - -- Scope: introduce local-only vs provider/global sign-out behavior, including revocation/end-session where supported by IdP metadata/config. -- Done when: sign-out path can deterministically return local clear success and provider sign-out status without exposing secrets. +- Scope: apply centralized extension + signature validation policy to remaining privileged file ingress routes. - Proof: - `pnpm nx run desktop-main:test` - - `pnpm nx run desktop-main:build` - -2. `BL-015B` Surface sign-out mode + outcome through preload and renderer UX. - -- Scope: extend preload/renderer flow to request mode and render user-safe outcomes (local cleared, provider signed out, provider not supported). -- Done when: Auth Session Lab can execute both paths and show accurate status transitions. -- Proof: - - `pnpm nx run renderer:test` - - `pnpm nx run renderer:build` - -### Workstream B: `BL-023` IPC integration hardening + - `pnpm nx run desktop-preload:build` -3. `BL-023A` Add unauthorized sender integration tests with real handlers. +2. `BL-028B` Security telemetry + docs -- Scope: test real handler registration path rejects wrong window/frame sender consistently across privileged channels. -- Done when: unauthorized sender rejection is covered by integration tests, not only unit-level wrapper tests. +- Scope: add structured security events for signature mismatch / rejected file ingress and document supported types. - Proof: - `pnpm nx run desktop-main:test` + - `pnpm docs-lint` -4. `BL-023B` Add correlation-id and timeout propagation integration tests. +3. `BL-029A` Official Python artifact sourcing -- Scope: verify correlation-id continuity and timeout envelope behavior across preload invoke client and main IPC handlers. -- Done when: tests assert stable error codes/correlation behavior for timeout and malformed/failed invoke cases. +- Scope: replace machine-local source dependency with a pinned official distribution artifact + checksum verification. - Proof: - - `pnpm nx run desktop-main:test` - - `pnpm nx run desktop-preload:build` + - `pnpm run python-runtime:prepare-local` + - `pnpm run python-runtime:assert` -### Workstream C: `BL-025` API typing end-to-end +4. `BL-029B` CI reproducible runtime assembly -5. `BL-025A` Introduce operation-to-request/response type map in contracts. - -- Scope: define typed operation map and export helper types for operation params/result payloads. -- Done when: operations can be referenced by key with compile-time request/response inference. +- Scope: ensure runtime bundle can be assembled deterministically in CI from pinned artifact + pinned requirements. - Proof: - - `pnpm nx run contracts:test` - - `pnpm nx run contracts:build` + - `pnpm run build-desktop-main` + - `pnpm forge:make:staging` -6. `BL-025B` Consume typed operation map in preload + main API gateway interfaces. +5. `BL-020A` Incremental i18n uplift (non-lab priority) -- Scope: remove stringly-typed call sites in preload and gateway boundaries where operation payload types can be inferred. -- Done when: `desktop.api.invoke` and main gateway wiring compile with mapped operation types and unchanged runtime behavior. +- Scope: migrate remaining hardcoded renderer strings in non-lab routes to transloco keys/locales. - Proof: - - `pnpm nx run desktop-preload:build` - - `pnpm nx run desktop-main:test` + - `pnpm i18n-check` - `pnpm nx run renderer:build` -### Cross-cut verification gate (after each merged unit) - -- `pnpm unit-test` -- `pnpm integration-test` -- `pnpm runtime:smoke` - ## Exit Criteria -- `BL-015`, `BL-023`, and `BL-025` merged through PR workflow with security checklist completed. -- Existing CI quality gates remain green. -- Docs updated for any changed contracts/flows. +- `BL-028` remaining scope closed and status moved to `Done`. +- `BL-029` implementation design agreed and initial artifact-based implementation landed. +- `BL-020` moved from `Proposed` to `Planned` or `Done` with tracked remaining scope. +- CI remains green on PR and post-merge paths. ## Progress Log -- 2026-02-13: Sprint 1 closure confirmed (`BL-016`, `BL-017`, `BL-018` complete with cross-cut verification). -- 2026-02-13: Sprint 2 initialized with committed scope (`BL-015`, `BL-023`, `BL-025`) and stretch (`BL-020`). -- 2026-02-13: Completed `BL-015A` by introducing explicit sign-out mode (`local` or `global`) and detailed sign-out outcomes in auth contracts, desktop-main service flow, and IPC handling. -- 2026-02-13: Completed `BL-015B` baseline by propagating sign-out mode through preload and Auth Session Lab UX with separate local/global controls and provider outcome messaging. -- 2026-02-13: Completed `BL-023A` by adding real-handler unauthorized-sender integration coverage in `apps/desktop-main/src/ipc/register-ipc-handlers.spec.ts`. -- 2026-02-13: Completed `BL-023B` by adding preload invoke-client tests for malformed responses, timeout behavior, and invoke failures with correlation-id assertions (`apps/desktop-preload/src/invoke-client.spec.ts`) and wiring `desktop-preload:test` target. -- 2026-02-13: Completed `BL-025A` and `BL-025B` baseline by adding operation type maps in contracts and consuming typed operation params/result signatures in desktop API/preload invoke surfaces. -- 2026-02-13: Auth lifecycle stabilization pass completed: bounded OIDC network timeouts in main auth service, auth-page initialization now surfaces true IPC errors, token diagnostics sequencing fixed to avoid startup race, and auth-lab redirect behavior corrected to honor only explicit external `returnUrl`. -- 2026-02-13: Production hardening completed by replacing production route/shell config to exclude lab routes and lab navigation/toggle from production artifacts. -- 2026-02-13: Added bundled update demo proof flow: app startup seeds local runtime demo file to `1.0.0-demo`, update check detects bundled `1.0.1-demo`, apply action validates sha256 and overwrites local demo file, and renderer surfaces source/version/path diagnostics. -- 2026-02-13: Completed `BL-021` by adding a typed renderer route registry (`app-route-registry.ts`) that derives both `app.routes.ts` and `APP_SHELL_CONFIG.navLinks`, removing duplicated route/nav metadata while retaining production route/shell file replacements. +- 2026-02-13: Sprint 3 initialized to focus only on remaining backlog items after Sprint 2 completion. diff --git a/docs/05-governance/backlog.md b/docs/05-governance/backlog.md index 5999e63..52368e8 100644 --- a/docs/05-governance/backlog.md +++ b/docs/05-governance/backlog.md @@ -4,37 +4,38 @@ Owner: Platform Engineering Review cadence: Weekly Last reviewed: 2026-02-13 -| ID | Title | Status | Priority | Area | Source | Owner | Notes | -| ------ | --------------------------------------------------------------------- | -------- | -------- | ------------------------------ | ----------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| BL-001 | Linux packaging strategy for desktop | Deferred | Low | Delivery + Packaging | FEEDBACK.md | Platform | Add Electron Forge Linux makers and release notes once Linux is in scope. | -| BL-002 | Enforce handshake contract-version mismatch path | Planned | Medium | IPC Contracts | FEEDBACK.md | Platform | Main process currently validates schema but does not return a dedicated mismatch error. | -| BL-003 | API operation compile-time typing hardening | Done | Medium | Platform API | Transient FR document (API, archived) | Platform | Implemented via operation type maps and typed invoke signatures across contracts/preload/desktop API (baseline delivered and extended by `BL-025`). | -| BL-004 | Enterprise proxy/TLS support matrix documentation | Deferred | Low | Security + Networking | Transient FR document (API, archived) | Platform | Document expected behaviors for proxy auth, TLS interception, and certificate errors. | -| BL-005 | Offline API queue/replay capability | Proposed | Medium | Platform API | Transient FR document (API, archived) | Platform | Current behavior is fail-fast + retry classification; queue/replay not implemented. | -| BL-006 | Storage capability-scoped authorization model | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Add explicit capability/role rules per storage operation. | -| BL-007 | Expanded data-classification tiers | Deferred | Low | Storage + Governance | Transient FR document (Storage, archived) | Platform | Add `public`, `secret`, and `high-value secret` tiers beyond current baseline. | -| BL-008 | Local Vault phase-2 implementation | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Separate higher-assurance vault capability remains unimplemented. | -| BL-009 | Storage recovery UX flows | Proposed | Medium | UX + Reliability | Transient FR document (Storage, archived) | Frontend | Build user-facing reset/repair/recovery workflow for storage failures. | -| BL-010 | Renderer structured logging adoption | Proposed | Medium | Observability | TASK.md | Frontend | Apply shared structured logs in renderer with IPC correlation IDs for key user flows. | -| BL-011 | Failure UX pattern implementation | Planned | Medium | UX + Reliability | TASK.md | Frontend | Implement documented toast/dialog/inline/offline patterns consistently across features. | -| BL-012 | IPC real-handler contract harness expansion | Done | Medium | Testing + IPC Contracts | TASK.md | Platform | Delivered via real-handler unauthorized sender integration coverage and preload invoke timeout/correlation tests; superseded by scoped execution under `BL-023`. | -| BL-013 | OIDC auth platform (desktop PKCE + secure IPC) | Planned | High | Security + Identity | TASK.md | Platform | Phased backlog and acceptance tests tracked in `docs/05-governance/oidc-auth-backlog.md`. | -| BL-014 | Remove temporary JWT-authorizer client-id audience compatibility | Planned | High | Security + Identity | OIDC integration | Platform + Security | Clerk OAuth access token currently omits `aud`; AWS authorizer temporarily allows both `YOUR_API_AUDIENCE` and OAuth client id. Remove client-id audience once Clerk emits API audience/scopes as required. | -| BL-015 | Add IdP global sign-out and token revocation flow | Done | Medium | Security + Identity | OIDC integration | Platform + Security | Delivered with explicit `local` vs `global` sign-out mode, revocation/end-session capability reporting, and renderer-safe lifecycle messaging. | -| BL-016 | Refactor desktop-main composition root and IPC modularization | Done | High | Desktop Runtime + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; desktop-main composition root split and handler registration modularized. | -| BL-017 | Refactor preload bridge into domain modules with shared invoke client | Done | High | Preload + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; preload segmented into domain APIs with shared invoke/correlation/timeout client. | -| BL-018 | Introduce reusable validated IPC handler factory in desktop-main | Done | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; validated handler factory centralizes sender auth, schema validation, and envelope handling. | -| BL-019 | Decompose OIDC service into smaller capability-focused modules | Proposed | Medium | Security + Identity | Fresh workspace review (2026-02-13) | Platform + Security | Split sign-in flow, discovery/provider client, token lifecycle, and diagnostics concerns currently concentrated in `oidc-service.ts`. | -| BL-020 | Complete renderer i18n migration for hardcoded user-facing strings | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-13) | Frontend | Replace hardcoded labels/messages in renderer feature pages with translation keys and locale entries. | -| BL-021 | Consolidate renderer route/nav metadata into a single typed registry | Done | Medium | Frontend Architecture | Fresh workspace review (2026-02-13) | Frontend | Delivered by introducing a typed renderer route registry that generates both router entries and shell nav links from one source while preserving production file-replacement exclusions. | -| BL-022 | Rationalize thin shell/core/repository libraries | Proposed | Low | Architecture + Maintainability | Fresh workspace review (2026-02-13) | Platform | Either consolidate low-value wrappers or expand with meaningful domain behavior to reduce packaging overhead and clarify boundaries. | -| BL-023 | Expand IPC integration harness for preload-main real handler paths | Done | Medium | Testing + IPC Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered with real-handler unauthorized sender tests and preload invoke malformed/timeout/failure correlation assertions. | -| BL-024 | Standardize structured renderer logging with shared helper adoption | Proposed | Medium | Observability | Fresh workspace review (2026-02-13) | Frontend | Apply structured logging in renderer flows with correlation IDs and redaction-safe details. | -| BL-025 | Strengthen compile-time typing for API operation contracts end-to-end | Done | Medium | Platform API + Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered by introducing operation-to-request/response type maps and consuming them in preload/desktop API invoke surfaces. | -| BL-026 | Exclude lab routes/features from production bundle surface | Done | High | Frontend + Security Posture | Sprint implementation (2026-02-13) | Frontend + Platform | Production route/shell config replacement now removes lab routes/nav/toggle from production artifacts to reduce discoverability/attack surface. | -| BL-027 | Provide deterministic bundled update demo patch cycle | Done | Medium | Delivery + Update Architecture | Sprint implementation (2026-02-13) | Platform | Added local bundled feed demo (`1.0.0-demo` -> `1.0.1-demo`) with hash validation and renderer diagnostics to prove end-to-end update model independent of installer updater infra. | -| BL-028 | Enforce robust file signature validation for privileged file ingress | Planned | High | Security + File Handling | Python sidecar architecture spike | Platform + Security | Add fail-closed extension + magic-byte/header verification before handing files to parser pipelines (renderer fs bridge and Python sidecar). Reject mismatches, log security events, and document supported types. | -| BL-029 | Standardize official Python runtime distribution for sidecar bundling | Proposed | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Replace machine-local Python source dependency with an official pinned distribution artifact (version + checksum), build runtime bundle from that artifact in CI, and enforce reproducible packaged interpreter/dependency state across developer and CI environments. | +| ID | Title | Status | Priority | Area | Source | Owner | Notes | +| ------ | --------------------------------------------------------------------- | ----------- | -------- | ------------------------------ | ----------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BL-001 | Linux packaging strategy for desktop | Deferred | Low | Delivery + Packaging | FEEDBACK.md | Platform | Add Electron Forge Linux makers and release notes once Linux is in scope. | +| BL-002 | Enforce handshake contract-version mismatch path | Planned | Medium | IPC Contracts | FEEDBACK.md | Platform | Main process currently validates schema but does not return a dedicated mismatch error. | +| BL-003 | API operation compile-time typing hardening | Done | Medium | Platform API | Transient FR document (API, archived) | Platform | Implemented via operation type maps and typed invoke signatures across contracts/preload/desktop API (baseline delivered and extended by `BL-025`). | +| BL-004 | Enterprise proxy/TLS support matrix documentation | Deferred | Low | Security + Networking | Transient FR document (API, archived) | Platform | Document expected behaviors for proxy auth, TLS interception, and certificate errors. | +| BL-005 | Offline API queue/replay capability | Proposed | Medium | Platform API | Transient FR document (API, archived) | Platform | Current behavior is fail-fast + retry classification; queue/replay not implemented. | +| BL-006 | Storage capability-scoped authorization model | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Add explicit capability/role rules per storage operation. | +| BL-007 | Expanded data-classification tiers | Deferred | Low | Storage + Governance | Transient FR document (Storage, archived) | Platform | Add `public`, `secret`, and `high-value secret` tiers beyond current baseline. | +| BL-008 | Local Vault phase-2 implementation | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Separate higher-assurance vault capability remains unimplemented. | +| BL-009 | Storage recovery UX flows | Proposed | Medium | UX + Reliability | Transient FR document (Storage, archived) | Frontend | Build user-facing reset/repair/recovery workflow for storage failures. | +| BL-010 | Renderer structured logging adoption | Proposed | Medium | Observability | TASK.md | Frontend | Apply shared structured logs in renderer with IPC correlation IDs for key user flows. | +| BL-011 | Failure UX pattern implementation | Planned | Medium | UX + Reliability | TASK.md | Frontend | Implement documented toast/dialog/inline/offline patterns consistently across features. | +| BL-012 | IPC real-handler contract harness expansion | Done | Medium | Testing + IPC Contracts | TASK.md | Platform | Delivered via real-handler unauthorized sender integration coverage and preload invoke timeout/correlation tests; superseded by scoped execution under `BL-023`. | +| BL-013 | OIDC auth platform (desktop PKCE + secure IPC) | Planned | High | Security + Identity | TASK.md | Platform | Phased backlog and acceptance tests tracked in `docs/05-governance/oidc-auth-backlog.md`. | +| BL-014 | Remove temporary JWT-authorizer client-id audience compatibility | Planned | High | Security + Identity | OIDC integration | Platform + Security | Clerk OAuth access token currently omits `aud`; AWS authorizer temporarily allows both `YOUR_API_AUDIENCE` and OAuth client id. Remove client-id audience once Clerk emits API audience/scopes as required. | +| BL-015 | Add IdP global sign-out and token revocation flow | Done | Medium | Security + Identity | OIDC integration | Platform + Security | Delivered with explicit `local` vs `global` sign-out mode, revocation/end-session capability reporting, and renderer-safe lifecycle messaging. | +| BL-016 | Refactor desktop-main composition root and IPC modularization | Done | High | Desktop Runtime + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; desktop-main composition root split and handler registration modularized. | +| BL-017 | Refactor preload bridge into domain modules with shared invoke client | Done | High | Preload + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; preload segmented into domain APIs with shared invoke/correlation/timeout client. | +| BL-018 | Introduce reusable validated IPC handler factory in desktop-main | Done | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; validated handler factory centralizes sender auth, schema validation, and envelope handling. | +| BL-019 | Decompose OIDC service into smaller capability-focused modules | Proposed | Medium | Security + Identity | Fresh workspace review (2026-02-13) | Platform + Security | Split sign-in flow, discovery/provider client, token lifecycle, and diagnostics concerns currently concentrated in `oidc-service.ts`. | +| BL-020 | Complete renderer i18n migration for hardcoded user-facing strings | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-13) | Frontend | Replace hardcoded labels/messages in renderer feature pages with translation keys and locale entries. | +| BL-021 | Consolidate renderer route/nav metadata into a single typed registry | Done | Medium | Frontend Architecture | Fresh workspace review (2026-02-13) | Frontend | Delivered by introducing a typed renderer route registry that generates both router entries and shell nav links from one source while preserving production file-replacement exclusions. | +| BL-022 | Rationalize thin shell/core/repository libraries | Proposed | Low | Architecture + Maintainability | Fresh workspace review (2026-02-13) | Platform | Either consolidate low-value wrappers or expand with meaningful domain behavior to reduce packaging overhead and clarify boundaries. | +| BL-023 | Expand IPC integration harness for preload-main real handler paths | Done | Medium | Testing + IPC Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered with real-handler unauthorized sender tests and preload invoke malformed/timeout/failure correlation assertions. | +| BL-024 | Standardize structured renderer logging with shared helper adoption | Proposed | Medium | Observability | Fresh workspace review (2026-02-13) | Frontend | Apply structured logging in renderer flows with correlation IDs and redaction-safe details. | +| BL-025 | Strengthen compile-time typing for API operation contracts end-to-end | Done | Medium | Platform API + Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered by introducing operation-to-request/response type maps and consuming them in preload/desktop API invoke surfaces. | +| BL-026 | Exclude lab routes/features from production bundle surface | Done | High | Frontend + Security Posture | Sprint implementation (2026-02-13) | Frontend + Platform | Production route/shell config replacement now removes lab routes/nav/toggle from production artifacts to reduce discoverability/attack surface. | +| BL-027 | Provide deterministic bundled update demo patch cycle | Done | Medium | Delivery + Update Architecture | Sprint implementation (2026-02-13) | Platform | Added local bundled feed demo (`1.0.0-demo` -> `1.0.1-demo`) with hash validation and renderer diagnostics to prove end-to-end update model independent of installer updater infra. | +| BL-028 | Enforce robust file signature validation for privileged file ingress | In Progress | High | Security + File Handling | Python sidecar architecture spike | Platform + Security | Python sidecar ingress path now enforces `.pdf` extension + `%PDF-` header checks with fail-closed behavior. Remaining scope: align renderer fs bridge parity, centralize supported signatures, and add security event logging/coverage for all privileged ingress paths. | +| BL-029 | Standardize official Python runtime distribution for sidecar bundling | Proposed | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Replace machine-local Python source dependency with an official pinned distribution artifact (version + checksum), build runtime bundle from that artifact in CI, and enforce reproducible packaged interpreter/dependency state across developer and CI environments. | +| BL-030 | Deterministic packaged Python sidecar runtime baseline | Done | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Delivered deterministic runtime manifest/assert/sync flow, pinned runtime dependency install (`requirements-runtime.txt`), packaged-build bundled-runtime enforcement (no system fallback), and runtime executable diagnostics proving packaged interpreter path. | ## Status Definitions From d32778f3dd88224eaf34537088bc0ede4e14235b Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Sat, 14 Feb 2026 07:34:39 +0000 Subject: [PATCH 2/9] feat(security): centralize privileged file ingress policy and token consumption --- CURRENT-SPRINT.md | 49 ++--- .../src/ipc/consume-selected-file-token.ts | 53 ++++++ .../src/ipc/file-handlers.spec.ts | 168 ++++++++++++++++++ apps/desktop-main/src/ipc/file-handlers.ts | 53 +++--- .../src/ipc/file-ingress-policy.ts | 99 +++++++++++ apps/desktop-main/src/ipc/python-handlers.ts | 103 +++-------- docs/05-governance/backlog.md | 72 ++++---- 7 files changed, 450 insertions(+), 147 deletions(-) create mode 100644 apps/desktop-main/src/ipc/consume-selected-file-token.ts create mode 100644 apps/desktop-main/src/ipc/file-handlers.spec.ts create mode 100644 apps/desktop-main/src/ipc/file-ingress-policy.ts diff --git a/CURRENT-SPRINT.md b/CURRENT-SPRINT.md index 8fd6802..cc6126e 100644 --- a/CURRENT-SPRINT.md +++ b/CURRENT-SPRINT.md @@ -1,18 +1,25 @@ # Current Sprint -Sprint window: 2026-02-13 onward (Sprint 3) -Owner: Platform Engineering + Security + Frontend +Sprint window: 2026-02-14 onward (Sprint 4) +Owner: Platform Engineering + Security Status: Active ## Sprint Goal -Finish remaining high-value hardening work without re-opening completed Sprint 1/2 scope. +Increase security and runtime determinism in privileged execution paths before additional feature expansion. -## In Scope (Committed) +## In Scope (Committed, Highest Value First) -- `BL-028` Enforce robust file signature validation parity across all privileged ingress paths (remaining scope beyond Python sidecar baseline). +- `BL-028` Enforce robust file signature validation parity across all privileged ingress paths. +- `BL-033` Centralize privileged file ingress policy across all IPC file routes. - `BL-029` Standardize official Python runtime distribution for sidecar bundling (artifact + checksum + CI reproducibility). -- `BL-020` Continue incremental renderer i18n migration for non-lab production-facing surfaces. +- `BL-032` Standardize IPC handler failure envelope and correlation guarantees. + +## Out of Scope (This Sprint) + +- `BL-020` Renderer i18n uplift deferred while single-maintainer workflow remains. +- `BL-034` / `BL-035` i18n architecture enhancements deferred. +- `BL-038` sidecar transport ADR deferred unless risk profile changes. ## Explicitly Completed (Do Not Re-Scope) @@ -31,48 +38,48 @@ Finish remaining high-value hardening work without re-opening completed Sprint 1 ## Execution Plan (Coherent + Testable) -1. `BL-028A` Privileged ingress parity +1. `BL-033A` Shared ingress policy module -- Scope: apply centralized extension + signature validation policy to remaining privileged file ingress routes. +- Scope: introduce one shared policy for extension/signature/size validation and consume it from all privileged file ingress handlers. - Proof: - `pnpm nx run desktop-main:test` - `pnpm nx run desktop-preload:build` -2. `BL-028B` Security telemetry + docs +2. `BL-028A` Close parity gaps + fail-closed behavior -- Scope: add structured security events for signature mismatch / rejected file ingress and document supported types. +- Scope: remove remaining route-by-route differences, enforce consistent rejection semantics, and add structured security events. - Proof: - `pnpm nx run desktop-main:test` - `pnpm docs-lint` -3. `BL-029A` Official Python artifact sourcing +3. `BL-029A` Official artifact + checksum flow -- Scope: replace machine-local source dependency with a pinned official distribution artifact + checksum verification. +- Scope: source Python runtime from pinned official artifact, verify checksum, and prepare deterministic runtime bundle inputs. - Proof: - `pnpm run python-runtime:prepare-local` - `pnpm run python-runtime:assert` 4. `BL-029B` CI reproducible runtime assembly -- Scope: ensure runtime bundle can be assembled deterministically in CI from pinned artifact + pinned requirements. +- Scope: guarantee package builds use prepared runtime artifact path and pinned runtime requirements in CI. - Proof: - `pnpm run build-desktop-main` - `pnpm forge:make:staging` -5. `BL-020A` Incremental i18n uplift (non-lab priority) +5. `BL-032A` IPC failure envelope normalization -- Scope: migrate remaining hardcoded renderer strings in non-lab routes to transloco keys/locales. +- Scope: ensure validated handler factory normalizes validation and unexpected runtime failures into a single safe contract with correlation IDs. - Proof: - - `pnpm i18n-check` - - `pnpm nx run renderer:build` + - `pnpm nx run desktop-main:test` + - `pnpm nx run desktop-preload:test` ## Exit Criteria -- `BL-028` remaining scope closed and status moved to `Done`. -- `BL-029` implementation design agreed and initial artifact-based implementation landed. -- `BL-020` moved from `Proposed` to `Planned` or `Done` with tracked remaining scope. +- `BL-028` and `BL-033` moved to `Done`. +- `BL-029` moved to `Done` or `In Progress` with artifact/checksum path merged and CI proof complete. +- `BL-032` moved to `Done` with integration test coverage proving envelope consistency. - CI remains green on PR and post-merge paths. ## Progress Log -- 2026-02-13: Sprint 3 initialized to focus only on remaining backlog items after Sprint 2 completion. +- 2026-02-14: Sprint reprioritized to security/runtime determinism; i18n work deferred. diff --git a/apps/desktop-main/src/ipc/consume-selected-file-token.ts b/apps/desktop-main/src/ipc/consume-selected-file-token.ts new file mode 100644 index 0000000..7ed9418 --- /dev/null +++ b/apps/desktop-main/src/ipc/consume-selected-file-token.ts @@ -0,0 +1,53 @@ +import path from 'node:path'; +import { BrowserWindow, type IpcMainInvokeEvent } from 'electron'; +import { + asFailure, + asSuccess, + type DesktopResult, +} from '@electron-foundation/contracts'; +import type { MainIpcContext } from './handler-context'; + +export type ConsumedSelectedFileToken = { + filePath: string; + fileName: string; +}; + +export const consumeSelectedFileToken = ( + event: IpcMainInvokeEvent, + fileToken: string, + context: MainIpcContext, + correlationId?: string, +): DesktopResult => { + const selected = context.selectedFileTokens.get(fileToken); + if (!selected || selected.expiresAt <= Date.now()) { + context.selectedFileTokens.delete(fileToken); + return asFailure( + 'FS/INVALID_TOKEN', + 'The selected file token is invalid or expired.', + undefined, + false, + correlationId, + ); + } + + const senderWindowId = BrowserWindow.fromWebContents(event.sender)?.id; + if (senderWindowId !== selected.windowId) { + context.selectedFileTokens.delete(fileToken); + return asFailure( + 'FS/INVALID_TOKEN_SCOPE', + 'Selected file token was issued for a different window.', + { + senderWindowId: senderWindowId ?? null, + tokenWindowId: selected.windowId, + }, + false, + correlationId, + ); + } + + context.selectedFileTokens.delete(fileToken); + return asSuccess({ + filePath: selected.filePath, + fileName: path.basename(selected.filePath), + }); +}; diff --git a/apps/desktop-main/src/ipc/file-handlers.spec.ts b/apps/desktop-main/src/ipc/file-handlers.spec.ts new file mode 100644 index 0000000..0f10219 --- /dev/null +++ b/apps/desktop-main/src/ipc/file-handlers.spec.ts @@ -0,0 +1,168 @@ +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import type { IpcMain, IpcMainInvokeEvent } from 'electron'; +import { + CONTRACT_VERSION, + IPC_CHANNELS, + type DesktopResult, +} from '@electron-foundation/contracts'; +import type { MainIpcContext } from './handler-context'; +import { registerFileIpcHandlers } from './file-handlers'; + +vi.mock('electron', () => ({ + BrowserWindow: { + fromWebContents: vi.fn(() => ({ id: 42 })), + }, + dialog: { + showOpenDialog: vi.fn(), + }, +})); + +describe('registerFileIpcHandlers', () => { + const senderWindowId = 42; + + const createRequest = (correlationId: string, payload: unknown) => ({ + contractVersion: CONTRACT_VERSION, + correlationId, + payload, + }); + + const createEvent = () => + ({ + sender: {}, + }) as IpcMainInvokeEvent; + + const registerHandlers = ( + context: MainIpcContext, + ): Map< + string, + (event: IpcMainInvokeEvent, payload: unknown) => Promise + > => { + const handlers = new Map< + string, + (event: IpcMainInvokeEvent, payload: unknown) => Promise + >(); + + const ipcMain = { + handle: (channel: string, handler: (...args: unknown[]) => unknown) => { + handlers.set( + channel, + handler as ( + event: IpcMainInvokeEvent, + payload: unknown, + ) => Promise, + ); + }, + } as unknown as IpcMain; + + registerFileIpcHandlers(ipcMain, context); + return handlers; + }; + + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'file-ipc-')); + }); + + afterEach(async () => { + vi.restoreAllMocks(); + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + const createContext = ( + selectedFileTokens?: MainIpcContext['selectedFileTokens'], + ): MainIpcContext => ({ + appVersion: '0.0.0-test', + appEnvironment: 'development', + fileTokenTtlMs: 5 * 60_000, + selectedFileTokens: selectedFileTokens ?? new Map(), + getCorrelationId: (payload: unknown) => + payload && + typeof payload === 'object' && + 'correlationId' in payload && + typeof (payload as { correlationId?: unknown }).correlationId === 'string' + ? (payload as { correlationId: string }).correlationId + : undefined, + assertAuthorizedSender: () => null as DesktopResult | null, + getOidcService: vi.fn(() => null), + getStorageGateway: vi.fn(), + invokeApiOperation: vi.fn(), + getApiOperationDiagnostics: vi.fn(), + getDemoUpdater: vi.fn(() => null), + getPythonSidecar: vi.fn(() => null), + logEvent: vi.fn(), + }); + + it('reads selected text file when extension is allowed', async () => { + const filePath = path.join(tempDir, 'notes.txt'); + await fs.writeFile(filePath, 'hello world', 'utf8'); + + const selectedFileTokens = new Map([ + [ + 'token-1', + { + filePath, + expiresAt: Date.now() + 60_000, + windowId: senderWindowId, + }, + ], + ]); + + const handlers = registerHandlers(createContext(selectedFileTokens)); + const readHandler = handlers.get(IPC_CHANNELS.fsReadTextFile); + expect(readHandler).toBeDefined(); + + const response = await readHandler!( + createEvent(), + createRequest('corr-fs-read-ok', { + fileToken: 'token-1', + encoding: 'utf8', + }), + ); + + expect(response).toMatchObject({ + ok: true, + data: { content: 'hello world' }, + }); + expect(selectedFileTokens.has('token-1')).toBe(false); + }); + + it('rejects selected file when extension is not allowed for text read', async () => { + const filePath = path.join(tempDir, 'report.pdf'); + await fs.writeFile(filePath, Buffer.from('%PDF-1.7\nsafe\n', 'ascii')); + + const selectedFileTokens = new Map([ + [ + 'token-2', + { + filePath, + expiresAt: Date.now() + 60_000, + windowId: senderWindowId, + }, + ], + ]); + + const handlers = registerHandlers(createContext(selectedFileTokens)); + const readHandler = handlers.get(IPC_CHANNELS.fsReadTextFile); + expect(readHandler).toBeDefined(); + + const response = await readHandler!( + createEvent(), + createRequest('corr-fs-read-bad-ext', { + fileToken: 'token-2', + encoding: 'utf8', + }), + ); + + expect(response).toMatchObject({ + ok: false, + error: { + code: 'FS/UNSUPPORTED_FILE_TYPE', + correlationId: 'corr-fs-read-bad-ext', + }, + }); + expect(selectedFileTokens.has('token-2')).toBe(false); + }); +}); diff --git a/apps/desktop-main/src/ipc/file-handlers.ts b/apps/desktop-main/src/ipc/file-handlers.ts index bf9cc5f..dc1eff5 100644 --- a/apps/desktop-main/src/ipc/file-handlers.ts +++ b/apps/desktop-main/src/ipc/file-handlers.ts @@ -10,6 +10,8 @@ import { readTextFileRequestSchema, } from '@electron-foundation/contracts'; import type { MainIpcContext } from './handler-context'; +import { consumeSelectedFileToken } from './consume-selected-file-token'; +import { evaluateFileIngressPolicy } from './file-ingress-policy'; import { registerValidatedHandler } from './register-validated-handler'; export const registerFileIpcHandlers = ( @@ -66,38 +68,49 @@ export const registerFileIpcHandlers = ( context, handler: async (event, request) => { try { - const selected = context.selectedFileTokens.get( + const consumed = consumeSelectedFileToken( + event, request.payload.fileToken, + context, + request.correlationId, ); - if (!selected || selected.expiresAt <= Date.now()) { - context.selectedFileTokens.delete(request.payload.fileToken); - return asFailure( - 'FS/INVALID_TOKEN', - 'The selected file token is invalid or expired.', - undefined, - false, - request.correlationId, - ); + if (!consumed.ok) { + return consumed; } - const senderWindowId = BrowserWindow.fromWebContents(event.sender)?.id; - if (senderWindowId !== selected.windowId) { - context.selectedFileTokens.delete(request.payload.fileToken); + const policy = await evaluateFileIngressPolicy( + consumed.data.filePath, + 'textRead', + ); + if (policy.kind !== 'ok') { + if (policy.kind === 'unsupported-extension') { + return asFailure( + 'FS/UNSUPPORTED_FILE_TYPE', + 'Selected file type is not supported for text read.', + { + fileName: policy.fileName, + extension: policy.extension, + allowedExtensions: policy.allowedExtensions, + }, + false, + request.correlationId, + ); + } + return asFailure( - 'FS/INVALID_TOKEN_SCOPE', - 'Selected file token was issued for a different window.', + 'FS/FILE_SIGNATURE_MISMATCH', + 'Selected file signature did not match expected content.', { - senderWindowId: senderWindowId ?? null, - tokenWindowId: selected.windowId, + fileName: policy.fileName, + headerHex: policy.headerHex, + expectedHex: policy.expectedHex, }, false, request.correlationId, ); } - context.selectedFileTokens.delete(request.payload.fileToken); - - const content = await fs.readFile(selected.filePath, { + const content = await fs.readFile(consumed.data.filePath, { encoding: request.payload.encoding, }); diff --git a/apps/desktop-main/src/ipc/file-ingress-policy.ts b/apps/desktop-main/src/ipc/file-ingress-policy.ts new file mode 100644 index 0000000..050290f --- /dev/null +++ b/apps/desktop-main/src/ipc/file-ingress-policy.ts @@ -0,0 +1,99 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +type FileSignatureRule = { + magic: Buffer; + offset: number; +}; + +type FileIngressPolicy = { + allowedExtensions: string[]; + signature?: FileSignatureRule; +}; + +const FILE_INGRESS_POLICIES: Record = { + textRead: { + allowedExtensions: ['.txt', '.md', '.json', '.log'], + }, + pdfInspect: { + allowedExtensions: ['.pdf'], + signature: { + magic: Buffer.from('%PDF-', 'ascii'), + offset: 0, + }, + }, +}; + +export type FileIngressPolicyName = keyof typeof FILE_INGRESS_POLICIES; + +export type FileIngressPolicyResult = + | { + kind: 'ok'; + fileName: string; + } + | { + kind: 'unsupported-extension'; + fileName: string; + extension: string; + allowedExtensions: readonly string[]; + } + | { + kind: 'signature-mismatch'; + fileName: string; + headerHex: string; + expectedHex: string; + }; + +const readSignatureWindowHex = async ( + filePath: string, + offset: number, + length: number, +) => { + const file = await fs.open(filePath, 'r'); + try { + const header = Buffer.alloc(length); + const readResult = await file.read(header, 0, header.length, offset); + return header.subarray(0, readResult.bytesRead).toString('hex'); + } finally { + await file.close(); + } +}; + +export const evaluateFileIngressPolicy = async ( + filePath: string, + policyName: FileIngressPolicyName, +): Promise => { + const policy = FILE_INGRESS_POLICIES[policyName]; + const fileName = path.basename(filePath); + const extension = path.extname(filePath).toLowerCase(); + + if (!policy.allowedExtensions.includes(extension)) { + return { + kind: 'unsupported-extension', + fileName, + extension, + allowedExtensions: policy.allowedExtensions, + }; + } + + if (!policy.signature) { + return { kind: 'ok', fileName }; + } + + const expectedHex = policy.signature.magic.toString('hex'); + const headerHex = await readSignatureWindowHex( + filePath, + policy.signature.offset, + policy.signature.magic.length, + ); + if (headerHex !== expectedHex) { + return { + kind: 'signature-mismatch', + fileName, + headerHex, + expectedHex, + }; + } + + return { kind: 'ok', fileName }; +}; diff --git a/apps/desktop-main/src/ipc/python-handlers.ts b/apps/desktop-main/src/ipc/python-handlers.ts index f447441..38080d3 100644 --- a/apps/desktop-main/src/ipc/python-handlers.ts +++ b/apps/desktop-main/src/ipc/python-handlers.ts @@ -1,6 +1,4 @@ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import { BrowserWindow, type IpcMain, type IpcMainInvokeEvent } from 'electron'; +import { type IpcMain } from 'electron'; import { asFailure, asSuccess, @@ -10,64 +8,14 @@ import { pythonStopRequestSchema, } from '@electron-foundation/contracts'; import type { MainIpcContext } from './handler-context'; +import { consumeSelectedFileToken } from './consume-selected-file-token'; +import { evaluateFileIngressPolicy } from './file-ingress-policy'; import { registerValidatedHandler } from './register-validated-handler'; export const registerPythonIpcHandlers = ( ipcMain: IpcMain, context: MainIpcContext, ) => { - const resolveFileTokenPath = async ( - event: IpcMainInvokeEvent, - fileToken: string, - correlationId?: string, - ) => { - const selected = context.selectedFileTokens.get(fileToken); - if (!selected || selected.expiresAt <= Date.now()) { - context.selectedFileTokens.delete(fileToken); - return asFailure( - 'FS/INVALID_TOKEN', - 'The selected file token is invalid or expired.', - undefined, - false, - correlationId, - ); - } - - const senderWindowId = BrowserWindow.fromWebContents(event.sender)?.id; - if (senderWindowId !== selected.windowId) { - context.selectedFileTokens.delete(fileToken); - return asFailure( - 'FS/INVALID_TOKEN_SCOPE', - 'Selected file token was issued for a different window.', - { - senderWindowId: senderWindowId ?? null, - tokenWindowId: selected.windowId, - }, - false, - correlationId, - ); - } - - context.selectedFileTokens.delete(fileToken); - return asSuccess({ path: selected.filePath }); - }; - - const looksLikePdf = async (filePath: string) => { - const file = await fs.open(filePath, 'r'); - try { - const header = Buffer.alloc(5); - const readResult = await file.read(header, 0, header.length, 0); - const bytesRead = readResult.bytesRead; - const headerSlice = header.subarray(0, bytesRead); - return { - valid: headerSlice.toString('ascii') === '%PDF-', - headerHex: headerSlice.toString('hex'), - }; - } finally { - await file.close(); - } - }; - registerValidatedHandler({ ipcMain, channel: IPC_CHANNELS.pythonProbe, @@ -106,34 +54,41 @@ export const registerPythonIpcHandlers = ( ); } - const resolved = await resolveFileTokenPath( + const consumed = consumeSelectedFileToken( event, request.payload.fileToken, + context, request.correlationId, ); - if (!resolved.ok) { - return resolved; + if (!consumed.ok) { + return consumed; } - const filePath = resolved.data.path; - if (path.extname(filePath).toLowerCase() !== '.pdf') { - return asFailure( - 'PYTHON/UNSUPPORTED_FILE_TYPE', - 'Only PDF files are supported for this operation.', - { fileName: path.basename(filePath) }, - false, - request.correlationId, - ); - } + const policy = await evaluateFileIngressPolicy( + consumed.data.filePath, + 'pdfInspect', + ); + if (policy.kind !== 'ok') { + if (policy.kind === 'unsupported-extension') { + return asFailure( + 'PYTHON/UNSUPPORTED_FILE_TYPE', + 'Only PDF files are supported for this operation.', + { + fileName: policy.fileName, + extension: policy.extension, + }, + false, + request.correlationId, + ); + } - const header = await looksLikePdf(filePath); - if (!header.valid) { return asFailure( 'PYTHON/FILE_SIGNATURE_MISMATCH', 'Selected file does not match expected PDF signature.', { - fileName: path.basename(filePath), - headerHex: header.headerHex, + fileName: policy.fileName, + headerHex: policy.headerHex, + expectedHex: policy.expectedHex, }, false, request.correlationId, @@ -141,14 +96,14 @@ export const registerPythonIpcHandlers = ( } try { - const diagnostics = await sidecar.inspectPdf(filePath); + const diagnostics = await sidecar.inspectPdf(consumed.data.filePath); return asSuccess(diagnostics); } catch (error) { return asFailure( 'PYTHON/INSPECT_FAILED', 'Python sidecar failed to inspect selected PDF.', { - fileName: path.basename(filePath), + fileName: consumed.data.fileName, message: error instanceof Error ? error.message : String(error), }, false, diff --git a/docs/05-governance/backlog.md b/docs/05-governance/backlog.md index 52368e8..5eda4cf 100644 --- a/docs/05-governance/backlog.md +++ b/docs/05-governance/backlog.md @@ -4,38 +4,46 @@ Owner: Platform Engineering Review cadence: Weekly Last reviewed: 2026-02-13 -| ID | Title | Status | Priority | Area | Source | Owner | Notes | -| ------ | --------------------------------------------------------------------- | ----------- | -------- | ------------------------------ | ----------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| BL-001 | Linux packaging strategy for desktop | Deferred | Low | Delivery + Packaging | FEEDBACK.md | Platform | Add Electron Forge Linux makers and release notes once Linux is in scope. | -| BL-002 | Enforce handshake contract-version mismatch path | Planned | Medium | IPC Contracts | FEEDBACK.md | Platform | Main process currently validates schema but does not return a dedicated mismatch error. | -| BL-003 | API operation compile-time typing hardening | Done | Medium | Platform API | Transient FR document (API, archived) | Platform | Implemented via operation type maps and typed invoke signatures across contracts/preload/desktop API (baseline delivered and extended by `BL-025`). | -| BL-004 | Enterprise proxy/TLS support matrix documentation | Deferred | Low | Security + Networking | Transient FR document (API, archived) | Platform | Document expected behaviors for proxy auth, TLS interception, and certificate errors. | -| BL-005 | Offline API queue/replay capability | Proposed | Medium | Platform API | Transient FR document (API, archived) | Platform | Current behavior is fail-fast + retry classification; queue/replay not implemented. | -| BL-006 | Storage capability-scoped authorization model | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Add explicit capability/role rules per storage operation. | -| BL-007 | Expanded data-classification tiers | Deferred | Low | Storage + Governance | Transient FR document (Storage, archived) | Platform | Add `public`, `secret`, and `high-value secret` tiers beyond current baseline. | -| BL-008 | Local Vault phase-2 implementation | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Separate higher-assurance vault capability remains unimplemented. | -| BL-009 | Storage recovery UX flows | Proposed | Medium | UX + Reliability | Transient FR document (Storage, archived) | Frontend | Build user-facing reset/repair/recovery workflow for storage failures. | -| BL-010 | Renderer structured logging adoption | Proposed | Medium | Observability | TASK.md | Frontend | Apply shared structured logs in renderer with IPC correlation IDs for key user flows. | -| BL-011 | Failure UX pattern implementation | Planned | Medium | UX + Reliability | TASK.md | Frontend | Implement documented toast/dialog/inline/offline patterns consistently across features. | -| BL-012 | IPC real-handler contract harness expansion | Done | Medium | Testing + IPC Contracts | TASK.md | Platform | Delivered via real-handler unauthorized sender integration coverage and preload invoke timeout/correlation tests; superseded by scoped execution under `BL-023`. | -| BL-013 | OIDC auth platform (desktop PKCE + secure IPC) | Planned | High | Security + Identity | TASK.md | Platform | Phased backlog and acceptance tests tracked in `docs/05-governance/oidc-auth-backlog.md`. | -| BL-014 | Remove temporary JWT-authorizer client-id audience compatibility | Planned | High | Security + Identity | OIDC integration | Platform + Security | Clerk OAuth access token currently omits `aud`; AWS authorizer temporarily allows both `YOUR_API_AUDIENCE` and OAuth client id. Remove client-id audience once Clerk emits API audience/scopes as required. | -| BL-015 | Add IdP global sign-out and token revocation flow | Done | Medium | Security + Identity | OIDC integration | Platform + Security | Delivered with explicit `local` vs `global` sign-out mode, revocation/end-session capability reporting, and renderer-safe lifecycle messaging. | -| BL-016 | Refactor desktop-main composition root and IPC modularization | Done | High | Desktop Runtime + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; desktop-main composition root split and handler registration modularized. | -| BL-017 | Refactor preload bridge into domain modules with shared invoke client | Done | High | Preload + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; preload segmented into domain APIs with shared invoke/correlation/timeout client. | -| BL-018 | Introduce reusable validated IPC handler factory in desktop-main | Done | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; validated handler factory centralizes sender auth, schema validation, and envelope handling. | -| BL-019 | Decompose OIDC service into smaller capability-focused modules | Proposed | Medium | Security + Identity | Fresh workspace review (2026-02-13) | Platform + Security | Split sign-in flow, discovery/provider client, token lifecycle, and diagnostics concerns currently concentrated in `oidc-service.ts`. | -| BL-020 | Complete renderer i18n migration for hardcoded user-facing strings | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-13) | Frontend | Replace hardcoded labels/messages in renderer feature pages with translation keys and locale entries. | -| BL-021 | Consolidate renderer route/nav metadata into a single typed registry | Done | Medium | Frontend Architecture | Fresh workspace review (2026-02-13) | Frontend | Delivered by introducing a typed renderer route registry that generates both router entries and shell nav links from one source while preserving production file-replacement exclusions. | -| BL-022 | Rationalize thin shell/core/repository libraries | Proposed | Low | Architecture + Maintainability | Fresh workspace review (2026-02-13) | Platform | Either consolidate low-value wrappers or expand with meaningful domain behavior to reduce packaging overhead and clarify boundaries. | -| BL-023 | Expand IPC integration harness for preload-main real handler paths | Done | Medium | Testing + IPC Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered with real-handler unauthorized sender tests and preload invoke malformed/timeout/failure correlation assertions. | -| BL-024 | Standardize structured renderer logging with shared helper adoption | Proposed | Medium | Observability | Fresh workspace review (2026-02-13) | Frontend | Apply structured logging in renderer flows with correlation IDs and redaction-safe details. | -| BL-025 | Strengthen compile-time typing for API operation contracts end-to-end | Done | Medium | Platform API + Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered by introducing operation-to-request/response type maps and consuming them in preload/desktop API invoke surfaces. | -| BL-026 | Exclude lab routes/features from production bundle surface | Done | High | Frontend + Security Posture | Sprint implementation (2026-02-13) | Frontend + Platform | Production route/shell config replacement now removes lab routes/nav/toggle from production artifacts to reduce discoverability/attack surface. | -| BL-027 | Provide deterministic bundled update demo patch cycle | Done | Medium | Delivery + Update Architecture | Sprint implementation (2026-02-13) | Platform | Added local bundled feed demo (`1.0.0-demo` -> `1.0.1-demo`) with hash validation and renderer diagnostics to prove end-to-end update model independent of installer updater infra. | -| BL-028 | Enforce robust file signature validation for privileged file ingress | In Progress | High | Security + File Handling | Python sidecar architecture spike | Platform + Security | Python sidecar ingress path now enforces `.pdf` extension + `%PDF-` header checks with fail-closed behavior. Remaining scope: align renderer fs bridge parity, centralize supported signatures, and add security event logging/coverage for all privileged ingress paths. | -| BL-029 | Standardize official Python runtime distribution for sidecar bundling | Proposed | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Replace machine-local Python source dependency with an official pinned distribution artifact (version + checksum), build runtime bundle from that artifact in CI, and enforce reproducible packaged interpreter/dependency state across developer and CI environments. | -| BL-030 | Deterministic packaged Python sidecar runtime baseline | Done | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Delivered deterministic runtime manifest/assert/sync flow, pinned runtime dependency install (`requirements-runtime.txt`), packaged-build bundled-runtime enforcement (no system fallback), and runtime executable diagnostics proving packaged interpreter path. | +| ID | Title | Status | Priority | Area | Source | Owner | Notes | +| ------ | --------------------------------------------------------------------- | ----------- | -------- | ------------------------------- | ----------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BL-001 | Linux packaging strategy for desktop | Deferred | Low | Delivery + Packaging | FEEDBACK.md | Platform | Add Electron Forge Linux makers and release notes once Linux is in scope. | +| BL-002 | Enforce handshake contract-version mismatch path | Planned | Medium | IPC Contracts | FEEDBACK.md | Platform | Main process currently validates schema but does not return a dedicated mismatch error. | +| BL-003 | API operation compile-time typing hardening | Done | Medium | Platform API | Transient FR document (API, archived) | Platform | Implemented via operation type maps and typed invoke signatures across contracts/preload/desktop API (baseline delivered and extended by `BL-025`). | +| BL-004 | Enterprise proxy/TLS support matrix documentation | Deferred | Low | Security + Networking | Transient FR document (API, archived) | Platform | Document expected behaviors for proxy auth, TLS interception, and certificate errors. | +| BL-005 | Offline API queue/replay capability | Proposed | Medium | Platform API | Transient FR document (API, archived) | Platform | Current behavior is fail-fast + retry classification; queue/replay not implemented. | +| BL-006 | Storage capability-scoped authorization model | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Add explicit capability/role rules per storage operation. | +| BL-007 | Expanded data-classification tiers | Deferred | Low | Storage + Governance | Transient FR document (Storage, archived) | Platform | Add `public`, `secret`, and `high-value secret` tiers beyond current baseline. | +| BL-008 | Local Vault phase-2 implementation | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Separate higher-assurance vault capability remains unimplemented. | +| BL-009 | Storage recovery UX flows | Proposed | Medium | UX + Reliability | Transient FR document (Storage, archived) | Frontend | Build user-facing reset/repair/recovery workflow for storage failures. | +| BL-010 | Renderer structured logging adoption | Proposed | Medium | Observability | TASK.md | Frontend | Apply shared structured logs in renderer with IPC correlation IDs for key user flows. | +| BL-011 | Failure UX pattern implementation | Planned | Medium | UX + Reliability | TASK.md | Frontend | Implement documented toast/dialog/inline/offline patterns consistently across features. | +| BL-012 | IPC real-handler contract harness expansion | Done | Medium | Testing + IPC Contracts | TASK.md | Platform | Delivered via real-handler unauthorized sender integration coverage and preload invoke timeout/correlation tests; superseded by scoped execution under `BL-023`. | +| BL-013 | OIDC auth platform (desktop PKCE + secure IPC) | Planned | High | Security + Identity | TASK.md | Platform | Phased backlog and acceptance tests tracked in `docs/05-governance/oidc-auth-backlog.md`. | +| BL-014 | Remove temporary JWT-authorizer client-id audience compatibility | Planned | High | Security + Identity | OIDC integration | Platform + Security | Clerk OAuth access token currently omits `aud`; AWS authorizer temporarily allows both `YOUR_API_AUDIENCE` and OAuth client id. Remove client-id audience once Clerk emits API audience/scopes as required. | +| BL-015 | Add IdP global sign-out and token revocation flow | Done | Medium | Security + Identity | OIDC integration | Platform + Security | Delivered with explicit `local` vs `global` sign-out mode, revocation/end-session capability reporting, and renderer-safe lifecycle messaging. | +| BL-016 | Refactor desktop-main composition root and IPC modularization | Done | High | Desktop Runtime + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; desktop-main composition root split and handler registration modularized. | +| BL-017 | Refactor preload bridge into domain modules with shared invoke client | Done | High | Preload + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; preload segmented into domain APIs with shared invoke/correlation/timeout client. | +| BL-018 | Introduce reusable validated IPC handler factory in desktop-main | Done | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; validated handler factory centralizes sender auth, schema validation, and envelope handling. | +| BL-019 | Decompose OIDC service into smaller capability-focused modules | Proposed | Medium | Security + Identity | Fresh workspace review (2026-02-13) | Platform + Security | Split sign-in flow, discovery/provider client, token lifecycle, and diagnostics concerns currently concentrated in `oidc-service.ts`. | +| BL-020 | Complete renderer i18n migration for hardcoded user-facing strings | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-13) | Frontend | Replace hardcoded labels/messages in renderer feature pages with translation keys and locale entries. | +| BL-021 | Consolidate renderer route/nav metadata into a single typed registry | Done | Medium | Frontend Architecture | Fresh workspace review (2026-02-13) | Frontend | Delivered by introducing a typed renderer route registry that generates both router entries and shell nav links from one source while preserving production file-replacement exclusions. | +| BL-022 | Rationalize thin shell/core/repository libraries | Proposed | Low | Architecture + Maintainability | Fresh workspace review (2026-02-13) | Platform | Either consolidate low-value wrappers or expand with meaningful domain behavior to reduce packaging overhead and clarify boundaries. | +| BL-023 | Expand IPC integration harness for preload-main real handler paths | Done | Medium | Testing + IPC Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered with real-handler unauthorized sender tests and preload invoke malformed/timeout/failure correlation assertions. | +| BL-024 | Standardize structured renderer logging with shared helper adoption | Proposed | Medium | Observability | Fresh workspace review (2026-02-13) | Frontend | Apply structured logging in renderer flows with correlation IDs and redaction-safe details. | +| BL-025 | Strengthen compile-time typing for API operation contracts end-to-end | Done | Medium | Platform API + Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered by introducing operation-to-request/response type maps and consuming them in preload/desktop API invoke surfaces. | +| BL-026 | Exclude lab routes/features from production bundle surface | Done | High | Frontend + Security Posture | Sprint implementation (2026-02-13) | Frontend + Platform | Production route/shell config replacement now removes lab routes/nav/toggle from production artifacts to reduce discoverability/attack surface. | +| BL-027 | Provide deterministic bundled update demo patch cycle | Done | Medium | Delivery + Update Architecture | Sprint implementation (2026-02-13) | Platform | Added local bundled feed demo (`1.0.0-demo` -> `1.0.1-demo`) with hash validation and renderer diagnostics to prove end-to-end update model independent of installer updater infra. | +| BL-028 | Enforce robust file signature validation for privileged file ingress | In Progress | High | Security + File Handling | Python sidecar architecture spike | Platform + Security | Python sidecar ingress path now enforces `.pdf` extension + `%PDF-` header checks with fail-closed behavior. Remaining scope: align renderer fs bridge parity, centralize supported signatures, and add security event logging/coverage for all privileged ingress paths. | +| BL-029 | Standardize official Python runtime distribution for sidecar bundling | Proposed | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Replace machine-local Python source dependency with an official pinned distribution artifact (version + checksum), build runtime bundle from that artifact in CI, and enforce reproducible packaged interpreter/dependency state across developer and CI environments. | +| BL-030 | Deterministic packaged Python sidecar runtime baseline | Done | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Delivered deterministic runtime manifest/assert/sync flow, pinned runtime dependency install (`requirements-runtime.txt`), packaged-build bundled-runtime enforcement (no system fallback), and runtime executable diagnostics proving packaged interpreter path. | +| BL-031 | Refactor desktop-main composition root into focused runtime modules | Proposed | High | Desktop Runtime + Architecture | Fresh workspace review (2026-02-14) | Platform | Split `main.ts` orchestration from lifecycle/app-window/auth-token/python-runtime concerns into focused modules with retained behavior. Acceptance: no behavior regressions in auth/session/update/python flows. Proof: `pnpm nx run desktop-main:test`, `pnpm nx run desktop-main:build`. | +| BL-032 | Standardize IPC handler failure envelope and correlation guarantees | Proposed | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-14) | Platform | Extend validated handler factory to normalize unexpected handler exceptions into typed error envelopes with correlation IDs and safe messages. Acceptance: renderer receives consistent failure shape for validation + runtime errors. Proof: `pnpm nx run desktop-main:test` plus integration assertions in preload/main harness. | +| BL-033 | Centralize privileged file ingress policy across all IPC file routes | Proposed | High | Security + File Handling | Fresh workspace review (2026-02-14) | Platform + Security | Move extension/signature/size policy to shared enforcement module consumed by all privileged file handlers (not just Python ingress). Acceptance: one policy source, fail-closed behavior, structured security logging on reject. Proof: `pnpm nx run desktop-main:test`, `pnpm docs-lint`. | +| BL-034 | Route-driven i18n asset loading manifest for renderer features | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-14) | Frontend | Replace manual translation path list with route/feature manifest-based loading to remove per-feature loader edits. Acceptance: adding a feature locale requires no loader code change. Proof: `pnpm i18n-check`, `pnpm nx run renderer:test`, `pnpm nx run renderer:build`. | +| BL-035 | Replace route/nav literal labels with typed i18n keys | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-14) | Frontend | Update route registry metadata to use translation keys instead of literals and enforce key typing for nav labels/titles. Acceptance: no hardcoded nav labels in route registry. Proof: `pnpm i18n-check`, `pnpm nx run renderer:build`. | +| BL-036 | Promote Python runtime sync to first-class Nx target dependency | Proposed | Medium | Build System + Determinism | Fresh workspace review (2026-02-14) | Platform | Model runtime sync/prepare as explicit Nx targets with `dependsOn` instead of script chaining for cache/task graph correctness. Acceptance: graph reflects runtime-prep dependency for package/build targets. Proof: `pnpm nx graph` (visual verification), `pnpm nx run desktop-main:build`, `pnpm forge:make:staging`. | +| BL-037 | Consolidate duplicated CI setup into reusable workflow primitives | Proposed | Medium | Delivery + CI Maintainability | Fresh workspace review (2026-02-14) | Platform | Extract repeated node/pnpm/install/setup patterns into reusable workflow/composite action while preserving job isolation. Acceptance: parity with current checks and no coverage loss. Proof: PR CI green on all existing jobs after refactor. | +| BL-038 | Evaluate sidecar transport hardening path (loopback HTTP vs stdio) | Proposed | Low | Security + Runtime Architecture | Fresh workspace review (2026-02-14) | Platform + Security | Produce ADR comparing current loopback model with stdio/pipe RPC model, migration cost, and risk reduction; no implementation required in first pass. Acceptance: decision record approved with explicit threat tradeoffs. Proof: decision recorded in `docs/05-governance/decision-log.md`. | ## Status Definitions From 99e84c5d91ebe00c2366a7e346635668e5f15c0c Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Sat, 14 Feb 2026 07:37:06 +0000 Subject: [PATCH 3/9] feat(ipc): normalize unhandled handler failures with correlation-safe envelope --- CURRENT-SPRINT.md | 2 + .../ipc/register-validated-handler.spec.ts | 162 ++++++++++++++++++ .../src/ipc/register-validated-handler.ts | 41 +++-- docs/05-governance/backlog.md | 80 ++++----- 4 files changed, 234 insertions(+), 51 deletions(-) create mode 100644 apps/desktop-main/src/ipc/register-validated-handler.spec.ts diff --git a/CURRENT-SPRINT.md b/CURRENT-SPRINT.md index cc6126e..614ea61 100644 --- a/CURRENT-SPRINT.md +++ b/CURRENT-SPRINT.md @@ -83,3 +83,5 @@ Increase security and runtime determinism in privileged execution paths before a ## Progress Log - 2026-02-14: Sprint reprioritized to security/runtime determinism; i18n work deferred. +- 2026-02-14: Implemented shared file token consumption and centralized ingress policy (`BL-033` / `BL-028` progress) with new handler tests and successful `desktop:dev:win` verification. +- 2026-02-14: Implemented validated handler exception normalization (`BL-032` progress): unexpected sync/async handler failures now return `IPC/HANDLER_FAILED` with correlation IDs and structured logging. diff --git a/apps/desktop-main/src/ipc/register-validated-handler.spec.ts b/apps/desktop-main/src/ipc/register-validated-handler.spec.ts new file mode 100644 index 0000000..a16183c --- /dev/null +++ b/apps/desktop-main/src/ipc/register-validated-handler.spec.ts @@ -0,0 +1,162 @@ +import type { IpcMain, IpcMainInvokeEvent } from 'electron'; +import { z } from 'zod'; +import { + asFailure, + CONTRACT_VERSION, + type DesktopResult, +} from '@electron-foundation/contracts'; +import type { MainIpcContext } from './handler-context'; +import { registerValidatedHandler } from './register-validated-handler'; + +describe('registerValidatedHandler', () => { + const createInvokePayload = (correlationId: string) => ({ + contractVersion: CONTRACT_VERSION, + correlationId, + payload: {}, + }); + + const createContext = (): MainIpcContext => ({ + appVersion: '0.0.0-test', + appEnvironment: 'development', + fileTokenTtlMs: 5 * 60_000, + selectedFileTokens: new Map(), + getCorrelationId: (payload: unknown) => + payload && + typeof payload === 'object' && + 'correlationId' in payload && + typeof (payload as { correlationId?: unknown }).correlationId === 'string' + ? (payload as { correlationId: string }).correlationId + : undefined, + assertAuthorizedSender: () => null as DesktopResult | null, + getOidcService: vi.fn(() => null), + getStorageGateway: vi.fn(), + invokeApiOperation: vi.fn(), + getApiOperationDiagnostics: vi.fn(), + getDemoUpdater: vi.fn(() => null), + getPythonSidecar: vi.fn(() => null), + logEvent: vi.fn(), + }); + + const register = ( + context: MainIpcContext, + handler: (event: IpcMainInvokeEvent, request: unknown) => unknown, + ) => { + const handlers = new Map< + string, + (event: IpcMainInvokeEvent, payload: unknown) => Promise + >(); + const ipcMain = { + handle: ( + channel: string, + registeredHandler: (...args: unknown[]) => unknown, + ) => { + handlers.set( + channel, + registeredHandler as ( + event: IpcMainInvokeEvent, + payload: unknown, + ) => Promise, + ); + }, + } as unknown as IpcMain; + + registerValidatedHandler({ + ipcMain, + channel: 'test:channel', + schema: z.object({ + contractVersion: z.string(), + correlationId: z.string(), + payload: z.object({}).strict(), + }), + context, + handler: handler as ( + event: IpcMainInvokeEvent, + request: { + contractVersion: string; + correlationId: string; + payload: Record; + }, + ) => unknown, + }); + + return handlers.get('test:channel'); + }; + + it('normalizes sync handler exceptions into IPC/HANDLER_FAILED', async () => { + const context = createContext(); + const invoke = register(context, () => { + throw new Error('sync boom'); + }); + expect(invoke).toBeDefined(); + + const result = await invoke!( + {} as IpcMainInvokeEvent, + createInvokePayload('corr-sync-throw'), + ); + + expect(result).toMatchObject({ + ok: false, + error: { + code: 'IPC/HANDLER_FAILED', + correlationId: 'corr-sync-throw', + }, + }); + expect(context.logEvent).toHaveBeenCalledWith( + 'error', + 'ipc.handler_unhandled_exception', + 'corr-sync-throw', + expect.objectContaining({ + channel: 'test:channel', + }), + ); + }); + + it('normalizes async handler rejections into IPC/HANDLER_FAILED', async () => { + const context = createContext(); + const invoke = register(context, async () => { + throw new Error('async boom'); + }); + expect(invoke).toBeDefined(); + + const result = await invoke!( + {} as IpcMainInvokeEvent, + createInvokePayload('corr-async-throw'), + ); + + expect(result).toMatchObject({ + ok: false, + error: { + code: 'IPC/HANDLER_FAILED', + correlationId: 'corr-async-throw', + }, + }); + }); + + it('preserves unauthorized sender short-circuit behavior', async () => { + const context = createContext(); + context.assertAuthorizedSender = (_event, correlationId) => + asFailure( + 'IPC/UNAUTHORIZED_SENDER', + 'IPC sender is not authorized for this operation.', + { reason: 'test' }, + false, + correlationId, + ) as DesktopResult; + + const invoke = register(context, vi.fn()); + expect(invoke).toBeDefined(); + + const result = await invoke!( + {} as IpcMainInvokeEvent, + createInvokePayload('corr-unauthorized'), + ); + + expect(result).toMatchObject({ + ok: false, + error: { + code: 'IPC/UNAUTHORIZED_SENDER', + correlationId: 'corr-unauthorized', + }, + }); + }); +}); diff --git a/apps/desktop-main/src/ipc/register-validated-handler.ts b/apps/desktop-main/src/ipc/register-validated-handler.ts index 2eec0be..c5cfbff 100644 --- a/apps/desktop-main/src/ipc/register-validated-handler.ts +++ b/apps/desktop-main/src/ipc/register-validated-handler.ts @@ -23,22 +23,41 @@ export const registerValidatedHandler = ({ }: RegisterValidatedHandlerArgs) => { ipcMain.handle(channel, async (event, payload) => { const correlationId = context.getCorrelationId(payload); - const unauthorized = context.assertAuthorizedSender(event, correlationId); - if (unauthorized) { - return unauthorized; - } + try { + const unauthorized = context.assertAuthorizedSender(event, correlationId); + if (unauthorized) { + return unauthorized; + } + + const parsed = schema.safeParse(payload); + if (!parsed.success) { + return asFailure( + 'IPC/VALIDATION_FAILED', + 'IPC payload failed validation.', + parsed.error.flatten(), + false, + correlationId, + ); + } - const parsed = schema.safeParse(payload); - if (!parsed.success) { + return await handler(event, parsed.data); + } catch (error) { + context.logEvent( + 'error', + 'ipc.handler_unhandled_exception', + correlationId, + { + channel, + message: error instanceof Error ? error.message : String(error), + }, + ); return asFailure( - 'IPC/VALIDATION_FAILED', - 'IPC payload failed validation.', - parsed.error.flatten(), + 'IPC/HANDLER_FAILED', + 'IPC handler execution failed.', + { channel }, false, correlationId, ); } - - return handler(event, parsed.data); }); }; diff --git a/docs/05-governance/backlog.md b/docs/05-governance/backlog.md index 5eda4cf..f8d8543 100644 --- a/docs/05-governance/backlog.md +++ b/docs/05-governance/backlog.md @@ -4,46 +4,46 @@ Owner: Platform Engineering Review cadence: Weekly Last reviewed: 2026-02-13 -| ID | Title | Status | Priority | Area | Source | Owner | Notes | -| ------ | --------------------------------------------------------------------- | ----------- | -------- | ------------------------------- | ----------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| BL-001 | Linux packaging strategy for desktop | Deferred | Low | Delivery + Packaging | FEEDBACK.md | Platform | Add Electron Forge Linux makers and release notes once Linux is in scope. | -| BL-002 | Enforce handshake contract-version mismatch path | Planned | Medium | IPC Contracts | FEEDBACK.md | Platform | Main process currently validates schema but does not return a dedicated mismatch error. | -| BL-003 | API operation compile-time typing hardening | Done | Medium | Platform API | Transient FR document (API, archived) | Platform | Implemented via operation type maps and typed invoke signatures across contracts/preload/desktop API (baseline delivered and extended by `BL-025`). | -| BL-004 | Enterprise proxy/TLS support matrix documentation | Deferred | Low | Security + Networking | Transient FR document (API, archived) | Platform | Document expected behaviors for proxy auth, TLS interception, and certificate errors. | -| BL-005 | Offline API queue/replay capability | Proposed | Medium | Platform API | Transient FR document (API, archived) | Platform | Current behavior is fail-fast + retry classification; queue/replay not implemented. | -| BL-006 | Storage capability-scoped authorization model | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Add explicit capability/role rules per storage operation. | -| BL-007 | Expanded data-classification tiers | Deferred | Low | Storage + Governance | Transient FR document (Storage, archived) | Platform | Add `public`, `secret`, and `high-value secret` tiers beyond current baseline. | -| BL-008 | Local Vault phase-2 implementation | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Separate higher-assurance vault capability remains unimplemented. | -| BL-009 | Storage recovery UX flows | Proposed | Medium | UX + Reliability | Transient FR document (Storage, archived) | Frontend | Build user-facing reset/repair/recovery workflow for storage failures. | -| BL-010 | Renderer structured logging adoption | Proposed | Medium | Observability | TASK.md | Frontend | Apply shared structured logs in renderer with IPC correlation IDs for key user flows. | -| BL-011 | Failure UX pattern implementation | Planned | Medium | UX + Reliability | TASK.md | Frontend | Implement documented toast/dialog/inline/offline patterns consistently across features. | -| BL-012 | IPC real-handler contract harness expansion | Done | Medium | Testing + IPC Contracts | TASK.md | Platform | Delivered via real-handler unauthorized sender integration coverage and preload invoke timeout/correlation tests; superseded by scoped execution under `BL-023`. | -| BL-013 | OIDC auth platform (desktop PKCE + secure IPC) | Planned | High | Security + Identity | TASK.md | Platform | Phased backlog and acceptance tests tracked in `docs/05-governance/oidc-auth-backlog.md`. | -| BL-014 | Remove temporary JWT-authorizer client-id audience compatibility | Planned | High | Security + Identity | OIDC integration | Platform + Security | Clerk OAuth access token currently omits `aud`; AWS authorizer temporarily allows both `YOUR_API_AUDIENCE` and OAuth client id. Remove client-id audience once Clerk emits API audience/scopes as required. | -| BL-015 | Add IdP global sign-out and token revocation flow | Done | Medium | Security + Identity | OIDC integration | Platform + Security | Delivered with explicit `local` vs `global` sign-out mode, revocation/end-session capability reporting, and renderer-safe lifecycle messaging. | -| BL-016 | Refactor desktop-main composition root and IPC modularization | Done | High | Desktop Runtime + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; desktop-main composition root split and handler registration modularized. | -| BL-017 | Refactor preload bridge into domain modules with shared invoke client | Done | High | Preload + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; preload segmented into domain APIs with shared invoke/correlation/timeout client. | -| BL-018 | Introduce reusable validated IPC handler factory in desktop-main | Done | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; validated handler factory centralizes sender auth, schema validation, and envelope handling. | -| BL-019 | Decompose OIDC service into smaller capability-focused modules | Proposed | Medium | Security + Identity | Fresh workspace review (2026-02-13) | Platform + Security | Split sign-in flow, discovery/provider client, token lifecycle, and diagnostics concerns currently concentrated in `oidc-service.ts`. | -| BL-020 | Complete renderer i18n migration for hardcoded user-facing strings | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-13) | Frontend | Replace hardcoded labels/messages in renderer feature pages with translation keys and locale entries. | -| BL-021 | Consolidate renderer route/nav metadata into a single typed registry | Done | Medium | Frontend Architecture | Fresh workspace review (2026-02-13) | Frontend | Delivered by introducing a typed renderer route registry that generates both router entries and shell nav links from one source while preserving production file-replacement exclusions. | -| BL-022 | Rationalize thin shell/core/repository libraries | Proposed | Low | Architecture + Maintainability | Fresh workspace review (2026-02-13) | Platform | Either consolidate low-value wrappers or expand with meaningful domain behavior to reduce packaging overhead and clarify boundaries. | -| BL-023 | Expand IPC integration harness for preload-main real handler paths | Done | Medium | Testing + IPC Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered with real-handler unauthorized sender tests and preload invoke malformed/timeout/failure correlation assertions. | -| BL-024 | Standardize structured renderer logging with shared helper adoption | Proposed | Medium | Observability | Fresh workspace review (2026-02-13) | Frontend | Apply structured logging in renderer flows with correlation IDs and redaction-safe details. | -| BL-025 | Strengthen compile-time typing for API operation contracts end-to-end | Done | Medium | Platform API + Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered by introducing operation-to-request/response type maps and consuming them in preload/desktop API invoke surfaces. | -| BL-026 | Exclude lab routes/features from production bundle surface | Done | High | Frontend + Security Posture | Sprint implementation (2026-02-13) | Frontend + Platform | Production route/shell config replacement now removes lab routes/nav/toggle from production artifacts to reduce discoverability/attack surface. | -| BL-027 | Provide deterministic bundled update demo patch cycle | Done | Medium | Delivery + Update Architecture | Sprint implementation (2026-02-13) | Platform | Added local bundled feed demo (`1.0.0-demo` -> `1.0.1-demo`) with hash validation and renderer diagnostics to prove end-to-end update model independent of installer updater infra. | -| BL-028 | Enforce robust file signature validation for privileged file ingress | In Progress | High | Security + File Handling | Python sidecar architecture spike | Platform + Security | Python sidecar ingress path now enforces `.pdf` extension + `%PDF-` header checks with fail-closed behavior. Remaining scope: align renderer fs bridge parity, centralize supported signatures, and add security event logging/coverage for all privileged ingress paths. | -| BL-029 | Standardize official Python runtime distribution for sidecar bundling | Proposed | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Replace machine-local Python source dependency with an official pinned distribution artifact (version + checksum), build runtime bundle from that artifact in CI, and enforce reproducible packaged interpreter/dependency state across developer and CI environments. | -| BL-030 | Deterministic packaged Python sidecar runtime baseline | Done | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Delivered deterministic runtime manifest/assert/sync flow, pinned runtime dependency install (`requirements-runtime.txt`), packaged-build bundled-runtime enforcement (no system fallback), and runtime executable diagnostics proving packaged interpreter path. | -| BL-031 | Refactor desktop-main composition root into focused runtime modules | Proposed | High | Desktop Runtime + Architecture | Fresh workspace review (2026-02-14) | Platform | Split `main.ts` orchestration from lifecycle/app-window/auth-token/python-runtime concerns into focused modules with retained behavior. Acceptance: no behavior regressions in auth/session/update/python flows. Proof: `pnpm nx run desktop-main:test`, `pnpm nx run desktop-main:build`. | -| BL-032 | Standardize IPC handler failure envelope and correlation guarantees | Proposed | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-14) | Platform | Extend validated handler factory to normalize unexpected handler exceptions into typed error envelopes with correlation IDs and safe messages. Acceptance: renderer receives consistent failure shape for validation + runtime errors. Proof: `pnpm nx run desktop-main:test` plus integration assertions in preload/main harness. | -| BL-033 | Centralize privileged file ingress policy across all IPC file routes | Proposed | High | Security + File Handling | Fresh workspace review (2026-02-14) | Platform + Security | Move extension/signature/size policy to shared enforcement module consumed by all privileged file handlers (not just Python ingress). Acceptance: one policy source, fail-closed behavior, structured security logging on reject. Proof: `pnpm nx run desktop-main:test`, `pnpm docs-lint`. | -| BL-034 | Route-driven i18n asset loading manifest for renderer features | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-14) | Frontend | Replace manual translation path list with route/feature manifest-based loading to remove per-feature loader edits. Acceptance: adding a feature locale requires no loader code change. Proof: `pnpm i18n-check`, `pnpm nx run renderer:test`, `pnpm nx run renderer:build`. | -| BL-035 | Replace route/nav literal labels with typed i18n keys | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-14) | Frontend | Update route registry metadata to use translation keys instead of literals and enforce key typing for nav labels/titles. Acceptance: no hardcoded nav labels in route registry. Proof: `pnpm i18n-check`, `pnpm nx run renderer:build`. | -| BL-036 | Promote Python runtime sync to first-class Nx target dependency | Proposed | Medium | Build System + Determinism | Fresh workspace review (2026-02-14) | Platform | Model runtime sync/prepare as explicit Nx targets with `dependsOn` instead of script chaining for cache/task graph correctness. Acceptance: graph reflects runtime-prep dependency for package/build targets. Proof: `pnpm nx graph` (visual verification), `pnpm nx run desktop-main:build`, `pnpm forge:make:staging`. | -| BL-037 | Consolidate duplicated CI setup into reusable workflow primitives | Proposed | Medium | Delivery + CI Maintainability | Fresh workspace review (2026-02-14) | Platform | Extract repeated node/pnpm/install/setup patterns into reusable workflow/composite action while preserving job isolation. Acceptance: parity with current checks and no coverage loss. Proof: PR CI green on all existing jobs after refactor. | -| BL-038 | Evaluate sidecar transport hardening path (loopback HTTP vs stdio) | Proposed | Low | Security + Runtime Architecture | Fresh workspace review (2026-02-14) | Platform + Security | Produce ADR comparing current loopback model with stdio/pipe RPC model, migration cost, and risk reduction; no implementation required in first pass. Acceptance: decision record approved with explicit threat tradeoffs. Proof: decision recorded in `docs/05-governance/decision-log.md`. | +| ID | Title | Status | Priority | Area | Source | Owner | Notes | +| ------ | --------------------------------------------------------------------- | ----------- | -------- | ------------------------------- | ----------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| BL-001 | Linux packaging strategy for desktop | Deferred | Low | Delivery + Packaging | FEEDBACK.md | Platform | Add Electron Forge Linux makers and release notes once Linux is in scope. | +| BL-002 | Enforce handshake contract-version mismatch path | Planned | Medium | IPC Contracts | FEEDBACK.md | Platform | Main process currently validates schema but does not return a dedicated mismatch error. | +| BL-003 | API operation compile-time typing hardening | Done | Medium | Platform API | Transient FR document (API, archived) | Platform | Implemented via operation type maps and typed invoke signatures across contracts/preload/desktop API (baseline delivered and extended by `BL-025`). | +| BL-004 | Enterprise proxy/TLS support matrix documentation | Deferred | Low | Security + Networking | Transient FR document (API, archived) | Platform | Document expected behaviors for proxy auth, TLS interception, and certificate errors. | +| BL-005 | Offline API queue/replay capability | Proposed | Medium | Platform API | Transient FR document (API, archived) | Platform | Current behavior is fail-fast + retry classification; queue/replay not implemented. | +| BL-006 | Storage capability-scoped authorization model | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Add explicit capability/role rules per storage operation. | +| BL-007 | Expanded data-classification tiers | Deferred | Low | Storage + Governance | Transient FR document (Storage, archived) | Platform | Add `public`, `secret`, and `high-value secret` tiers beyond current baseline. | +| BL-008 | Local Vault phase-2 implementation | Proposed | Medium | Storage + Security | Transient FR document (Storage, archived) | Platform | Separate higher-assurance vault capability remains unimplemented. | +| BL-009 | Storage recovery UX flows | Proposed | Medium | UX + Reliability | Transient FR document (Storage, archived) | Frontend | Build user-facing reset/repair/recovery workflow for storage failures. | +| BL-010 | Renderer structured logging adoption | Proposed | Medium | Observability | TASK.md | Frontend | Apply shared structured logs in renderer with IPC correlation IDs for key user flows. | +| BL-011 | Failure UX pattern implementation | Planned | Medium | UX + Reliability | TASK.md | Frontend | Implement documented toast/dialog/inline/offline patterns consistently across features. | +| BL-012 | IPC real-handler contract harness expansion | Done | Medium | Testing + IPC Contracts | TASK.md | Platform | Delivered via real-handler unauthorized sender integration coverage and preload invoke timeout/correlation tests; superseded by scoped execution under `BL-023`. | +| BL-013 | OIDC auth platform (desktop PKCE + secure IPC) | Planned | High | Security + Identity | TASK.md | Platform | Phased backlog and acceptance tests tracked in `docs/05-governance/oidc-auth-backlog.md`. | +| BL-014 | Remove temporary JWT-authorizer client-id audience compatibility | Planned | High | Security + Identity | OIDC integration | Platform + Security | Clerk OAuth access token currently omits `aud`; AWS authorizer temporarily allows both `YOUR_API_AUDIENCE` and OAuth client id. Remove client-id audience once Clerk emits API audience/scopes as required. | +| BL-015 | Add IdP global sign-out and token revocation flow | Done | Medium | Security + Identity | OIDC integration | Platform + Security | Delivered with explicit `local` vs `global` sign-out mode, revocation/end-session capability reporting, and renderer-safe lifecycle messaging. | +| BL-016 | Refactor desktop-main composition root and IPC modularization | Done | High | Desktop Runtime + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; desktop-main composition root split and handler registration modularized. | +| BL-017 | Refactor preload bridge into domain modules with shared invoke client | Done | High | Preload + IPC | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; preload segmented into domain APIs with shared invoke/correlation/timeout client. | +| BL-018 | Introduce reusable validated IPC handler factory in desktop-main | Done | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-13) | Platform | Completed in Sprint 1; validated handler factory centralizes sender auth, schema validation, and envelope handling. | +| BL-019 | Decompose OIDC service into smaller capability-focused modules | Proposed | Medium | Security + Identity | Fresh workspace review (2026-02-13) | Platform + Security | Split sign-in flow, discovery/provider client, token lifecycle, and diagnostics concerns currently concentrated in `oidc-service.ts`. | +| BL-020 | Complete renderer i18n migration for hardcoded user-facing strings | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-13) | Frontend | Replace hardcoded labels/messages in renderer feature pages with translation keys and locale entries. | +| BL-021 | Consolidate renderer route/nav metadata into a single typed registry | Done | Medium | Frontend Architecture | Fresh workspace review (2026-02-13) | Frontend | Delivered by introducing a typed renderer route registry that generates both router entries and shell nav links from one source while preserving production file-replacement exclusions. | +| BL-022 | Rationalize thin shell/core/repository libraries | Proposed | Low | Architecture + Maintainability | Fresh workspace review (2026-02-13) | Platform | Either consolidate low-value wrappers or expand with meaningful domain behavior to reduce packaging overhead and clarify boundaries. | +| BL-023 | Expand IPC integration harness for preload-main real handler paths | Done | Medium | Testing + IPC Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered with real-handler unauthorized sender tests and preload invoke malformed/timeout/failure correlation assertions. | +| BL-024 | Standardize structured renderer logging with shared helper adoption | Proposed | Medium | Observability | Fresh workspace review (2026-02-13) | Frontend | Apply structured logging in renderer flows with correlation IDs and redaction-safe details. | +| BL-025 | Strengthen compile-time typing for API operation contracts end-to-end | Done | Medium | Platform API + Contracts | Fresh workspace review (2026-02-13) | Platform | Delivered by introducing operation-to-request/response type maps and consuming them in preload/desktop API invoke surfaces. | +| BL-026 | Exclude lab routes/features from production bundle surface | Done | High | Frontend + Security Posture | Sprint implementation (2026-02-13) | Frontend + Platform | Production route/shell config replacement now removes lab routes/nav/toggle from production artifacts to reduce discoverability/attack surface. | +| BL-027 | Provide deterministic bundled update demo patch cycle | Done | Medium | Delivery + Update Architecture | Sprint implementation (2026-02-13) | Platform | Added local bundled feed demo (`1.0.0-demo` -> `1.0.1-demo`) with hash validation and renderer diagnostics to prove end-to-end update model independent of installer updater infra. | +| BL-028 | Enforce robust file signature validation for privileged file ingress | In Progress | High | Security + File Handling | Python sidecar architecture spike | Platform + Security | Python sidecar ingress path now enforces `.pdf` extension + `%PDF-` header checks with fail-closed behavior. Remaining scope: align renderer fs bridge parity, centralize supported signatures, and add security event logging/coverage for all privileged ingress paths. | +| BL-029 | Standardize official Python runtime distribution for sidecar bundling | Proposed | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Replace machine-local Python source dependency with an official pinned distribution artifact (version + checksum), build runtime bundle from that artifact in CI, and enforce reproducible packaged interpreter/dependency state across developer and CI environments. | +| BL-030 | Deterministic packaged Python sidecar runtime baseline | Done | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Delivered deterministic runtime manifest/assert/sync flow, pinned runtime dependency install (`requirements-runtime.txt`), packaged-build bundled-runtime enforcement (no system fallback), and runtime executable diagnostics proving packaged interpreter path. | +| BL-031 | Refactor desktop-main composition root into focused runtime modules | Proposed | High | Desktop Runtime + Architecture | Fresh workspace review (2026-02-14) | Platform | Split `main.ts` orchestration from lifecycle/app-window/auth-token/python-runtime concerns into focused modules with retained behavior. Acceptance: no behavior regressions in auth/session/update/python flows. Proof: `pnpm nx run desktop-main:test`, `pnpm nx run desktop-main:build`. | +| BL-032 | Standardize IPC handler failure envelope and correlation guarantees | In Progress | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-14) | Platform | Validated handler factory now catches unexpected sync/async handler failures and returns `IPC/HANDLER_FAILED` with correlation ID plus structured exception logging; remaining scope is preload-main real-handler integration assertions. Proof: `pnpm nx run desktop-main:test` plus integration assertions in preload/main harness. | +| BL-033 | Centralize privileged file ingress policy across all IPC file routes | In Progress | High | Security + File Handling | Fresh workspace review (2026-02-14) | Platform + Security | Shared token consumption and ingress policy module now enforce extension/signature checks across file and python handlers; remaining scope is parity audit for any additional privileged ingress surfaces and reject-event telemetry consistency. Proof: `pnpm nx run desktop-main:test`, `pnpm docs-lint`. | +| BL-034 | Route-driven i18n asset loading manifest for renderer features | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-14) | Frontend | Replace manual translation path list with route/feature manifest-based loading to remove per-feature loader edits. Acceptance: adding a feature locale requires no loader code change. Proof: `pnpm i18n-check`, `pnpm nx run renderer:test`, `pnpm nx run renderer:build`. | +| BL-035 | Replace route/nav literal labels with typed i18n keys | Proposed | Medium | Frontend + I18n | Fresh workspace review (2026-02-14) | Frontend | Update route registry metadata to use translation keys instead of literals and enforce key typing for nav labels/titles. Acceptance: no hardcoded nav labels in route registry. Proof: `pnpm i18n-check`, `pnpm nx run renderer:build`. | +| BL-036 | Promote Python runtime sync to first-class Nx target dependency | Proposed | Medium | Build System + Determinism | Fresh workspace review (2026-02-14) | Platform | Model runtime sync/prepare as explicit Nx targets with `dependsOn` instead of script chaining for cache/task graph correctness. Acceptance: graph reflects runtime-prep dependency for package/build targets. Proof: `pnpm nx graph` (visual verification), `pnpm nx run desktop-main:build`, `pnpm forge:make:staging`. | +| BL-037 | Consolidate duplicated CI setup into reusable workflow primitives | Proposed | Medium | Delivery + CI Maintainability | Fresh workspace review (2026-02-14) | Platform | Extract repeated node/pnpm/install/setup patterns into reusable workflow/composite action while preserving job isolation. Acceptance: parity with current checks and no coverage loss. Proof: PR CI green on all existing jobs after refactor. | +| BL-038 | Evaluate sidecar transport hardening path (loopback HTTP vs stdio) | Proposed | Low | Security + Runtime Architecture | Fresh workspace review (2026-02-14) | Platform + Security | Produce ADR comparing current loopback model with stdio/pipe RPC model, migration cost, and risk reduction; no implementation required in first pass. Acceptance: decision record approved with explicit threat tradeoffs. Proof: decision recorded in `docs/05-governance/decision-log.md`. | ## Status Definitions From e76c82eee09d330fa8df6eda0b431f6a99d59ed3 Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Sat, 14 Feb 2026 09:19:18 +0000 Subject: [PATCH 4/9] feat(runtime): pin official python artifact source and harden bundle assertions --- CURRENT-SPRINT.md | 1 + README.md | 14 +- apps/desktop-preload/src/api/dialog-api.ts | 3 + build/python-runtime/README.md | 25 ++- docs/05-governance/backlog.md | 2 +- tools/python-runtime-artifacts.json | 14 ++ .../scripts/assert-python-runtime-bundle.mjs | 37 ++++ .../scripts/prepare-local-python-runtime.mjs | 199 +++++++++++++++++- 8 files changed, 274 insertions(+), 21 deletions(-) create mode 100644 tools/python-runtime-artifacts.json diff --git a/CURRENT-SPRINT.md b/CURRENT-SPRINT.md index 614ea61..029b522 100644 --- a/CURRENT-SPRINT.md +++ b/CURRENT-SPRINT.md @@ -85,3 +85,4 @@ Increase security and runtime determinism in privileged execution paths before a - 2026-02-14: Sprint reprioritized to security/runtime determinism; i18n work deferred. - 2026-02-14: Implemented shared file token consumption and centralized ingress policy (`BL-033` / `BL-028` progress) with new handler tests and successful `desktop:dev:win` verification. - 2026-02-14: Implemented validated handler exception normalization (`BL-032` progress): unexpected sync/async handler failures now return `IPC/HANDLER_FAILED` with correlation IDs and structured logging. +- 2026-02-14: Started `BL-029A` implementation: added pinned official Python artifact catalog + SHA256 verification flow in runtime prep/assert scripts, regenerated runtime bundle from official source, and validated `python-runtime:assert` + `build-desktop-main`. diff --git a/README.md b/README.md index d30f211..6f6f7c2 100644 --- a/README.md +++ b/README.md @@ -212,17 +212,21 @@ How to run/verify: Deterministic packaged runtime (staging/production): -- Provide a local bundled runtime payload at: +- Runtime source is pinned via official artifact catalog: + - `tools/python-runtime-artifacts.json` +- Prepare runtime payload from pinned artifact: + - `pnpm run python-runtime:prepare-local` +- Prepared payload is written to: - `build/python-runtime/-/` - example: `build/python-runtime/win32-x64/` - Pin sidecar dependencies in: - `apps/desktop-main/python-sidecar/requirements-runtime.txt` -- Fast local bootstrap from your current Python install: - - `pnpm run python-runtime:prepare-local` -- Add `manifest.json` with `executableRelativePath` (see `build/python-runtime/README.md`). - Run validation: - `pnpm run python-runtime:assert` - - assertion verifies interpreter exists and imports `fitz` when PyMuPDF is declared in manifest + - assertion verifies interpreter exists, source policy is `official-artifact` by default, and imports `fitz` when PyMuPDF is declared in manifest +- Optional local emergency override: + - `PYTHON_RUNTIME_SOURCE_DIR=...` to provide a local source directory + - `PYTHON_RUNTIME_ALLOW_UNOFFICIAL_SOURCE=1` if assertion must permit non-official source manifests - Runtime payload is copied into desktop build artifacts by: - `pnpm run build-desktop-main` - `pnpm run forge:make:staging` diff --git a/apps/desktop-preload/src/api/dialog-api.ts b/apps/desktop-preload/src/api/dialog-api.ts index dd2bd09..b918be5 100644 --- a/apps/desktop-preload/src/api/dialog-api.ts +++ b/apps/desktop-preload/src/api/dialog-api.ts @@ -7,6 +7,8 @@ import { } from '@electron-foundation/contracts'; import { createCorrelationId, invokeIpc } from '../invoke-client'; +const dialogOpenTimeoutMs = 120_000; + export const createDialogApi = (): DesktopDialogApi => ({ async openFile(request = {}) { const correlationId = createCorrelationId(); @@ -21,6 +23,7 @@ export const createDialogApi = (): DesktopDialogApi => ({ payload, correlationId, openFileDialogResponseSchema, + dialogOpenTimeoutMs, ); }, }); diff --git a/build/python-runtime/README.md b/build/python-runtime/README.md index 2c9e507..669afe8 100644 --- a/build/python-runtime/README.md +++ b/build/python-runtime/README.md @@ -1,12 +1,16 @@ -# Local Python Runtime Bundle +# Python Runtime Bundle -Place machine-local bundled Python runtime files under: +Default model: -- `build/python-runtime/-/` +- `pnpm run python-runtime:prepare-local` now prepares runtime from a pinned official artifact catalog: + - `tools/python-runtime-artifacts.json` +- The script downloads the configured artifact, verifies SHA256, extracts it, applies runtime shaping, installs pinned runtime requirements, and writes: + - `build/python-runtime/-/manifest.json` +- `pnpm run python-runtime:assert` enforces bundle validity and (by default) requires `source.kind = official-artifact`. -Example for Windows x64: +Runtime payload location: -- `build/python-runtime/win32-x64/` +- `build/python-runtime/-/` Required file: @@ -28,16 +32,16 @@ Rules: - `executableRelativePath` is relative to `build/python-runtime/-/`. - The referenced executable must exist. -- Staging/production packaging runs `pnpm run python-runtime:assert` and fails if bundle files are missing/invalid. +- Staging/production packaging runs `pnpm run python-runtime:assert` and fails if bundle files are missing/invalid or source policy is not satisfied. Notes: - Runtime binaries are intentionally not tracked in git. - `desktop-main` build copies runtime payload into packaged artifacts under `python-runtime/-/`. -Convenience commands: +Commands: -- prepare bundle from local Python install: +- prepare bundle from pinned official artifact (default): - `pnpm run python-runtime:prepare-local` - validate bundle: - `pnpm run python-runtime:assert` @@ -45,10 +49,11 @@ Convenience commands: Optional environment overrides: - `PYTHON` -> explicit Python command to inspect. -- `PYTHON_RUNTIME_SOURCE_DIR` -> explicit folder to copy as runtime payload. +- `PYTHON_RUNTIME_SOURCE_DIR` -> explicit folder to copy as runtime payload (local override, non-default). - `PYTHON_RUNTIME_TARGET` -> override target folder (default `-`). - `PYTHON_RUNTIME_REQUIREMENTS` -> override path to requirements file used for deterministic package install. - `PYTHON_RUNTIME_PACKAGES` -> fallback comma-separated package names for manifest recording when no requirements file exists. +- `PYTHON_RUNTIME_ALLOW_UNOFFICIAL_SOURCE=1` -> allow assert pass for non-official source manifests (local emergency only). -The local prepare script also prunes non-runtime payload (docs/tests/demo assets) to keep package size down. +The prepare script prunes non-runtime payload (docs/tests/demo assets) to keep package size down. It also clears runtime `Lib/site-packages` and reinstalls pinned runtime dependencies from the requirements file for deterministic package contents. diff --git a/docs/05-governance/backlog.md b/docs/05-governance/backlog.md index f8d8543..b7e9a84 100644 --- a/docs/05-governance/backlog.md +++ b/docs/05-governance/backlog.md @@ -34,7 +34,7 @@ Last reviewed: 2026-02-13 | BL-026 | Exclude lab routes/features from production bundle surface | Done | High | Frontend + Security Posture | Sprint implementation (2026-02-13) | Frontend + Platform | Production route/shell config replacement now removes lab routes/nav/toggle from production artifacts to reduce discoverability/attack surface. | | BL-027 | Provide deterministic bundled update demo patch cycle | Done | Medium | Delivery + Update Architecture | Sprint implementation (2026-02-13) | Platform | Added local bundled feed demo (`1.0.0-demo` -> `1.0.1-demo`) with hash validation and renderer diagnostics to prove end-to-end update model independent of installer updater infra. | | BL-028 | Enforce robust file signature validation for privileged file ingress | In Progress | High | Security + File Handling | Python sidecar architecture spike | Platform + Security | Python sidecar ingress path now enforces `.pdf` extension + `%PDF-` header checks with fail-closed behavior. Remaining scope: align renderer fs bridge parity, centralize supported signatures, and add security event logging/coverage for all privileged ingress paths. | -| BL-029 | Standardize official Python runtime distribution for sidecar bundling | Proposed | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Replace machine-local Python source dependency with an official pinned distribution artifact (version + checksum), build runtime bundle from that artifact in CI, and enforce reproducible packaged interpreter/dependency state across developer and CI environments. | +| BL-029 | Standardize official Python runtime distribution for sidecar bundling | In Progress | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | `BL-029A` in progress: runtime prep now sources from pinned official artifact catalog (`tools/python-runtime-artifacts.json`) with SHA256 verification and manifest source metadata; remaining scope is CI artifact sourcing hardening and full reproducibility validation (`BL-029B`). | | BL-030 | Deterministic packaged Python sidecar runtime baseline | Done | High | Delivery + Runtime Determinism | Python sidecar packaging hardening | Platform + Security | Delivered deterministic runtime manifest/assert/sync flow, pinned runtime dependency install (`requirements-runtime.txt`), packaged-build bundled-runtime enforcement (no system fallback), and runtime executable diagnostics proving packaged interpreter path. | | BL-031 | Refactor desktop-main composition root into focused runtime modules | Proposed | High | Desktop Runtime + Architecture | Fresh workspace review (2026-02-14) | Platform | Split `main.ts` orchestration from lifecycle/app-window/auth-token/python-runtime concerns into focused modules with retained behavior. Acceptance: no behavior regressions in auth/session/update/python flows. Proof: `pnpm nx run desktop-main:test`, `pnpm nx run desktop-main:build`. | | BL-032 | Standardize IPC handler failure envelope and correlation guarantees | In Progress | High | IPC Contracts + Reliability | Fresh workspace review (2026-02-14) | Platform | Validated handler factory now catches unexpected sync/async handler failures and returns `IPC/HANDLER_FAILED` with correlation ID plus structured exception logging; remaining scope is preload-main real-handler integration assertions. Proof: `pnpm nx run desktop-main:test` plus integration assertions in preload/main harness. | diff --git a/tools/python-runtime-artifacts.json b/tools/python-runtime-artifacts.json new file mode 100644 index 0000000..a519f7d --- /dev/null +++ b/tools/python-runtime-artifacts.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "targets": { + "win32-x64": { + "distribution": "python.org-embed", + "pythonVersion": "3.13.12", + "archiveType": "zip", + "archiveFileName": "python-3.13.12-embed-amd64.zip", + "url": "https://www.python.org/ftp/python/3.13.12/python-3.13.12-embed-amd64.zip", + "sha256": "76f238f606250c87c6beac75dccd35ee99070a13490555936abb6cb64ecce3d0", + "executableName": "python.exe" + } + } +} diff --git a/tools/scripts/assert-python-runtime-bundle.mjs b/tools/scripts/assert-python-runtime-bundle.mjs index bde2374..ebcd8d8 100644 --- a/tools/scripts/assert-python-runtime-bundle.mjs +++ b/tools/scripts/assert-python-runtime-bundle.mjs @@ -10,6 +10,8 @@ const rootDir = path.resolve( ); const runtimeTarget = process.env.PYTHON_RUNTIME_TARGET ?? `${process.platform}-${process.arch}`; +const allowUnofficialSource = + process.env.PYTHON_RUNTIME_ALLOW_UNOFFICIAL_SOURCE === '1'; const runtimeRoot = path.join( rootDir, 'build', @@ -67,6 +69,41 @@ if (!existsSync(executablePath)) { process.exit(1); } +const source = manifest.source ?? null; +if (!allowUnofficialSource) { + if ( + !source || + typeof source !== 'object' || + source.kind !== 'official-artifact' + ) { + console.error( + [ + 'python-runtime assertion failed: runtime source must be official-artifact.', + 'Set PYTHON_RUNTIME_ALLOW_UNOFFICIAL_SOURCE=1 only for local emergency overrides.', + `Manifest: ${manifestPath}`, + ].join('\n'), + ); + process.exit(1); + } + + const artifact = source.artifact; + if ( + !artifact || + typeof artifact !== 'object' || + typeof artifact.url !== 'string' || + typeof artifact.sha256 !== 'string' || + artifact.sha256.trim().length === 0 + ) { + console.error( + [ + 'python-runtime assertion failed: official-artifact source metadata missing url/sha256.', + `Manifest: ${manifestPath}`, + ].join('\n'), + ); + process.exit(1); + } +} + const manifestPackages = Array.isArray(manifest.packages) ? manifest.packages : []; diff --git a/tools/scripts/prepare-local-python-runtime.mjs b/tools/scripts/prepare-local-python-runtime.mjs index cdbb948..a202399 100644 --- a/tools/scripts/prepare-local-python-runtime.mjs +++ b/tools/scripts/prepare-local-python-runtime.mjs @@ -1,12 +1,16 @@ import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; import { cpSync, + createWriteStream, existsSync, mkdirSync, + readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs'; +import { get } from 'node:https'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -33,6 +37,18 @@ const requirementsFilePath = process.env.PYTHON_RUNTIME_REQUIREMENTS 'python-sidecar', 'requirements-runtime.txt', ); +const artifactCatalogPath = path.join( + rootDir, + 'tools', + 'python-runtime-artifacts.json', +); +const artifactCacheRoot = path.join( + rootDir, + 'build', + 'python-runtime', + '_artifacts', + runtimeTarget, +); const prunePaths = [ 'Doc', 'Tools', @@ -41,6 +57,129 @@ const prunePaths = [ path.join('tcl', 'tk8.6', 'demos'), ]; +const loadArtifactConfig = () => { + if (!existsSync(artifactCatalogPath)) { + return null; + } + + const raw = JSON.parse(readFileSync(artifactCatalogPath, 'utf8')); + const targetConfig = raw?.targets?.[runtimeTarget]; + if (!targetConfig || typeof targetConfig !== 'object') { + return null; + } + + if ( + typeof targetConfig.url !== 'string' || + typeof targetConfig.sha256 !== 'string' || + typeof targetConfig.archiveFileName !== 'string' + ) { + throw new Error( + `Invalid artifact catalog entry for target ${runtimeTarget}.`, + ); + } + + return { + distribution: String(targetConfig.distribution ?? 'unknown'), + pythonVersion: String(targetConfig.pythonVersion ?? ''), + archiveType: String(targetConfig.archiveType ?? 'zip'), + archiveFileName: targetConfig.archiveFileName, + url: targetConfig.url, + sha256: targetConfig.sha256.toLowerCase(), + executableName: String(targetConfig.executableName ?? 'python.exe'), + }; +}; + +const sha256File = (filePath) => { + const hash = createHash('sha256'); + hash.update(readFileSync(filePath)); + return hash.digest('hex').toLowerCase(); +}; + +const downloadFile = (url, destinationPath) => + new Promise((resolve, reject) => { + const request = get(url, (response) => { + if ( + response.statusCode && + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location + ) { + const redirectUrl = new URL(response.headers.location, url).toString(); + response.resume(); + downloadFile(redirectUrl, destinationPath).then(resolve).catch(reject); + return; + } + + if (response.statusCode !== 200) { + response.resume(); + reject( + new Error( + `Failed to download runtime artifact (${response.statusCode ?? 'unknown'}): ${url}`, + ), + ); + return; + } + + mkdirSync(path.dirname(destinationPath), { recursive: true }); + const stream = createWriteStream(destinationPath); + response.pipe(stream); + stream.on('finish', () => { + stream.close(); + resolve(undefined); + }); + stream.on('error', (error) => reject(error)); + }); + + request.on('error', (error) => reject(error)); + }); + +const extractArchiveToDir = (archivePath, destinationPath) => { + rmSync(destinationPath, { recursive: true, force: true }); + mkdirSync(destinationPath, { recursive: true }); + + if (process.platform === 'win32') { + execFileSync('tar', ['-xf', archivePath, '-C', destinationPath], { + cwd: rootDir, + stdio: 'inherit', + }); + return; + } + + throw new Error( + `Runtime artifact extraction is not implemented for platform ${process.platform}.`, + ); +}; + +const enableEmbeddedSitePackages = (runtimeDir) => { + const entries = existsSync(runtimeDir) ? readdirSync(runtimeDir) : []; + const pthFiles = entries.filter((entry) => + entry.toLowerCase().endsWith('._pth'), + ); + + for (const fileName of pthFiles) { + const pthPath = path.join(runtimeDir, fileName); + const lines = readFileSync(pthPath, 'utf8').split(/\r?\n/); + const hasImportSite = lines.some( + (line) => line.trim() === 'import site' || line.trim() === '#import site', + ); + const hasSitePackages = lines.some( + (line) => line.trim() === 'Lib/site-packages', + ); + + const updatedLines = lines.map((line) => + line.trim() === '#import site' ? 'import site' : line, + ); + if (!hasImportSite) { + updatedLines.push('import site'); + } + if (!hasSitePackages) { + updatedLines.splice(1, 0, 'Lib/site-packages'); + } + + writeFileSync(pthPath, `${updatedLines.join('\n')}\n`, 'utf8'); + } +}; + const resolvePythonCommand = () => { if (process.env.PYTHON) { return { command: process.env.PYTHON, args: [] }; @@ -66,9 +205,44 @@ const pythonExecutable = execFileSync( { cwd: rootDir, encoding: 'utf8' }, ).trim(); -const sourceDir = process.env.PYTHON_RUNTIME_SOURCE_DIR +const artifactConfig = loadArtifactConfig(); +const sourceOverrideDir = process.env.PYTHON_RUNTIME_SOURCE_DIR ? path.resolve(rootDir, process.env.PYTHON_RUNTIME_SOURCE_DIR) - : path.dirname(pythonExecutable); + : null; +const sourceDir = + sourceOverrideDir ?? path.join(artifactCacheRoot, 'extracted'); + +if (!sourceOverrideDir) { + if (!artifactConfig) { + throw new Error( + `No official artifact catalog entry found for target ${runtimeTarget}. Set PYTHON_RUNTIME_SOURCE_DIR to override locally.`, + ); + } + + const artifactPath = path.join( + artifactCacheRoot, + artifactConfig.archiveFileName, + ); + const downloadedHash = existsSync(artifactPath) + ? sha256File(artifactPath) + : null; + if (downloadedHash !== artifactConfig.sha256) { + if (existsSync(artifactPath)) { + rmSync(artifactPath, { force: true }); + } + console.info(`Downloading runtime artifact: ${artifactConfig.url}`); + await downloadFile(artifactConfig.url, artifactPath); + } + + const verifiedHash = sha256File(artifactPath); + if (verifiedHash !== artifactConfig.sha256) { + throw new Error( + `Artifact checksum mismatch for ${artifactConfig.archiveFileName}. expected=${artifactConfig.sha256} actual=${verifiedHash}`, + ); + } + + extractArchiveToDir(artifactPath, sourceDir); +} const parseRequirementPackageNames = (requirementsPath) => { if (!existsSync(requirementsPath)) { @@ -89,6 +263,7 @@ const parseRequirementPackageNames = (requirementsPath) => { rmSync(outputRoot, { recursive: true, force: true }); mkdirSync(outputRoot, { recursive: true }); cpSync(sourceDir, outputRuntimeDir, { recursive: true }); +enableEmbeddedSitePackages(outputRuntimeDir); for (const relativePath of prunePaths) { rmSync(path.join(outputRuntimeDir, relativePath), { @@ -102,9 +277,10 @@ mkdirSync(outputSitePackagesDir, { recursive: true }); const executableName = process.platform === 'win32' - ? path.basename(pythonExecutable).toLowerCase().endsWith('.exe') - ? path.basename(pythonExecutable) - : 'python.exe' + ? (artifactConfig?.executableName ?? + (path.basename(pythonExecutable).toLowerCase().endsWith('.exe') + ? path.basename(pythonExecutable) + : 'python.exe')) : path.basename(pythonExecutable); const runtimePythonExecutablePath = path.join(outputRuntimeDir, executableName); @@ -173,8 +349,21 @@ const manifest = { ? path.relative(rootDir, requirementsFilePath).replaceAll('\\', '/') : null, source: { + kind: sourceOverrideDir ? 'local-override' : 'official-artifact', pythonExecutable, sourceDir, + runtimeTarget, + artifact: + sourceOverrideDir || !artifactConfig + ? null + : { + distribution: artifactConfig.distribution, + pythonVersion: artifactConfig.pythonVersion, + url: artifactConfig.url, + archiveFileName: artifactConfig.archiveFileName, + archiveType: artifactConfig.archiveType, + sha256: artifactConfig.sha256, + }, }, }; From 260c30d64563661583e41a05ea1f4ac8933980fc Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Sat, 14 Feb 2026 09:30:58 +0000 Subject: [PATCH 5/9] docs(config): add packaged runtime config env/json examples --- runtime-config.env.example | 10 ++++++++++ runtime-config.json.example | 9 +++++++++ 2 files changed, 19 insertions(+) create mode 100644 runtime-config.env.example create mode 100644 runtime-config.json.example diff --git a/runtime-config.env.example b/runtime-config.env.example new file mode 100644 index 0000000..714ec9a --- /dev/null +++ b/runtime-config.env.example @@ -0,0 +1,10 @@ +# Runtime config file for packaged builds. +# Place at: %APPDATA%\Angulectron\config\runtime-config.env + +OIDC_ISSUER=https://your-issuer.example.com +OIDC_CLIENT_ID=your-client-id +OIDC_REDIRECT_URI=http://127.0.0.1:42813/callback +OIDC_SCOPES=openid profile email offline_access +OIDC_AUDIENCE=api.your-domain.example +API_SECURE_ENDPOINT_URL_TEMPLATE=https://api.your-domain.example/users/{{user_id}}/portfolio +API_SECURE_ENDPOINT_CLAIM_MAP={"user_id":"sub"} diff --git a/runtime-config.json.example b/runtime-config.json.example new file mode 100644 index 0000000..cd84de7 --- /dev/null +++ b/runtime-config.json.example @@ -0,0 +1,9 @@ +{ + "OIDC_ISSUER": "https://your-issuer.example.com", + "OIDC_CLIENT_ID": "your-client-id", + "OIDC_REDIRECT_URI": "http://127.0.0.1:42813/callback", + "OIDC_SCOPES": "openid profile email offline_access", + "OIDC_AUDIENCE": "api.your-domain.example", + "API_SECURE_ENDPOINT_URL_TEMPLATE": "https://api.your-domain.example/users/{{user_id}}/portfolio", + "API_SECURE_ENDPOINT_CLAIM_MAP": "{\"user_id\":\"sub\"}" +} From e1035146666cd056ae09ed6c94cb42dc159e1267 Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Sat, 14 Feb 2026 09:46:02 +0000 Subject: [PATCH 6/9] feat(config): support packaged runtime config files with allowlisted keys --- .gitignore | 2 + README.md | 54 ++++++++ apps/desktop-main/src/main.ts | 15 ++ .../src/runtime-user-config.spec.ts | 105 ++++++++++++++ apps/desktop-main/src/runtime-user-config.ts | 131 ++++++++++++++++++ 5 files changed, 307 insertions(+) create mode 100644 apps/desktop-main/src/runtime-user-config.spec.ts create mode 100644 apps/desktop-main/src/runtime-user-config.ts diff --git a/.gitignore b/.gitignore index fb66ff3..763ebd2 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,8 @@ TASK.md .env* !.env.example !.env.*.example +runtime-config.env +runtime-config.json # e2e and test artifacts playwright-report/ diff --git a/README.md b/README.md index 6f6f7c2..be40afd 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,60 @@ Token persistence behavior: - If `keytar` native binding is missing, run: - `pnpm native:rebuild:keytar` +### Packaged Runtime Config File (Windows) + +Packaged builds do not read `.env.local` automatically. +Instead, place one runtime config file at: + +- `%APPDATA%\Angulectron\config\runtime-config.env` +- or `%APPDATA%\Angulectron\config\runtime-config.json` + +Supported keys (allowlisted): + +- `OIDC_ISSUER` +- `OIDC_CLIENT_ID` +- `OIDC_REDIRECT_URI` +- `OIDC_SCOPES` (must include `openid`) + +Optional: + +- `OIDC_AUDIENCE` +- `OIDC_ALLOWED_SIGNOUT_ORIGINS` (space/comma separated allowlist for global sign-out redirects) +- `API_SECURE_ENDPOINT_URL_TEMPLATE` +- `API_SECURE_ENDPOINT_CLAIM_MAP` + +Example `runtime-config.env`: + +```env +OIDC_ISSUER=https://your-issuer.example.com +OIDC_CLIENT_ID=your-client-id +OIDC_REDIRECT_URI=http://127.0.0.1:42813/callback +OIDC_SCOPES=openid profile email offline_access +OIDC_AUDIENCE=api.your-domain.example +API_SECURE_ENDPOINT_URL_TEMPLATE=https://api.your-domain.example/users/{{user_id}}/portfolio +API_SECURE_ENDPOINT_CLAIM_MAP={"user_id":"sub"} +``` + +Example `runtime-config.json`: + +```json +{ + "OIDC_ISSUER": "https://your-issuer.example.com", + "OIDC_CLIENT_ID": "your-client-id", + "OIDC_REDIRECT_URI": "http://127.0.0.1:42813/callback", + "OIDC_SCOPES": "openid profile email offline_access", + "OIDC_AUDIENCE": "api.your-domain.example", + "API_SECURE_ENDPOINT_URL_TEMPLATE": "https://api.your-domain.example/users/{{user_id}}/portfolio", + "API_SECURE_ENDPOINT_CLAIM_MAP": "{\"user_id\":\"sub\"}" +} +``` + +Notes: + +- Restart the app after changing runtime config files. +- Process environment variables still take precedence over file values if both are set. +- Do not put client secrets into renderer or checked-in files; this flow assumes desktop public-client PKCE. + ## Bring Your Own Secure API Endpoint The `call.secure-endpoint` API operation is endpoint-configurable and does not rely on a hardcoded private URL. diff --git a/apps/desktop-main/src/main.ts b/apps/desktop-main/src/main.ts index 249d2cd..768ac60 100644 --- a/apps/desktop-main/src/main.ts +++ b/apps/desktop-main/src/main.ts @@ -27,6 +27,7 @@ import { resolveAppMetadataVersion, resolveRuntimeFlags, } from './runtime-config'; +import { loadUserRuntimeConfig } from './runtime-user-config'; import { createRefreshTokenStore } from './secure-token-store'; import { StorageGateway } from './storage-gateway'; import { DemoUpdater } from './demo-updater'; @@ -354,6 +355,20 @@ const bootstrap = async () => { : undefined, }); + const runtimeConfig = loadUserRuntimeConfig(app.getPath('userData')); + if (runtimeConfig.parseError) { + logEvent('warn', 'runtime.config_file_parse_failed', undefined, { + sourcePath: runtimeConfig.sourcePath, + message: runtimeConfig.parseError, + }); + } else if (runtimeConfig.sourcePath) { + logEvent('info', 'runtime.config_file_loaded', undefined, { + sourcePath: runtimeConfig.sourcePath, + appliedKeys: runtimeConfig.appliedKeys, + skippedExistingKeys: runtimeConfig.skippedExistingKeys, + }); + } + const oidcConfig = loadOidcConfig(); demoUpdater = new DemoUpdater(app.getPath('userData')); demoUpdater.seedRuntimeWithBaseline(); diff --git a/apps/desktop-main/src/runtime-user-config.spec.ts b/apps/desktop-main/src/runtime-user-config.spec.ts new file mode 100644 index 0000000..1bb0c8d --- /dev/null +++ b/apps/desktop-main/src/runtime-user-config.spec.ts @@ -0,0 +1,105 @@ +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { loadUserRuntimeConfig } from './runtime-user-config'; + +describe('loadUserRuntimeConfig', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = path.join( + os.tmpdir(), + `runtime-user-config-${Date.now()}-${Math.random().toString(16).slice(2)}`, + ); + mkdirSync(path.join(tempDir, 'config'), { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('returns empty result when no config file exists', () => { + const env: NodeJS.ProcessEnv = {}; + const result = loadUserRuntimeConfig(tempDir, env); + expect(result.sourcePath).toBeNull(); + expect(result.appliedKeys).toEqual([]); + expect(result.skippedExistingKeys).toEqual([]); + }); + + it('loads allowed values from runtime-config.env', () => { + writeFileSync( + path.join(tempDir, 'config', 'runtime-config.env'), + [ + '# comment', + 'OIDC_ISSUER=https://issuer.example.com', + 'OIDC_CLIENT_ID=desktop-client', + 'UNSAFE_KEY=ignored', + ].join('\n'), + 'utf8', + ); + + const env: NodeJS.ProcessEnv = {}; + const result = loadUserRuntimeConfig(tempDir, env); + expect(result.parseError).toBeUndefined(); + expect(env.OIDC_ISSUER).toBe('https://issuer.example.com'); + expect(env.OIDC_CLIENT_ID).toBe('desktop-client'); + expect(env.UNSAFE_KEY).toBeUndefined(); + expect(result.appliedKeys).toEqual(['OIDC_ISSUER', 'OIDC_CLIENT_ID']); + }); + + it('respects existing env values over file values', () => { + writeFileSync( + path.join(tempDir, 'config', 'runtime-config.env'), + 'OIDC_CLIENT_ID=file-client\nOIDC_SCOPES=openid profile', + 'utf8', + ); + + const env: NodeJS.ProcessEnv = { + OIDC_CLIENT_ID: 'existing-client', + }; + const result = loadUserRuntimeConfig(tempDir, env); + expect(env.OIDC_CLIENT_ID).toBe('existing-client'); + expect(env.OIDC_SCOPES).toBe('openid profile'); + expect(result.skippedExistingKeys).toEqual(['OIDC_CLIENT_ID']); + expect(result.appliedKeys).toEqual(['OIDC_SCOPES']); + }); + + it('loads allowed values from runtime-config.json', () => { + writeFileSync( + path.join(tempDir, 'config', 'runtime-config.json'), + JSON.stringify( + { + OIDC_ISSUER: 'https://issuer.example.com', + OIDC_CLIENT_ID: 'json-client', + API_SECURE_ENDPOINT_URL_TEMPLATE: + 'https://api.example.com/users/{{user_id}}/portfolio', + UNSAFE_KEY: 'ignored', + }, + null, + 2, + ), + 'utf8', + ); + + const env: NodeJS.ProcessEnv = {}; + const result = loadUserRuntimeConfig(tempDir, env); + expect(result.parseError).toBeUndefined(); + expect(env.OIDC_ISSUER).toBe('https://issuer.example.com'); + expect(env.OIDC_CLIENT_ID).toBe('json-client'); + expect(env.API_SECURE_ENDPOINT_URL_TEMPLATE).toContain('{{user_id}}'); + expect(env.UNSAFE_KEY).toBeUndefined(); + }); + + it('returns parseError for invalid json config', () => { + writeFileSync( + path.join(tempDir, 'config', 'runtime-config.json'), + '{ invalid-json', + 'utf8', + ); + + const env: NodeJS.ProcessEnv = {}; + const result = loadUserRuntimeConfig(tempDir, env); + expect(result.parseError).toBeDefined(); + expect(result.appliedKeys).toEqual([]); + }); +}); diff --git a/apps/desktop-main/src/runtime-user-config.ts b/apps/desktop-main/src/runtime-user-config.ts new file mode 100644 index 0000000..91125ba --- /dev/null +++ b/apps/desktop-main/src/runtime-user-config.ts @@ -0,0 +1,131 @@ +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; + +const runtimeConfigFileNames = ['runtime-config.json', 'runtime-config.env']; + +const allowedRuntimeConfigKeys = new Set([ + 'OIDC_ISSUER', + 'OIDC_CLIENT_ID', + 'OIDC_REDIRECT_URI', + 'OIDC_SCOPES', + 'OIDC_AUDIENCE', + 'OIDC_SEND_AUDIENCE_IN_AUTHORIZE', + 'OIDC_API_BEARER_TOKEN_SOURCE', + 'OIDC_ALLOWED_SIGNOUT_ORIGINS', + 'API_SECURE_ENDPOINT_URL_TEMPLATE', + 'API_SECURE_ENDPOINT_CLAIM_MAP', +]); + +const stripWrappingQuotes = (value: string): string => { + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + return value.slice(1, -1); + } + + return value; +}; + +const parseEnvConfig = (raw: string): Record => { + const entries: Record = {}; + for (const line of raw.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + + const separator = trimmed.indexOf('='); + if (separator <= 0) { + continue; + } + + const key = trimmed.slice(0, separator).trim(); + const value = stripWrappingQuotes(trimmed.slice(separator + 1).trim()); + if (!key || !allowedRuntimeConfigKeys.has(key)) { + continue; + } + + entries[key] = value; + } + + return entries; +}; + +const parseJsonConfig = (raw: string): Record => { + const parsed = JSON.parse(raw) as Record; + const entries: Record = {}; + for (const [key, value] of Object.entries(parsed)) { + if (!allowedRuntimeConfigKeys.has(key) || typeof value !== 'string') { + continue; + } + + entries[key] = value; + } + + return entries; +}; + +const findRuntimeConfigPath = (userDataPath: string): string | null => { + const configDir = path.join(userDataPath, 'config'); + for (const fileName of runtimeConfigFileNames) { + const fullPath = path.join(configDir, fileName); + if (existsSync(fullPath)) { + return fullPath; + } + } + + return null; +}; + +export type RuntimeConfigLoadResult = { + sourcePath: string | null; + appliedKeys: string[]; + skippedExistingKeys: string[]; + parseError?: string; +}; + +export const loadUserRuntimeConfig = ( + userDataPath: string, + env: NodeJS.ProcessEnv = process.env, +): RuntimeConfigLoadResult => { + const sourcePath = findRuntimeConfigPath(userDataPath); + if (!sourcePath) { + return { + sourcePath: null, + appliedKeys: [], + skippedExistingKeys: [], + }; + } + + try { + const raw = readFileSync(sourcePath, 'utf8'); + const entries = sourcePath.endsWith('.json') + ? parseJsonConfig(raw) + : parseEnvConfig(raw); + const appliedKeys: string[] = []; + const skippedExistingKeys: string[] = []; + for (const [key, value] of Object.entries(entries)) { + if (typeof env[key] === 'string' && env[key]!.trim().length > 0) { + skippedExistingKeys.push(key); + continue; + } + + env[key] = value; + appliedKeys.push(key); + } + + return { + sourcePath, + appliedKeys, + skippedExistingKeys, + }; + } catch (error) { + return { + sourcePath, + appliedKeys: [], + skippedExistingKeys: [], + parseError: error instanceof Error ? error.message : String(error), + }; + } +}; From c8e83db270275330c94dbf3a82325fd652a64efb Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Sat, 14 Feb 2026 09:46:55 +0000 Subject: [PATCH 7/9] test(contracts): sanitize tenant-specific auth fixture values --- libs/shared/contracts/src/lib/contracts.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/shared/contracts/src/lib/contracts.spec.ts b/libs/shared/contracts/src/lib/contracts.spec.ts index 7e82be1..7c902ec 100644 --- a/libs/shared/contracts/src/lib/contracts.spec.ts +++ b/libs/shared/contracts/src/lib/contracts.spec.ts @@ -267,14 +267,14 @@ describe('auth contracts', () => { const parsed = authGetTokenDiagnosticsResponseSchema.safeParse({ sessionState: 'active', bearerSource: 'access_token', - expectedAudience: 'api.adopa.uk', + expectedAudience: 'api.example.com', accessToken: { present: true, format: 'jwt', claims: { - iss: 'https://willing-elephant-20.clerk.accounts.dev', + iss: 'https://issuer.example.com', sub: 'user_abc', - aud: ['api.adopa.uk'], + aud: ['api.example.com'], exp: 1770903664, iat: 1770900000, }, @@ -283,7 +283,7 @@ describe('auth contracts', () => { present: true, format: 'jwt', claims: { - aud: 'TOtjISa3Sgz2sDi2', + aud: 'desktop-client-id', }, }, }); From 722ffc5f98999c6d1b98a44de3f9d5b3cda2c970 Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Sat, 14 Feb 2026 09:53:20 +0000 Subject: [PATCH 8/9] ci(security): require no-secrets confirmation in PR checklist --- .github/pull_request_template.md | 3 ++- PR_DRAFT.md | 3 ++- tools/scripts/security-checklist-gate.mjs | 9 ++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b8e780a..11756bf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -32,10 +32,11 @@ IMPORTANT: -- If this PR touches `apps/desktop-main/**`, `apps/desktop-preload/**`, `libs/shared/contracts/**`, `.github/workflows/**`, or `docs/02-architecture/security-architecture.md`, the two items below MUST be checked to pass CI. +- If this PR touches `apps/desktop-main/**`, `apps/desktop-preload/**`, `libs/shared/contracts/**`, `.github/workflows/**`, or `docs/02-architecture/security-architecture.md`, the three items below MUST be checked to pass CI. - [ ] Security review completed - [ ] Threat model updated or N/A explained +- [ ] Confirmed no secrets/sensitive data present in committed files ### Security Notes diff --git a/PR_DRAFT.md b/PR_DRAFT.md index a639c5e..918ad79 100644 --- a/PR_DRAFT.md +++ b/PR_DRAFT.md @@ -73,10 +73,11 @@ IMPORTANT: -- If this PR touches `apps/desktop-main/**`, `apps/desktop-preload/**`, `libs/shared/contracts/**`, `.github/workflows/**`, or `docs/02-architecture/security-architecture.md`, the two items below MUST be checked to pass CI. +- If this PR touches `apps/desktop-main/**`, `apps/desktop-preload/**`, `libs/shared/contracts/**`, `.github/workflows/**`, or `docs/02-architecture/security-architecture.md`, the three items below MUST be checked to pass CI. - [x] Security review completed - [x] Threat model updated or N/A explained +- [x] Confirmed no secrets/sensitive data present in committed files ### Security Notes diff --git a/tools/scripts/security-checklist-gate.mjs b/tools/scripts/security-checklist-gate.mjs index fcd8534..3648860 100644 --- a/tools/scripts/security-checklist-gate.mjs +++ b/tools/scripts/security-checklist-gate.mjs @@ -57,14 +57,21 @@ const hasSecurityReview = /- \[x\] Security review completed/i.test(body); const hasThreatModel = /- \[x\] Threat model updated or N\/A explained/i.test( body, ); +const hasNoSecretsConfirmation = + /- \[x\] Confirmed no secrets\/sensitive data present in committed files/i.test( + body, + ); -if (!hasSecurityReview || !hasThreatModel) { +if (!hasSecurityReview || !hasThreatModel || !hasNoSecretsConfirmation) { console.error( 'Security-sensitive change requires completed security checklist in PR body.', ); console.error('Expected checked items:'); console.error('- [x] Security review completed'); console.error('- [x] Threat model updated or N/A explained'); + console.error( + '- [x] Confirmed no secrets/sensitive data present in committed files', + ); process.exit(1); } From 7c2ec0a78815296fb69c106b2be626a12de9c84a Mon Sep 17 00:00:00 2001 From: Simon Hagger Date: Sat, 14 Feb 2026 09:57:39 +0000 Subject: [PATCH 9/9] chore(ci): retrigger checks after PR checklist update