Skip to content

feat(pv): P9 providers & registry — Codex + Opencode + registry/routing/secret/home-fs#450

Merged
Luis85 merged 53 commits into
nextfrom
feature/providers-registry
May 26, 2026
Merged

feat(pv): P9 providers & registry — Codex + Opencode + registry/routing/secret/home-fs#450
Luis85 merged 53 commits into
nextfrom
feature/providers-registry

Conversation

@Luis85
Copy link
Copy Markdown
Owner

@Luis85 Luis85 commented May 26, 2026

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 widened ChatRuntimeFactory (providerId)→Result routing seam — data-driven, no switch(providerId) (NFR-PV-014, source-guard test). Claude reuses the P1 runtime; the active provider routes its runtime + capabilities + getCatalog(providerId).
  • Frozen capability matrix (verbatim from claudian capabilities.ts): Claude complete; Codex/Opencode capability-gated, GATED-OFF caps honest-false (NG1).
  • Codex — JSON-RPC-over-stdio runtime + JSONL history + turn-steer (BACKED); Opencode — shared ACP transport + ACP history + provider-commands (BACKED). Thin in-tree transports (JsonRpcStdioChannel), no new runtime dep; real transports coverage-excluded.
  • SecretStorePortapp.secretStorage only (never data.json/log/DTO; SecretStorage.ts); HomeFsPort read-only, rooted at os.homedir(), scoped to ~/.codex/~/.claude, path-escape→err, consented once via ProviderConsentGate (HomeFileSystem.ts). The ObsidianSecretStore* ESLint ban honoured via these names; the deleted-symbol guard relax was scoped (only the regrown SecretStorePort/SECRET_STORE_PORT).
  • UIProviderChooser/ProviderOption/ProviderSecretField + provider-aware ModelSelector/opencode-model-picker; provider tokens; en+de i18n. Wired in AgentSidebarView + main.ts + ChatSurface/tabsStore; homeFsConsent round-trips via _coerceSettings. Single-Claude / ports-absent = byte-identical P8 (NFR-PV-001).

Gate (local, green)

vue-tsc 0 · eslint 0 · 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:api clean · npm audit --audit-level=high clean.

Parity self-review (Stage 9)

review.md approved-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.secretStorage round-trip + the minAppVersion availability check), M4 (parity screenshots), the live npm run dev flow.

🤖 Generated with Claude Code

Symprowire and others added 30 commits May 26, 2026 08:38
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>
Symprowire and others added 23 commits May 26, 2026 11:30
…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>
@Luis85 Luis85 merged commit 4cc6559 into next May 26, 2026
6 checks passed
@Luis85 Luis85 deleted the feature/providers-registry branch May 26, 2026 17:41
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +78 to +82
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}`);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +832 to +836
<ProviderChooser
v-if="providerVm !== undefined"
:options="providerVm.options"
:show-chooser="providerVm.showChooser"
@select="onSelectProvider"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants