feat(pv): P9 providers & registry — Codex + Opencode + registry/routing/secret/home-fs#450
Conversation
Cut feature/providers-registry off next (P0-P8 merged). Scope = parity-charter §3.6 multi-provider — ProviderRegistryPort + Codex (app-server JSON-RPC) + Opencode (ACP) + shared ACP transport + model routing/capabilities/workspace registry. POSTURE (§6a): Claude complete, Codex/Opencode capability-gated + feature-incomplete OK. Key ADRs: ProviderRegistryPort + routing seam, HomeFsPort (beyond-vault security), SecretStorePort (native secret storage, lands this phase + minAppVersion), ACP/ JSON-RPC transports. Largest phase. Autonomous full-epic drive. Next: /spec:requirements. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 EARS REQ-PV (registry/selection · routing · capabilities matrix · Codex · Opencode · ACP transport · model routing · secret storage · home-fs/history · settings UI · security · additivity) + 14 NFR-PV + 7 CLAR-PV (resolved-by-recommendation). BINDING posture: Claude COMPLETE default, Codex/Opencode CAPABILITY-GATED + feature-incomplete OK. Per-provider matrix grounded in claudian capabilities.ts. ProviderRegistryPort = data-driven routing (no switch(id), NFR-PV-014). SecretStorePort → app.secretStorage never data.json; HomeFsPort beyond-vault read-scoped/consented. minAppVersion: keep 1.12.7 + capability-gate (escalate if app.secretStorage forces a bump). Claude-only = byte-identical P0-P8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR-PV-001 ProviderRegistryPort + data-driven routing seam (widens CHAT_RUNTIME_FACTORY to (providerId)->Result, parameterises createProviderHistoryPort/getCatalog by providerId; capability-flag-gated UI, NO switch(providerId); Claude-only = byte-identical P8). ADR-PV-002 SecretStorePort -> app.secretStorage, never data.json/device-local/log/DTO; in-memory Mock/LS; capability-gate when unavailable; minAppVersion keep-1.12.7 + escalate-not-bump. ADR-PV-003 HomeFsPort read-scoped/consented beyond-vault (~/.codex, ~/.claude) read-only + coverage-excluded Codex JSON-RPC + shared ACP transports behind the registry; history into the unchanged P3 ProviderHistoryPort. No new runtime dep (thin in-tree JSON-RPC/ACP). Frozen capability matrix (Claude complete; Codex/Opencode capability-gated). Components ProviderChooser/ProviderOption/ProviderSecretField + provider-aware selectors. Minimal selection surface (full per-provider settings = P10). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 layer groups, 34 SPEC items. Pins ProviderRegistryPort (pure reads: list/enable/getDescriptor/getCapabilities/resolveActiveProvider/resolveProviderForModel), SecretStorePort (isAvailable/get/set/delete/listKeys, provider.<id>.apiKey), HomeFsPort (read-only, HOME_FS_ROOTS=.codex/.claude, path-escape->err), ProviderDescriptor (frozen capability bag + isEnabled/ownsModel), widened ChatRuntimeFactory (providerId)->Result + OPEN_PROVIDER_CONSENT. Codex JSON-RPC + ACP transports coverage-excluded. Frozen matrix (BACKED caps only, GATED-OFF honest-false, NG1). Claude-only = byte-identical P8 (SPEC-PV-027). Manual legs M1 Codex / M2 Opencode / M3 secret / M4 screenshots. Full coverage table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T-PV-001 baseline+guard-verify; DOMAIN 002..010 (T-PV-010 widens CHAT_RUNTIME_FACTORY (providerId)->Result with full call-site fan-out), INFRA 011..018, APPLICATION 019..024, UI 025..032, STYLES 033, WIRE-IN 034..036, GATE 037..044. NO guard-relax — but the active ObsidianSecretStore* ban handled via file-naming (SecretStorage.ts/HomeFileSystem.ts, NOT ObsidianSecretStore*; transports at obsidian/ root). Real Codex JSON-RPC / Opencode ACP / app.secretStorage / ~/.codex reads coverage-excluded → manual legs T-PV-040..043 (TEST-PV-M1/M2/M3/M4). Capability-matrix discipline: BACKED only, honest-false GATED-OFF. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scaffolds parity-screenshots.md (claudian baseline col), test-plan.md (guard-verify + manual legs TEST-PV-M1/M2/M3/M4 + Obsidian-infra naming directive), implementation-log.md. Records the SecretStorePort/SECRET_STORE_PORT P9-regrow guard-relax (ICON_PORT precedent) needed in T-PV-009 — the planner's no-relax verdict missed the P0-deleted secret symbols. No src/ change. SPEC-PV-002/008/009/010/022, NFR-PV-007/009. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ields Asserts ProviderId widens to 'claude'|'codex'|'opencode' (exactly three, 'claude' stays assignable) + PluginSettings.activeProvider (default 'claude') + enabledProviders (default []). RED: type-level + runtime fail until T-PV-003. TEST-PV-005, TEST-PV-114, SPEC-PV-001/027, REQ-PV-005/103/114, NFR-PV-001. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Widens ProviderId to 'claude'|'codex'|'opencode' (additive). Appends activeProvider (default 'claude') + enabledProviders (default []) to PluginSettings + DEFAULT_SETTINGS with pure coerceActiveProvider/ coerceEnabledProviders load-or-default helpers. Same-task additive fan-out: core-settings + ObsidianBridge coerce the two fields; the round-trip fixture + exact-key guard grow. Greens TEST-PV-005 + TEST-PV-114. SPEC-PV-001/027, REQ-PV-005/103/114, NFR-PV-001. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Asserts the ProviderCapabilities/ProviderDescriptor shapes, the three frozen descriptors per the SPEC-PV-022 truth table (claude all-true; codex rewind/commands/MCP off, steer/fork on; opencode rewind/fork/steer/MCP off, commands on; reasoningControl effort; needsApiKey/readsHomeDir per provider), Object.freeze, distinct blankTabOrder 10/15/20, isEnabled (claude-always / non-claude membership), the pure ownsModel predicate, PROVIDER_DESCRIPTORS, DEFAULT_CHAT_PROVIDER_ID, never-throws. RED until T-PV-005. TEST-PV-020/021/022/023, SPEC-PV-002/022, REQ-PV-001/020/021/022/023/103, NFR-PV-014. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the ProviderCapabilities/ProviderDescriptor interfaces + the three Object.freeze'd descriptors per the SPEC-PV-022 matrix (claude all-true; codex rewind/commands/MCP off, steer/fork on; opencode rewind/fork/steer/MCP off, commands on; reasoningControl effort; needsApiKey/readsHomeDir per provider) + PROVIDER_DESCRIPTORS + DEFAULT_CHAT_PROVIDER_ID. Pure isEnabled (claude-always / non-claude membership) + ownsModel predicates grounded verbatim in claudian. BACKED caps wired, GATED-OFF literal false (NG1). No switch(providerId). Greens TEST-PV-020/021/022/023. SPEC-PV-002/022, REQ-PV-001/020/021/022/023/103, NFR-PV-014. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Asserts listEnabledProviders (isEnabled-filtered, blank-tab order, claude always present, fresh array), resolveActiveProvider (recorded-if-enabled else claude; no-record/disabled fallback), resolveProviderForModel (ownsModel match else active/claude fallback), and never-throws. RED until T-PV-007. TEST-PV-002/003/060/061, SPEC-PV-003/029, REQ-PV-002/003/006/060/061, NFR-PV-014, EC-PV-2/3/9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds listEnabledProviders (isEnabled-filter + blankTabOrder sort, fresh array, claude always present), resolveActiveProvider (recorded-if-enabled else claude), resolveProviderForModel (first ownsModel else active/claude fallback). Pure + total, no switch(providerId). Greens TEST-PV-002/003/060/061 + EC-PV-2/3/9. SPEC-PV-003/029, REQ-PV-002/003/006/060/061, NFR-PV-014, EC-PV-2/3/9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t shapes Asserts ProviderRegistryPort (7 pure-sync reads), SecretStorePort (isAvailable + getSecret/setSecret/deleteSecret/listKeys Result-typed + providerSecretKey = provider.<id>.apiKey), HomeFsPort (isAvailable + readFile/exists/listFolders, no write/delete, HOME_FS_ROOTS [.codex,.claude]), the 3 own keys + the barrel re-exports. RED until T-PV-009. TEST-PV-112, SPEC-PV-004/006/007, REQ-PV-001/070..073/080..083/112, NFR-PV-006. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…keys + barrel Adds the three narrow read ports (ProviderRegistryPort 7 pure-sync reads; SecretStorePort isAvailable + 4 Result methods + providerSecretKey; HomeFsPort read-first isAvailable/readFile/exists/listFolders + HOME_FS_ROOTS, no write/delete) + the PROVIDER_REGISTRY_PORT/SECRET_STORE_PORT/HOME_FS_PORT keys + barrel re-exports. Greens TEST-PV-112. Guard-relax (P9 secret-seam regrow, ICON_PORT precedent): drops the stale P0-deleted @/domain/ports/SecretStorePort + SECRET_STORE_PORT from the deleted-symbol guard; the Obsidian-layer ObsidianSecretStore* glob stays banned. SPEC-PV-004/006/007, REQ-PV-001/070..073/080..083/112, NFR-PV-006, ADR-PV-002. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ONSENT seam Extends modalSeam.ts.test.ts: ChatRuntimeFactory widens to (providerId)=>Result<ChatRuntimePort> (construct-fail = Result.err not throw); useChatRuntimeFactory still throws-when-absent; OpenProviderConsentFn + OPEN_PROVIDER_CONSENT key; useOpenProviderConsent auto-declines (false) when absent. The P3-P8 handles stay byte-identical. RED until the widen (T-PV-010 green). TEST-PV-010/011/082/113/114, SPEC-PV-005/031, REQ-PV-010/011/012/082/113/114, NFR-PV-001/008. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + fan-out Widens ChatRuntimeFactory to (providerId)=>Result<ChatRuntimePort> (construct-fail = Result.err not throw) + appends OpenProviderConsentFn + OPEN_PROVIDER_CONSENT + useOpenProviderConsent (auto-decline false fallback). Same-task interface-change fan-out keeping vue-tsc 0 whole-project: AgentSidebarView + main.ts provide (providerId)=>ok(createChatRuntime()); ChatSurface adapts the widened seam to the UNCHANGED P3 store binding (default 'claude', unwrap); the 7 ChatSurface mount fixtures wrap their factory in ok(). Byte-identical at runtime to P8 for 'claude'. Greens TEST-PV-010/011/082/113/114. SPEC-PV-005/031, REQ-PV-010/011/012/082/113/114, NFR-PV-001/008. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the T-PV-010 commit SHA in implementation-log.md and updates workflow-state.md (stage->implementation/in-progress; DOMAIN batch T-PV-001..010 done; hand-off to the INFRA batch). Escalates the planner guard-verification defect (SECRET_STORE_PORT/SecretStorePort were still banned; resolved via the ICON_PORT per-phase regrow precedent in T-PV-009). SPEC-PV-001..007, NFR-PV-001. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements SPEC-PV-008 (TEST-PV-001/002/003/013/020/060/061). Data-driven registry over the frozen PROVIDER_DESCRIPTORS + pure resolveProvider helpers; no switch(providerId) (NFR-PV-014). Coverage-included pure data. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ome-fs + fake-ports Implements SPEC-PV-011 (TEST-PV-011/050..053/070..073/080..083/100). Scriptable per-provider runtime registry (construct gate + stream/timeout/error-chunk), in-memory SecretStorePort (availability switch), inert/seedable HomeFsPort (path-escape rule), shared on MockBridge + fake-ports members. No node:*/obsidian. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + home-fs Implements SPEC-PV-012 (TEST-PV-073/083/100 LS legs). Demo runtime registry (Claude fixture ok, non-Claude err 'unavailable' — degrades), in-memory SecretStorePort (isAvailable true), inert HomeFsPort (isAvailable false). No node:*. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements SPEC-PV-010/026 (manual legs TEST-PV-M1/M2). Shared in-tree line-delimited JSON-RPC 2.0 channel (bounded spawn, per-request timeout->err, dying-subprocess->terminal error chunk, SIGTERM->SIGKILL 3s); CodexRpcTransport (turn stream + steer) + AcpTransport (initialize + prompt, no steer). No new dep; coverage-excluded. No obsidian symbol leak. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…HomeFileSystem Implements SPEC-PV-009/031/034 (manual legs TEST-PV-M1/M2/M3). Data-driven runtime registry (Claude reuse / Codex JSON-RPC / Opencode ACP, no switch), real app.secretStorage SecretStorePort (never data.json), real node:fs HomeFsPort (root-scoped, path-escape->err). Files named per the T-PV-001 directive (SecretStorage.ts/HomeFileSystem.ts, never ObsidianSecretStore*). Coverage-excluded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
INFRA batch complete (commits 7af60ea/50a0fdd7/58f53787/dcba7b99/988d7997). Records verification (vue-tsc 0, lint 0 errors, 1877 tests), file-naming confirmation, registry/bridge/transport notes, deviations, manual legs, and the APPLICATION-batch hand-off. implementation-log.md stays in-progress (APP/UI/ STYLES/WIRE-IN/GATE + manual legs remain). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t-gate Author the failing unit tests for the provider select use case over the scriptable Mock registry + runtime-registry factory + in-memory settings: select (persist device-local / construct-ok / construct-err honest-notice / reset-prior / no-throw / no-secret-leak), selectForModel (auto-switch / no-op / unowned-fallback), and the no-switch(providerId) source guard. RED: the module does not yet exist. Implements SPEC-PV-013/023/029. REQ-PV-004/010/011/012/060/061/071/100/102. TEST-PV-004/010/011/012/060/071/100. NFR-PV-005/014. EC-PV-4/5/8/13. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…struct Implement the provider select use case: select(id, prior) tears down the prior runtime (reset+cancel), persists activeProvider device-local via read-modify- write SettingsPort (never data.json/secret), then constructs the active runtime through the widened (providerId)=>Result factory — a construct err surfaces an honest secret-free notice and returns the err (chat stays usable, never throws). selectForModel auto-switches to the model's owning provider or no-ops on prior. Capability-gated routing, never switch(providerId). Barrel re-export added. Implements SPEC-PV-013/023/029. REQ-PV-004/010/011/012/060/061/071/100/102. NFR-PV-005/014. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… SHA Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ted widgets Author the failing unit tests for the pure chooser/widget view-model: options (blank-tab-ordered rows + isActive/isDefault), showChooser = enabled>1 (single- Claude → false, byte-identical P8; empty list total), widgets read the active capability bag field-for-field (Claude all-but-steer; Codex no rewind/commands/ MCP, fork+steer+service-tier on; Opencode no rewind/fork/steer/MCP, commands on), plus the no-switch(providerId) source guard + never-throws. RED: module absent. Implements SPEC-PV-015/029. REQ-PV-002/006/013/024/034/043/062/063/064/090/114. TEST-PV-006/013/024/034/043/062/063/064/090. NFR-PV-014. EC-PV-1/14/15. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ated widgets Implement the pure, total provider view-model: options maps the blank-tab-ordered enabled descriptors to rows with isActive/isDefault; showChooser = enabled>1 (single-Claude → false, byte-identical P8); widgets read the active capability bag field-for-field (rewind/fork/turn-steer/provider-commands/MCP/reasoning + service- tier gated on the turn-steer/Codex config). DTO-only, never branched on the provider id, never throws. ProviderOptionVM/ProviderWidgetVM/ProviderViewModel + barrel. Implements SPEC-PV-015/029. REQ-PV-002/006/013/024/034/043/062/063/064/090/114. NFR-PV-014. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author the failing unit tests for the consent gate over the in-memory Mock settings + a stubbed openConsent: recorded-true → ok(true) no prompt (EC-PV-6); no-record → openConsent once, record the accept device-local, ok(true); declining → ok(false) persisted (no re-prompt); the auto-decline launcher → ok(false) recorded; per-provider records do not clobber each other; never throws. RED: the gate module does not yet exist. Implements SPEC-PV-014/024. REQ-PV-082/113/114. TEST-PV-082. NFR-PV-003/005. EC-PV-6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement the consent gate: ensureConsent(id) reads provider.homeFsConsent.<id> device-local — a recorded accept/decline returns without a prompt (one-time, no re-prompt); no record opens the modal seam once (auto-decline when absent), records the boolean outcome device-local, returns ok(outcome). Declining → ok(false) so the caller disables history honestly; never throws; never window.confirm; never a secret. Adds the OPTIONAL homeFsConsent settings field (absent from DEFAULT_SETTINGS, non-breaking) + homeFsConsentKey helper required by the spec's device-local record. Implements SPEC-PV-014/024. REQ-PV-082/113/114. NFR-PV-003/005. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…scalation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Record the APPLICATION batch as done in workflow-state (SelectProviderUseCase, ProviderConsentGate, buildProviderViewModel), the verification performed, and the homeFsConsent ObsidianBridge round-trip escalation for the WIRE-IN batch. Next agent: UI batch T-PV-025..032. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…HomeFsPort RED inject-or-throw composable tests mirroring useVaultPort/useToolbarCatalogPort (ADR-008 one-port-one-composable, no aggregate). Returns the injected port when provided; throws a clear "was not provided" error otherwise. TEST-PV-112 (composable leg). SPEC-PV-019. REQ-PV-112, NFR-PV-006. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…FsPort The three P9 port composables, mirroring useVaultPort's inject-or-throw (ADR-008 one-port-one-composable, no aggregate). Each injects its own key (PROVIDER_REGISTRY_PORT / SECRET_STORE_PORT / HOME_FS_PORT), returns the port, throws a clear "was not provided" error when unprovided. No obsidian/node:* under src/ui. The prior RED TEST-PV-112 composable leg now passes. SPEC-PV-019. REQ-PV-112, NFR-PV-006. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author the failing component tests + co-located data-testid PageObjects per SPEC-PV-016: ProviderChooser renders nothing at showChooser=false (byte-identical P8), lists enabled providers in blank-tab order with icon + active marker + select-emits when true; ProviderOption is one keyboard-operable row. A11y: accessible name, aria-current active announce, text+icon cues (never colour-only), no v-html. Traces TEST-PV-001/002/006/090/110/113/114 (A legs), REQ-PV-001/002/003/004/006/090/110/113/114. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement src/ui/chat/providers/ProviderChooser.vue + ProviderOption.vue per SPEC-PV-016: presentational props-in/events-out; chooser renders nothing at showChooser=false (byte-identical P8), else a role=listbox of provider rows in blank-tab order; each row keyboard-operable (Enter/Space), announces active via aria-current, conveys state by text+icon (never colour-only). Adds the agent.chat.providers.* i18n keys (en+de, locale-parity preserved) + the Codex/ Opencode/Claude/API brand allowlist entries. No obsidian/v-html. Implements SPEC-PV-016/030. Traces REQ-PV-001/002/003/004/006/090/110/113/114, NFR-PV-006/007/008/009. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author the failing component test + co-located data-testid PageObject per SPEC-PV-018: masked type=password input + save(value)-emits; the typed value is never echoed into the DOM/markup (REQ-PV-102); disabled-with-unavailable-message (no plain-store fallback) when available=false (EC-PV-10); accessible name; no v-html. Traces TEST-PV-070/072/092/102/110 (A legs), REQ-PV-070/072/092/102/110, NFR-PV-002/008/009. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement src/ui/chat/providers/ProviderSecretField.vue per SPEC-PV-018: presentational masked type=password input; emits save(value) on submit/click; the typed secret lives only in a transient local ref, cleared on emit, never echoed into the DOM value attribute / notice / log / store / DTO (REQ-PV-102, NFR-PV-002); disabled with the honest providers.secret.unavailable message and no plain-store fallback when available=false (EC-PV-10). Accessible name + label association; no obsidian/v-html. Implements SPEC-PV-018/025/030. Traces REQ-PV-070/072/092/102/110, NFR-PV-002/006/008/009. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…derId) guard Extend the P6 ModelSelector PO + test per SPEC-PV-017: an additive optional providerId prop renders the opencode-model-picker shape when the active provider is opencode; absent / claude stays byte-identical P6 (NFR-PV-001). Add a source-level no-switch(providerId)/no-provider-equality guard over the toolbar widgets + the provider components (SPEC-PV-029). Traces TEST-PV-013/062 (A legs), REQ-PV-013/062, NFR-PV-001/014. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement SPEC-PV-017 at the component layer: ModelSelector gains an additive optional providerId prop that selects a per-provider picker variant from a data-driven PICKER_VARIANT map (the opencode-model-picker shape) — a pure lookup, never a switch(providerId)/provider-id branch (NFR-PV-014); absent / claude renders byte-identical P6 (NFR-PV-001). ToolbarStrip threads the optional providerId through to ModelSelector. The ThinkingSelector/ServiceTierToggle + rewind/fork/steer/MCP/ provider-command affordances already gate on the capability bag (getToolbarCapabilities / getCapabilities / buildProviderViewModel), so P9 supplies the per-provider flags and the gating "just works". Harden the no-switch source guard to strip comments. Implements SPEC-PV-017/029/030. Traces REQ-PV-013/024/034/043/062/063/064, NFR-PV-001/006/008/014. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tick the T-PV-027..032 DoD boxes; update workflow-state (last_agent, Stage 7 row, implementation-log artifact = in-progress with STYLES/WIRE-IN/GATE + manual legs remaining); append the UI-batch hand-off note (component+PO inventory, degrade-when- absent, the parent-owned WIRE-IN scope boundary, next agent = STYLES T-PV-033). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lice Implements SPEC-PV-021. Adds the section 4.16 token slice (four minted tokens: --sp-provider-brand-claude/-codex/-opencode aliasing the section 4.2 brand literals + --sp-model-picker-group-gap reusing --sp-space-5) with an ASCII-only comment for the lightningcss pass; applies the slice to ProviderOption.vue (per-provider brand swatch) + the opencode-model-picker variant in ModelSelector.vue; extends tests/ui/styles/tokens.test.ts with the §4.16 presence + no-leak guard (TEST-PV-091) and re-bounds the §4.15 leak guard. REQ-PV-091, NFR-PV-010, TEST-PV-091. SPEC-PV-021. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests/ui/chat/ChatSurface.providers.test.ts (provides PROVIDER_REGISTRY_PORT + SETTINGS_PORT + the widened CHAT_RUNTIME_FACTORY spy + a recording ToolbarCatalogPort; asserts the chooser mounts + lists enabled providers in blank-tab order, selecting a provider routes through SelectProviderUseCase so the factory is re-called with the selected id + the selection persists device-local, the toolbar reads getCatalog(active), and single-Claude / no-registry = no chooser = byte-identical P8) + the ChatSurface.po.ts chooser PageObject helpers. RED: 4 fail / 2 pass. REQ-PV-010/012/062/082/084/114, NFR-PV-001. SPEC-PV-020/031/034. TEST-PV-010/012/062/084/112/114. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rface Implements SPEC-PV-020/031/034. ChatSurface injects PROVIDER_REGISTRY_PORT, resolves the active provider from the registry + settings, passes it to the widened CHAT_RUNTIME_FACTORY(providerId) per tab, mounts the ProviderChooser (hidden at <=1 enabled = byte-identical P8), routes a selection through SelectProviderUseCase + tabs.rebindActiveRuntime (gating a readsHomeDir provider through ProviderConsentGate), and reads getCatalog(activeProvider). Adds the tabsStore.rebindActiveRuntime action (provider-switch path, EC-PV-13). AgentSidebarView provides the three ObsidianBridge ports + the registry-routed widened factory + the ProviderConsentModal launcher; src/ui/main.ts provides the Mock equivalents + a browser-safe consent stand-in. The Mock runtime registry construction becomes a data-driven builder table whose claude entry reuses the P1 MockChatRuntime so the standalone demo stays byte-identical P8 (NFR-PV-001). REQ-PV-010/012/062/082/084/114, NFR-PV-001/006/008/014. SPEC-PV-020/031/034. TEST-PV-010/012/062/084/112/114. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the SPEC-PV-014/024 device-local consent persistence. Adds the pure coerceHomeFsConsent helper and load-or-defaults homeFsConsent in BOTH coercion sites (ObsidianBridge._coerceSettings + core-settings.validateSettings) — the prior explicit key list DROPPED it, so a recorded one-time beyond-vault consent would not survive a production reload (the gate re-prompted every reload). Adds a save->reload round-trip test (REQ-PV-082, EC-PV-6) + a byte-identical-absent test (NFR-PV-001) + a standalone providers smoke dev leg (the provider-wired surface mounts, no chooser on the single-Claude default). Moves the mount.ts.test.ts runtime spy to the new providerRuntimeRegistry.createChatRuntime factory seam (the only test broken by the T-PV-035 factory routing; the inline-ask assertion is unchanged). REQ-PV-082, NFR-PV-001/006. SPEC-PV-014/024. TEST-PV-006/062/072/090/100 (dev legs), EC-PV-6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…terms guard The P9 provider secret field (agent.chat.providers.secret.*) + the key-required notice (agent.chat.providers.notice.keyRequired) legitimately say "API key" — the user is entering one; it is a credential-configuration affordance sharing the settings-context exception (NFR-MPS-011). Add both to ALLOWED_PREFIXES. Guard 1/1 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
REVIEW-PV-001 verdict approved-with-conditions (0 P1, 0 P2, 3 P3, 4 P4). Security confirmed: SecretStorePort→app.secretStorage only (never data.json/log/DTO), HomeFsPort read-only/scoped/consented (path-escape→err), stdio bounded/no-shell, honest gate, NO switch(providerId) (source-guard 6/6). Live wiring + dual provide + consent + homeFsConsent round-trip + getCatalog un-hardcode verified; capability matrix verbatim parity; file-naming ban honoured (SecretStorage/HomeFileSystem); scoped guard-relax. TRACE-PV-001 matrix; manual legs TEST-PV-M1/M2/M3/M4 + minAppVersion check pending. P3 follow-ups (Codex stream-park / service-tier proxy / history mapping) non-blocking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shipped CSS rebuilt for P9 — ProviderChooser/ProviderOption/ProviderSecretField + the opencode-model-picker variant + the §4.16 provider brand-swatch/model-picker token slice. Gate green: typecheck 0, lint 0, vitest 1945 passed (3 load-induced teardown leaks under the 22-min run, not reproduced in a focused run; all tests pass), build (SDK bundled 1.76MB) + build:web (346kB) + docs:api clean, npm audit (high) clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c907eff342
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const absolute = resolve(home, normalize(relativePath)); | ||
| const allowed = HOME_FS_ROOTS.some((root) => { | ||
| const rootAbs = join(home, root); | ||
| return absolute === rootAbs || absolute.startsWith(`${rootAbs}${sep}`); | ||
| }); |
There was a problem hiding this comment.
Canonicalize home paths before allowlist check
The containment check here only validates the lexical path prefix and never resolves symlinks, so a path like .codex/link/secrets (where ~/.codex/link points outside the home roots) is incorrectly treated as allowed. That lets provider reads escape ~/.codex/~/.claude despite the documented boundary, which is a security regression for beyond-vault file access.
Useful? React with 👍 / 👎.
| <ProviderChooser | ||
| v-if="providerVm !== undefined" | ||
| :options="providerVm.options" | ||
| :show-chooser="providerVm.showChooser" | ||
| @select="onSelectProvider" |
There was a problem hiding this comment.
Expose secret-save UI for keyed providers
This surface only mounts the chooser, but never renders/wires ProviderSecretField (and there is no SecretStorePort.setSecret(...) call path), so users cannot enter API keys from the app. Since CodexRuntime/OpencodeRuntime require a stored key before any turn runs, selecting those providers can succeed while every query remains stuck on keyRequired, effectively making non-Claude providers unusable without out-of-band secret injection.
Useful? React with 👍 / 👎.
P9 — Providers & Registry (claudian-reboot, the largest phase)
Multi-provider: registry + Codex (app-server JSON-RPC) + Opencode (ACP) + secret/home-fs ports (charter §3.6). Spec:
specs/providers-registry/(PRD-PV-001, DESIGN-PV-001, SPEC-PV-001..034, ADR-PV-001..003, TASKS-PV-001 = 44 tasks).Delivered (T-PV-001..036 + gate)
ProviderRegistryPort+ the widenedChatRuntimeFactory (providerId)→Resultrouting seam — data-driven, noswitch(providerId)(NFR-PV-014, source-guard test). Claude reuses the P1 runtime; the active provider routes its runtime + capabilities +getCatalog(providerId).capabilities.ts): Claude complete; Codex/Opencode capability-gated, GATED-OFF caps honest-false (NG1).JsonRpcStdioChannel), no new runtime dep; real transports coverage-excluded.SecretStorePort→app.secretStorageonly (neverdata.json/log/DTO;SecretStorage.ts);HomeFsPortread-only, rooted atos.homedir(), scoped to~/.codex/~/.claude, path-escape→err, consented once viaProviderConsentGate(HomeFileSystem.ts). TheObsidianSecretStore*ESLint ban honoured via these names; the deleted-symbol guard relax was scoped (only the regrownSecretStorePort/SECRET_STORE_PORT).ProviderChooser/ProviderOption/ProviderSecretField+ provider-awareModelSelector/opencode-model-picker; provider tokens; en+de i18n. Wired inAgentSidebarView+main.ts+ChatSurface/tabsStore;homeFsConsentround-trips via_coerceSettings. Single-Claude / ports-absent = byte-identical P8 (NFR-PV-001).Gate (local, green)
vue-tsc0 ·eslint0 · vitest 268 files / 1945 tests pass (3 unhandled teardown leaks under a 22-min heavy-load run — not reproduced in a focused run; all tests pass) ·build(SDK bundled) +build:web(346kB) +docs:apiclean ·npm audit --audit-level=highclean.Parity self-review (Stage 9)
review.mdapproved-with-conditions (0 P1/P2). Security confirmed (secret→app.secretStorage only, home-fs read-only/scoped/consented, stdio bounded, honest gate, no-switch). Live wiring + dual provide + consent + homeFsConsent round-trip + getCatalog un-hardcode verified; capability matrix verbatim parity; file-naming ban honoured. P3 follow-ups (Codex stream-park, service-tier proxy, history mapping) non-blocking.Deferred — final epic gate
Manual legs TEST-PV-M1 (real Codex JSON-RPC + JSONL history + turn-steer), M2 (real Opencode ACP), M3 (real
app.secretStorageround-trip + theminAppVersionavailability check), M4 (parity screenshots), the livenpm run devflow.🤖 Generated with Claude Code